My future method is not working fine, while using flutter builder widget, Where did I go wrong? - flutter

Here is my stateful widget and url is a property pass it to the widget from parent widget. I don't know where did I go wrong?? I created a future builder widget that has getData() as a future. But the print statement inside was not executed ever. Why is that and it returns me always null value, and this results me a red container appearing on screen and not the table widget.
class TimeTable extends StatefulWidget {
final url;
const TimeTable({Key? key,required this.url}) : super(key: key);
#override
_TimeTableState createState() => _TimeTableState();
}
class _TimeTableState extends State<TimeTable> {
Future<List<Train>> getData() async{
final list = await TrainClient(url: widget.url).getName();
print("this line not executed");
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: getData(),
builder: (context,projectSnap){
if(projectSnap.connectionState == ConnectionState.none ||
projectSnap.data == null) {
return Container(color: Colors.red,);
}
return buildDataTable(trains: projectSnap.data);
}));
}
}
getData is a future method and it returns a list, The list gets printed when I call that object Train Client. I had my print statement inside TrainClient class to check whether the list is created successfully.
Here is the code of TrainClient
class TrainClient {
final String url;
TrainClient({required this.url});
Future<List<Train>> getName() async {
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode == 200) {
print("ulla");
final data = json.decode(response.body);
final result = data["RESULTS"]["directTrains"]["trainsList"];
final list = result.map((json) => Train.fromJson(json));
print(list);
return list;
}else{
throw Exception();
}
}
}
The TrainClient class has no error since it printed the list successfully as shown below
(Instance of 'Train', Instance of 'Train', Instance of 'Train', ..., Instance of 'Train', Instance of 'Train')

You should always obtain future earlier (in initState/didChangeDependencies).
Each time your build is executed, new future is created. So it never finishes, if your widget rebuilds often.
late final _dataFuture = getData();
...
FutureBuilder(
future: _dataFuture,
builder: (context,projectSnap){
...
}
);

Related

Can I Use a Future<String> to 'Fill In' a Text() Widget Instead of Using FutureBuilder in Flutter?

I'm trying to better understand Futures in Flutter. In this example, my app makes an API call to get some information of type Future<String>. I'd like to display this information in a Text() widget. However, because my String is wrapped in a Future I'm unable to put this information in my Text() widget, and I'm not sure how to handle this without resorting to a FutureBuilder to create the small widget tree.
The following example uses a FutureBuilder and it works fine. Note that I've commented out the following line near the bottom:
Future<String> category = getData();
Is it possible to turn category into a String and simply drop this in my Text() widget?
import 'package:flutter/material.dart';
import 'cocktails.dart';
class CocktailScreen extends StatefulWidget {
const CocktailScreen({super.key});
#override
State<CocktailScreen> createState() => _CocktailScreenState();
}
class _CocktailScreenState extends State<CocktailScreen> {
#override
Widget build(BuildContext context) {
Cocktails cocktails = Cocktails();
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
FutureBuilder categoryText = FutureBuilder(
initialData: '',
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
}
return const CircularProgressIndicator();
},
);
//Future<String> category = getData();
return Center(
child: categoryText,
);
}
}
Here's my Cocktails class:
import 'networking.dart';
const apiKey = '1';
const apiUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
class Cocktails {
Future<dynamic> getCocktailByName(String cocktailName) async {
NetworkHelper networkHelper =
NetworkHelper('$apiUrl?s=$cocktailName&apikey=$apiKey');
dynamic cocktailData = await networkHelper.getData();
return cocktailData;
}
}
And here's my NetworkHelper class:
import 'package:http/http.dart' as http;
import 'dart:convert';
class NetworkHelper {
NetworkHelper(this.url);
final String url;
Future<dynamic> getData() async {
http.Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
String data = response.body;
var decodedData = jsonDecode(data);
return decodedData;
} else {
//print('Error: ${response.statusCode}');
throw 'Sorry, there\'s a problem with the request';
}
}
}
Yes, you can achieve getting Future value and update the state based on in without using Using FutureBuilder, by calling the Future in the initState(), and using the then keyword, to update the state when the Future returns a snapshot.
class StatefuleWidget extends StatefulWidget {
const StatefuleWidget({super.key});
#override
State<StatefuleWidget> createState() => _StatefuleWidgetState();
}
class _StatefuleWidgetState extends State<StatefuleWidget> {
String? text;
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
#override
void initState() {
super.initState();
getData().then((value) {
setState(() {
text = value;
});
});
}
#override
Widget build(BuildContext context) {
return Text(text ?? 'Loading');
}
}
here I made the text variable nullable, then in the implementation of the Text() widget I set to it a loading text as default value to be shown until it Future is done0
The best way is using FutureBuilder:
FutureBuilder categoryText = FutureBuilder<String>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
var data = snapshot.data ?? '';
return Text(data);
}
}
},
),
but if you don't want to use FutureBuilder, first define a string variable like below and change your adasd to this :
String category = '';
Future<void> getData() async {
var data = await cocktails.getCocktailByName('margarita');
setState(() {
category = data['drinks'][0]['strCategory'];
});
}
then call it in initState :
#override
void initState() {
super.initState();
getData();
}
and use it like this:
#override
Widget build(BuildContext context) {
return Center(
child: Text(category),
);
}
remember define category and getData and cocktails out of build method not inside it.

