An issue when getting data from an API in Flutter with Dart - flutter

I'm having an issue with my app, I'm creating a flutter app to track cryptocurrency prices.
The issue is that I get the data properly from the API, then I print it into the counsel but when I try to display it inside the app, it displays null.
Here is the code I use to get the data from the API
class CurrencyData { var decodedData;
Future getCoinsData() async {
http.Response response =
await http.get(coinUrl);
if (response.statusCode == 200) {
decodedData = jsonDecode(response.body);
} else {
print(response.statusCode);
throw 'Problem with the request, try again later!';
}
return decodedData;
}
}
Here is the code where I call the data to display it.
class _DashboardPageState extends State<DashboardPage> {
CurrencyData currencyData = CurrencyData();
var btcPrice;
var btcChange24h;
void cryptoCurrencyData() async {
var data = await currencyData.getCoinsData();
print(btcPrice = data['data'][0]['priceUsd']);
print(btcChange24h = data['data'][0]['changePercent24Hr']);
}
#override
void initState() {
super.initState();
cryptoCurrencyData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView(
children: <Widget>[
Column(
children: <Widget>[
// the top bar
Container(
padding: EdgeInsets.all(40),
constraints: BoxConstraints.expand(height: 175),
decoration: BoxDecoration(
color: Colors.lightBlue,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 20.0,
// has the effect of softening the shadow
spreadRadius:
5.0, // has the effect of extending the shadow
),
],
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Container(
padding: EdgeInsets.only(top: 25),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Center(
child: Text(
'Crypto Tracker',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 30.0,
fontWeight: FontWeight.bold,
),
),
)
],
),
),
),
// the body part
CurrencyWidget(
currencyIconUrl: 'assets/images/btc.png',
currencyName: 'Bitcoin',
currencyShortName: 'BTC',
currencyPrice: btcPrice,
currencyChange24h: btcChange24h,
),
I get the data printed into the console but I also get Null displayed in the emulator as shown in the below screenshot.
The image where null is displayed
A screenshot of the data being printed in the console
Any idea what the issue may be?

The problem is that getting api data is async task so it takes time, while build method build screen in that time, so it is printing null.
1) You can call setState at the end of function which change null to actual data when it gets from API.
void cryptoCurrencyData() async {
var data = await currencyData.getCoinsData();
btcPrice = data['data'][0]['priceUsd']; // assign
btcChange24h = data['data'][0]['changePercent24Hr']; // aasign
print(btcPrice = data['data'][0]['priceUsd']);
print(btcChange24h = data['data'][0]['changePercent24Hr']);
setState(() {}); // added
}
2) However, FutureBuilder is more better option where you can show loading indicator or something which shows data is loading and display when arrives.
Note: in this way you don't need cryptoCurrencyData method and also you don't need to store value in different variable.
FutureBuilder(
future: currencyData.getCoinsData(),
builder: (_, sanpshot) {
if (!sanpshot.hasData) {
return CircularProgressIndicator();
}
return CurrencyWidget(
currencyIconUrl: 'assets/images/btc.png',
currencyName: 'Bitcoin',
currencyShortName: 'BTC',
currencyPrice: data['data'][0]['priceUsd'],
currencyChange24h: data['data'][0]['changePercent24Hr'],
);
},
),

Related

Flutter Firebase Pagination Problem to Scrolling Top