ternary operator is not working as expected in flutter

I have to render screens condition wise. but conditions are not getting fulfilled.
My function is always returns the false boolean value even if it doesn't have to.
Here is my code
class _HomePageState extends State<HomePage> {
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var data = await prefs.getString('isAuthenticated');
// the value of data is true here
print(data);
if (data == "true") {
return true;
} else {
return false;
}
}
#override
void initState() {
super.initState();
checkIsLoggedIn();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: checkIsLoggedIn() == true ? Profile() : LoginScreen());
//error : checkIsLoggedin () function returns false in my case.
}
}
Use then() to wait for the Future. Then, assign the result with setState().
class _HomePageState extends State<HomePage> {
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var data = await prefs.getString('isAuthenticated');
// the value of data is true here
print(data);
if (data == "true") {
return true;
} else {
return false;
}
}
var isLoggedIn = false; // HERE
#override
void initState() {
super.initState();
checkIsLoggedIn().then((result) { // HERE
setState(() {
isLoggedIn = result;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isLoggedIn == true // HERE
? Profile()
: LoginScreen());
}
}
When getting into problems, I do recommend you are first looking into the warnings you are getting around the problematic area. If you are using a default Flutter project, you should have the lint unrelated_type_equality_checks activated since it is part of the Core set of Dart Lints:
DON'T Compare references of unrelated types for equality.
Comparing references of a type where neither is a subtype of the other most likely will return false and might not reflect programmer's intent.
https://dart-lang.github.io/linter/lints/unrelated_type_equality_checks.html
This would give the following warning in your IDE with a link to the documentation I linked to previously:
The problem is that your checkIsLoggedIn() method have the following signature which means it returns an object of the type Future<bool>:
Future<bool> checkIsLoggedIn() async {
And you are then comparing this against true which are of the type bool. If you are doing an equality check against these two different types, it will ALWAYS end up returning false, which is also what you are observing.
A Future also means that you are handling a value that are potentially still not created. So when you are running your checkIsLoggedIn() == true it does not even know yet if the bool inside your Future<bool> are true or false.
The solution are to either make it so checkIsLoggedIn() returns bool, which cannot be done in this case since the method are handling asynchronously logic.
Another solution would then be to await the returned Future from checkIsLoggedIn(). But we cannot use await unless we mark the method, we are inside, as async. But for Flutter, we are not allowed to have build() marked as async.
So a solution would instead be to handle the asynchronously nature of checkIsLoggedIn() by using FutureBuilder:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Which are a Widget made for handling the building of the GUI when we don't know the value of the Future, but then triggers an automatic rebuild when the value of the Future are received.
You can use .getbool instead .getString
To wait for checkIsLoggedIn result you can use a FutureBuilder.
Here an example :D
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final isAuthenticated = await prefs.getBool('isAuthenticated');
return isAuthenticated;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<bool>(
future: checkIsLoggedIn(),
builder: (context, snapshot) {
// SHOW LOADING WHILE PREFS IS GETTING DATA
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
// SHOW SCREEN ACCORDING DATA
return snapshot.data! //
? const Profile()
: const LoginScreen();
},
),
);
}
}

Async Data Initialization in initState

I'm calling an async method getMyLocation() to get my current location in my initState(). The method can take a while...
I wanted to understand the behavior of initState() in these cases. Does the method still execute in the background as build() renders or does initState() timeout since it needs to complete before build() renders?
In my build() I have a statement checking if my latitude is null, in which case I return a Loading() widget. Sometimes Screen() renders and sometimes Loading() goes on indefinitely. I am assuming sometimes the getMyLocation() successfully executes during initState() and sometimes it timesout?
#override
void initState() {
super.initState();
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = userData.getUser();
userData.getMyLocation();
}
getMyLocation() async {
_myUser.longitude = await getCurrentLongitude();
_myUser.latitute = await getCurrentLatitude();
notifyListeners();
}
Widget build(BuildContext context) {
final userData = Provider.of<MyUser>(context);
final myUser = userData.getUser();
myUser.latitude == null?
return Loading()
: return Screen()
Great question. First of all, initState() runs synchronously, it prepares various things needed for build() method to run properly. If you are executing some async function here, it will just return a Future because you can't await it in the initState(). In your case you probably need a FutureBuilder. The "proper way" of dealing with futures would be something like:
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<void> getMyLocation() async {
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = await userData.getUser();
// if getUser() is async then we have to await
myUser.longitude = await getCurrentLongitude();
myUser.latitute = await getCurrentLatitude();
// notifyListeners();
// You probably do not need this, should be done in provider methods instead
}
Widget build(BuildContext context) {
return FutureBuilder(
future: getMyLocation(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return SomeErrorWidget();
}
if (snapshot.hasData) {
return Screen(snapshot.data);
}
return SomeLoadingWidget();
});
}

How to reference variable in method in FutureBuilder (builder:)?

I want to use the variable dbRef in inputData() in future Builder builder: you can see the variable in between asterisk .
void inputData() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
final **dbRef** = FirebaseDatabase.instance.reference().child("Add Job Details").child(uid).child("Favorites");
}
#override
Widget build(BuildContext context) {
return FutureBuilder (
future: **dbRef**.once(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {
List<Map<dynamic, dynamic>> list = [];
for (String key in snapshot.data.value.keys) {
list.add(snapshot.data.value[key]);
}
This is one more approach to tackle the problem.
The idea is to use a variable _loading and set it to true initially.
Now, after in your inputData() function, you can set it to false once you get the dbref.
Store dbref, the way I stored _myFuture in the code below i.e., globally within the class.
Use your _loading variable to return a progress bar if its true else return FutureBuilder with your dbref.once() in place. Now, that you have loaded it, it should be available at this point.
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Is the future being loaded?
bool _loading;
// This is the future we will be using in our FutureBuilder.
// It is currently null and we will assign it in _loadMyFuture function.
// Until assigned, we will keep the _loading variable as true.
Future<String> _myFuture;
// Load the _myFuture with the future we are going to use in FutureBuilder
Future<void> _loadMyFuture() async {
// Fake the wait for 2 seconds
await Future.delayed(const Duration(seconds: 2));
// Our fake future that will take 2 seconds to return "Hello"
_myFuture = Future(() async {
await Future.delayed(const Duration(seconds: 2));
return "Hello";
});
}
// We initialize stuff here. Remember, initState is called once in the beginning so hot-reload wont make flutter call it again
#override
initState() {
super.initState();
_loading = true; // Start loading
_loadMyFuture().then((x) => setState(() => _loading = false)); // Set loading = false when the future is loaded
}
#override
Widget build(BuildContext context) {
// If loading, show loading bar
return _loading?_loader():FutureBuilder<String>(
future: _myFuture,
builder: (context, snapshot) {
if(!snapshot.hasData) return _loader(); // still loading but now it's due to the delay in _myFuture
else return Text(snapshot.data);
},
);
}
// A simple loading widget
Widget _loader() {
return Container(
child: CircularProgressIndicator(),
width: 30,
height: 30
);
}
}
Here is the output of this approach
This does the job but, you might need to do it for every class where you require your uid.
========================================
Here is the approach I described in the comments.
// Create a User Manager like this
class UserManager {
static String _uid;
static String get uid => _uid;
static Future<void> loadUID() async {
// Your loading code
await Future.delayed(const Duration(seconds: 5));
_uid = '1234'; // Let's assign it directly for the sake of this example
}
}
In your welcome screen:
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _loading = true;
#override
void initState() {
super.initState();
UserManager.loadUID().then((x) => setState(() => _loading = false));
}
#override
Widget build(BuildContext context) {
return _loading ? _loader() : Text('Welcome User ${UserManager.uid}!');
}
// A simple loading widget
Widget _loader() {
return Container(child: CircularProgressIndicator(), width: 30, height: 30);
}
}
The advantage of this method is that once you have loaded the uid, You can directly access it like this:
String uid = UserManager.uid;
thus eliminating use of futures.
Hope this helps!

Flutter: How to make a sequence of http requests on a widget before build method

I have 3 classes: Users, Posts and Comments. User has many Posts and
Posts has many Comments.
I want that all data to be fetched before the widget's build method is called.
I tryed to use initState() to do this:
class FetchDataExample extends StatefulWidget {
final User _user;
FetchDataExample(this._user);
#override
_State createState() => _State(_user);
}
class _State extends State<FetchDataExample> {
final User _user;
_State(this._user);
#override
void initState() {
_user.setPosts();
super.initState();
}
#override
Widget build(BuildContext context) {
print(this._user.posts[0]);
return Container(
);
}
}
In User class I have:
void setPosts() async {
String url = 'https://jsonplaceholder.typicode.com/posts?userId=' + this.id.toString();
var request = Requester.get(url); // Returns a Future<Response>
await request.then((value) => this.posts = Post.jsonToPosts(json.decode(value.body)));
this.posts.forEach((post) => post.setComments());
print(this.posts[0]);
}
The 'setComments()' has the same logic.
I have two prints:
Inside build that returns null;
Inside setPosts the returns Instance of 'Post';
So, by the time that Build method is called in the widget, the initState has not finished yet.
I need it be finished, does anyone know how can I do that?
You can use a FutureBuilder to build a widget by using latest result from a future.
And also you can combile multiple futures into a single one using Future.wait method.
Here is a sample code:
_getPageData() async {
var _combinedFutures = await Future.wait([setPosts, setComments]);
//do stuff with data
}
...
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:_getPageData(),
builder: (context, snapshot) {
return Container();
}),
);
});