I made a social media application using Flutter Firebase, and like every social media application, I have a stream of posts shared by users on the home screen. At first, I didn't have any problems, but as the number of data increased, I started to have problems especially getting photos. Later I found out that this was because I was getting all the data at once and decided to use Pagination. I have successfully used Pagination and I also started using Cached Network Image to load my photos faster. But I still have such a problem in the flow. When I scroll the screen to the bottom, the data is loaded at the limit I set, in the example my limit is 12, so I have no problem when scrolling down the screen, but when I want to quickly scroll the screen up, it tries to load all the data again, the system is having too much difficulty, I can't load it at the end and the application gives a lost connection error and closes itself.
In my opinion, the same thing should happen when we swipe the screen up, just as the data is loaded piece by piece as much as the limit number we set when swiping down the screen.
Otherwise, this problem that I am experiencing occurs.
Do you know any solution for this?
This is my code for Pagination;
getData() async {
var Ref1 = (widget.post != null)
? _firestore
.collection("users")
.doc(widget.post["profileID"])
.collection("Datas")
.orderBy("uploadTime", descending: true)
.limit(perpage)
: null;
setState(() {
loadingProducts = true;
});
var reponse = await Ref1.get();
listt = reponse.docs;
lastDocument = reponse.docs[reponse.docs.length - 1];
setState(() {
loadingProducts = false;
});
}
getmoreData() async {
if (moreDataAvailable == false) {
return;
}
if (gettingmoreData == true) {
return;
}
setState(() {
gettingmoreData = true;
});
var Ref1 = (widget.post != null)
? _firestore
.collection("users")
.doc(widget.post["profileID"])
.collection("Datas")
.orderBy("uploadTime", descending: true)
.startAfterDocument(lastDocument)
.limit(perpage)
: null;
var reponse = await Ref1.get();
if (reponse.docs.length < perpage) {
moreDataAvailable = false;
}
lastDocument = reponse.docs[reponse.docs.length - 1];
listt.addAll(reponse.docs);
setState(() {});
setState(() {
gettingmoreData = false;
});
}
And this is my Builder;
GridView.builder(
controller: scrollController,
physics: ScrollPhysics(),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: listt.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () =>
navigateToDetail(listt[index]),
child: Hero(
tag: (listt[index]["foto"] != null)
? NetworkImage(
listt[index]["foto"])
: AssetImage(
"assets/images/n_image.jpg"),
child: Container(
child: Column(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
Container(
height: size.height * 0.078,
width: double.infinity,
decoration: BoxDecoration(
borderRadius:
BorderRadius.only(
bottomRight:
Radius.circular(
10.0),
bottomLeft:
Radius.circular(
10.0),
),
color: Colors.grey[600]
.withOpacity(0.5)),
child: Center(
child: AutoSizeText(
"${listt[index]["name"]}",
textAlign:
TextAlign.center,
style: GoogleFonts.lora(
textStyle: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
maxLines: 2,
),
),
),
],
),
margin: EdgeInsets.all(5.0),
decoration: BoxDecoration(
image: DecorationImage(
image: (listt[index]
["foto"] !=
null)
? OptimizedCacheImageProvider(
listt[index]["foto"])
: AssetImage(
"assets/images/n_image.jpg"),
fit: BoxFit.cover,
),
color: Colors.white,
borderRadius:
BorderRadius.circular(10.0),
),
),
),
);
},
),
And im listening controller in initstate with this;
scrollController.addListener(() {
double maxScroll = scrollController.position.maxScrollExtent;
double currentScroll = scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.25;
if (maxScroll - currentScroll <= delta) {
getmoreTarif();
}
});
Your current code tracks the last document of the current results and then calls startAfterDocument with that document to get the next set of results. This works for scrolling forward, but not when scrolling backward. To paginate backwards, you'll also need to track the first document of the current results and then call endBeforeDocument with that document.

How to make my own Loading Screen Indicator in flutter?

So far, I have made a loading animation that includes Container, Column, Text, and Image(gif). And this code is working fine in a new project but when I try to implement this code into my ongoing project, it is not working!
It does not throw any error but the loading screen is blank.
Like data from API loads without loading animation.
So, what I am doing wrong in this, or how to implement loader correctly?
Loader's code:
return Scaffold(
body: Container(
height: 130,
width: 135,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(17),
border: Border.all(color: Colors.blue)),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
"Loading...",
style: TextStyle(
fontSize: 13,
letterSpacing: 2.1,
color: Colors.blue,
),
),
Padding(
padding: const EdgeInsets.all(3.0),
child: Image.network(
"https://cdn.dribbble.com/users/42716/screenshots/3913441/media/4ef7d67070fee7ab75948280f51d369f.gif",
height: 100,
),
),
],
),
),
);
Here I am implementing code:
Future<List<VeReportModel>>? _vReport() async {
debugPrint("Do not exploit this code $fDate &&&&&&&& Thank you");
try {
if (await NetworkUtils.isNetworkAvailable()) {
//UIUtils.showProcessIndicator(); //TODO This is old Indicator I want a new one here
Loader();
ApiResponse? apiResponse = await RestApiClient(session: session)
.vReport(fDate, tDate, spedLimit, stLimit, vId!);
UIUtils.hideProcessIndicator(); //TODO Old Indicator disposed
Common.printWrapped('Map: _vEvent(): apiResponse=$apiResponse');
if (apiResponse!.successful && apiResponse.code == apiSuccessCode) {
if (apiResponse.data != null) {
List mapMessages = apiResponse.result;
debugPrint("mapMessage $mapMessages");
return mapMessages
.map((v) => VeReportModel.fromJson(v))
.toList();
}
}
} else {
UIUtils.hideProcessIndicator();
UIUtils.displayInternetError(context);
}
} catch (e,stack) {
UIUtils.hideProcessIndicator();
UIUtils.catchErrorMsg(context);
debugPrint('Error While acknowledging v report : $e');
GeneralException.handleError(e, stack: stack, module: moduleVeReport);
debugPrint('VeReport: Error while getting v list: $e');
}
return [];
}

Value detected of type null when passing data via routes in flutter

This error just been a while before the data passed successfully from loading screen.
The loading screen that passing data :
if i print the instance on here, the error is not appeared
void setupWorldTime() async {
WorldTime instance = WorldTime(location: 'Jawa Timur', flag: 'jakarta.png', url: 'Asia/Jakarta');
await instance.getTime();
Navigator.pushReplacementNamed(context, '/home', arguments: {
'location': instance.location,
'flag': instance.flag,
'time': instance.time,
});
}
The home screen which is receiving data :
Map data = {};
#override
Widget build(BuildContext context) {
data = ModalRoute.of(context)!.settings.arguments as Map;
print(data['location']);
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 50),
child: Column(
children: [
TextButton.icon(
onPressed: () {
Navigator.pushNamed(context, '/location');
},
icon: const Icon(Icons.edit_location),
label: const Text('Edit Location'),
),
const SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
data['time'],
style: const TextStyle(
fontSize: 30,
),
),
]),
],
),
),
),
);
}
print(data['location']); printing the data perfectly, but before it the error above showing instantly, is that mean the print method expected the value of data is null before it is receive the value? how to fix it
Your method setupWorldTime is an async function and will not call the Navigator.pushReplacementNamed bit until instance.getTime() has returned. Because your home screen starts without its arguments set, data = ModalRoute.of(context)!.settings.arguments as Map will set data to null, leading to your error. Only once instance.getTime() has returned will build() be called again, this time with data != null and your message disappears.
To fix this, in your build function you should test for data == null and show something else (like a loading indicator) if data is indeed still null, or use a FutureBuilder (preferred).

I want to update the app version in playstore to show a message dialog to user

I am new flutter .I want to update new version app in playstore to show a message dialog to user to update the new version and I used the plugin version_check 0.2.0.
When the user has already updated, but it still displays Message dialog the same. How not to show message dialog after update.Who can help me?
This my Code
This my Code
This my Code
As everything is not clear in the question, you should follow given steps to achieve the same.
Step 1. Go to Remote Config in firebase and add few parameters shown in the image and then publish it.
Step 2. Create a function VersionCheck and _showVersionDialog as follows:
versionCheck(){
//Get Current installed version of app
WidgetsBinding.instance.addPostFrameCallback((_) async {
final PackageInfo info = await PackageInfo.fromPlatform();
double currentVersion = double.parse(info.version.trim().replaceAll(".", ""));
//Get Latest version info from firebase config
final RemoteConfig remoteConfig = await RemoteConfig.instance;
try {
// Using default duration to force fetching from remote server.
await remoteConfig.fetch(expiration: const Duration(seconds: 0));
await remoteConfig.activateFetched();
remoteConfig.getString('force_update_current_version');
double newVersion = double.parse(remoteConfig
.getString('force_update_current_version')
.trim()
.replaceAll(".", ""));
if (newVersion > currentVersion) {
setState(() {
versionCode = remoteConfig.getString('force_update_current_version');
aboutVersion = remoteConfig.getString('update_feature');
});
_showVersionDialog(context);
}
} on FetchThrottledException catch (exception) {
// Fetch throttled.
print(exception);
} catch (exception) {
print('Unable to fetch remote config. Cached or default values will be '
'used');
}
});
}
_showVersionDialog(context) async {
await showDialog<String>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
String title = "Update Available";
String message =
"About Update: \n";
return ButtonBarTheme(
data: ButtonBarThemeData(alignment: MainAxisAlignment.center),
child: new AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title),
Text("v"+versionCode),
],
),
content: Container(
height: 80,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message,style: TextStyle(fontWeight: FontWeight.bold),),
Text(aboutVersion),
],
),
),
actions: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: new Text(
'Update',
style: TextStyle(color: Colors.white),
),
color: Color(0xFF121A21),
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(30.0),
),
onPressed: () {
_launchURL(PLAY_STORE_URL);
},
),
),
],
),
);
},
);
}
Step 3. Call VersionCheck in init function of your main screen as follows.
#override
void initState() {
Future.delayed(const Duration(milliseconds: 5000), () {
if(mounted){
setState(() {
versionCheck();
});
}
});
super.initState();
}
Step 4. Whenever you want the update dialog to appear on screen just increase the version code value in remote config of firebase than your actual version code value.
This will help you to achieve what you want.

Searchable SliverGrid Rendering Wrong Items

I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)