I'm new to Flutter, (comming from web and especially JS/VueJS)
I'm have a db in firebase that has a collection called edito and inside, i have different artist with a specific Id to call Deezer Api with it.
So what i want to do is first called my db and get the Id for each of artist and then put this id in a function as parameter to complete the url.
I did 2 Future function, one to call the db and one to call the api.
But i don't understand how to use one with the others in the build to get a listview with the information of the api of deezer for each data.
i'm getting the list but it's stuck in and endless loop.
All of my app will be on this nested function, is it possible to do this and call it in any widget that i want ?
here is my code, thanks
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class GetAlbum extends StatefulWidget {
#override
_GetAlbumState createState() => _GetAlbumState();
}
class _GetAlbumState extends State<GetAlbum> {
Map mapResponse;
Future<QuerySnapshot> getDocument() async{
return FirebaseFirestore.instance.collection("edito").get();
}
Future<dynamic> fetchData(id) async{
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
mapResponse = json.decode(response.body);
});
}
}
Future<dynamic> getDocut;
Future<dynamic> getArtist;
#override
void initState() {
getDocut = getDocument();
getArtist = fetchData(null);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future : getDocut,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(!snapshot.hasData) {
return CircularProgressIndicator();
}else{
return new ListView(
children: snapshot.data.docs.map<Widget>((document){
print(document.data().length);
return FutureBuilder(
future: fetchData(document.data()['idDeezer'].toString()),
builder: (context, snapshot){
return Container(
child: mapResponse==null?Container(): Text(mapResponse['title'].toString(), style: TextStyle(fontSize: 30),),
);
}
);
}).toList(),
);
}
},
);
}
}
Here's a simplified example of making two linked Future calls where the 2nd depends on data from the first, and using the results in a FutureBuilder:
import 'package:flutter/material.dart';
class FutureBuilder2StatefulPage extends StatefulWidget {
#override
_FutureBuilder2StatefulPageState createState() => _FutureBuilder2StatefulPageState();
}
class _FutureBuilder2StatefulPageState extends State<FutureBuilder2StatefulPage> {
Future<String> _slowData;
#override
void initState() {
super.initState();
_slowData = getAllSlowData(); // combined async calls into one future
}
// linked async calls
Future<String> getAllSlowData() async {
int id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<int> loadId() async {
int _id = await Future.delayed(Duration(seconds: 2), () => 42);
print('loadId() completed with: $_id'); // debugging
return _id;
}
Future<String> loadMoreData({int id}) async {
return await Future.delayed(Duration(seconds: 2), () => 'Retrieved data for id:$id');
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBldr Stateful'),
),
body: FutureBuilder<String>(
future: _slowData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(child: Text(snapshot.data));
}
return Center(child: Text('Loading...'));
},
),
);
}
}
This avoids having to nest the FutureBuilder which may be error prone.
And calling future methods directly from a FutureBuilder is not recommended since the call could be made many times if its containing widget is rebuilt (which can happen a lot).
I tried to add firebase in the first one but i get null for the id in the get AllSlowDAta but i got it right with the Future.delayed.
// linked async calls
Future<String> getAllSlowData() async {
String id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<dynamic> loadId() async {
//return await Future.delayed(Duration(seconds: 2), () => '302127');
await FirebaseFirestore.instance.collection("edito")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc.data()["idDeezer"];
});
});
}
Future<dynamic> loadMoreData({String id}) async {
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
return json.decode(response.body);
});
}
}
Related
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.
appreciate the help! I've looked through some of the other responses on here and I can't find an answer.
I have a Provider, in which I have an async function defined. It reaches out to an external API, gets data, and then is meant to update the attributes in the Provider with the data received.
The Widget that uses the provider is meant to build a ListView with that data. projects is null until the response is received. That's why I need the async await functionality to work here. The error I'm getting says that "length can't be called on null", which means projects is still null at the time is reaches that line. That is because the async functionality isn't working.
Here is the Provider, in which my async function is defined:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../../constants/urls.dart';
import 'project.dart';
class Projects with ChangeNotifier{
List<Project> _projects;
List<Project> _myProjects;
final String authToken;
final List<Project> previousProjects;
final bool _initialLoad = true;
Projects(this.authToken, this.previousProjects);
List<Project> get projects {
return _projects;
}
List<Project> get myProjects {
return _myProjects;
}
bool get initialLoad {
return _initialLoad;
}
Future<void> fetchProjects() async {
print('inside future, a');
try {
var response = await http.get(
Uri.parse(Constants.fetchProjectsURL),
headers: {"Authorization": "Bearer " + authToken},
);
print('inside future, b');
if (response.statusCode == 200) {
final extractedData = json.decode(response.body) as List;
final List<Project> tempLoadedProjects = [];
extractedData.forEach((project) {
tempLoadedProjects.add(
Project(
// insert project params
),
);
});
_projects = tempLoadedProjects;
print(_projects);
print(projects);
notifyListeners();
} else {
print('something happened');
}
} catch (error) {
throw error;
}
}
}
Then, I used this provider in the following Widget:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../providers/projects/projects_provider.dart';
class ProjectsColumn extends StatelessWidget {
Future<void> fetchProjects(ctx) async {
await Provider.of<Projects>(ctx).fetchProjects();
}
Widget build(BuildContext context) {
print('Before fetch');
fetchProjects(context);
print('After fetch');
final projects = Provider.of<Projects>(context, listen: false).projects;
return ListView.builder(
itemCount: projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${projects[index]}',
),
);
});
}
}
Thoughts?
You need to put await before the method to a wait, but you can't do this in build() method, So you can use future builder like the answer of #jamesdlin
or you can call fetchProjects method in intState first like this way:
class ProjectsColumn extends StatefulWidget {
#override
State<ProjectsColumn> createState() => _ProjectsColumnState();
}
class _ProjectsColumnState extends State<ProjectsColumn> {
bool _isLoading = true;
Future<void> _fetchProjects() async {
await Provider.of<Projects>(context, listen: false).fetchProjects();
_isLoading = false;
if (mounted) setState(() {});
}
#override
void initState() {
super.initState();
_fetchProjects();
}
#override
Widget build(BuildContext context) {
return _isLoading
? const Center(child: CircularProgressIndicator())
: Consumer<Projects>(
builder: (context, builder, child) => builder.projects.isEmpty
? const Center(child: Text('No Projects Found'))
: ListView.builder(
shrinkWrap: true,
itemCount: builder.projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${builder.projects[index]}',
),
);
},
),
);
}
}
EDIT:
a) From the docs HERE BuildContext objects are passed to WidgetBuilder functions (such as StatelessWidget.build), and are available from the State.context member., and in the previous example I used StatefulWidget widget that extends state class, then you can use context outside build but inside the class extends state, not like StatelessWidget.
b) mounted condition, it represents whether a state is currently in the widget tree, i used it to prevent the famous error: setState() called after dispose()
see docs HERE, also this useful answer HERE
I try to use FutureBuilder in Flutter to wait ulti my initState is finished then buil the UI for the app.
But when the app is running, the screen keep rebuilding each time I press another button (the button does totally different thing).
Future loadUser() async {
String jsonString = await storage.read(key: "jwt");
final jsonResponse = json.decode(jsonString);
loggedUser = new LoggedUser.fromJson(jsonResponse);
print(loggedUser.token);
getProfile();
getJourneyByUserId()
.then((receivedList){
addRanges(receivedList);});
}
Future<List<Journey>>getJourneyByUserId() async {
var res = await http.get(
Uri.parse("$baseUrl/journeys/userid=${loggedUser.user.userId}"),
headers: {
'Content_Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ${loggedUser.token}',
},
);
if (res.statusCode == 200) {
print("Get journeys successfully");
}
var data = jsonDecode(res.body);
List idList = [];
for (var i in data) {
idList.add(i["journeyId"]);
}
for (var i in idList) {
var res = await http.get(
Uri.parse("$baseUrl/journeys/$i"),
);
var data = jsonDecode(res.body);
Journey userJourney = new Journey.fromJson(data);
setState(() {
journeyList.add(userJourney);
});
}
print("Journey ${journeyList.length}");
return journeyList;
}
addRanges(journeyList){
setState(() {
rangeList=[];
});
if (journeyList.isNotEmpty) {
for (var i in journeyList) {
DateTime startDate =
DateTime(i.startDate.year, i.startDate.month, i.startDate.day);
DateTime endDate =
DateTime(i.endDate.year, i.endDate.month, i.endDate.day);
setState(() {
rangeList.add(PickerDateRange(startDate, endDate));
});
}
}
print("Range ${rangeList.length}");
return rangeList;
}
returnRange() {
List<PickerDateRange> list = [];
for(int i =0; i<rangeList.length;i++){
list.add(rangeList[i]);
}
return list;
}
Future functionForBuilder() async {
return await returnRange();
}
//initState function
#override
void initState() {
super.initState();
loadUser();
functionForBuilder();
}
//build the UI
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("$_name's Profile",style: TextStyle(color: kColorPalette4),),
centerTitle: true,
),
body: Container(
child: FutureBuilder(
future: functionForBuilder(),
builder: (BuildContext context,AsyncSnapshot snapshot){
//here I set the condition for each case of snapshot
}
I have read some documents say that I should assign the functionForBuilder() to a Future variable when initState then use it in the future child of FutureBuilder. Example:
Future _future;
//initState function
#override
void initState() {
super.initState();
loadUser();
_future=functionForBuilder();
}
// then with the FutureBuilder
future: _future
With this way the screen is not rebuild anymore but my function returnRange() seems like not running as my expextation (I called the returnRange() once in the build() function).
Thanks in advance for your answer!
Whenever you assign to the _future variable again, you must do that inside a setState block, otherwise the widget will not rebuild with the new future.
For example:
void updateData() {
setState(() {
_future = functionForBuilder();
});
}
If you use FutureBuilder, it rebuild items again and again.
Try two ways:
Don't use `future: functionForBuilder(), comment it.
Remove FutureBuilder(), simply use Container().
And let me know any issue?
Code:
call your future in the initstate method not in the build as shown in the example.
class MyPage extends StatefulWidget { #override State<MyPage> createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { // Declare a variable. late final Future<int> _future; #override void initState() { super.initState(); _future = _calculate(); // Assign your Future to it. } // This is your actual Future. Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42); #override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder<int>( future: _future, // Use your variable here (not the actual Future) builder: (_, snapshot) { if (snapshot.hasData) return Text('Value = ${snapshot.data!}'); return Text('Loading...'); }, ), ); } }
I'm trying to create a Stream, which will be called in the main page. This Stream returns me a list from my database. I will be using this list to create several cards in the main screen, and whenever there is a new card or a card removed, I will refresh the screen.
This is my Stream:
Stream<List> readData() async*{
Map<dynamic, dynamic> button_list = Map();
List lst = [];
final FirebaseUser user = await _auth.currentUser();
final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.forEach((element) {
button_list = element.snapshot.value as Map;
lst = button_list.values.toList();
print(lst);
});
final lstStream = Stream.fromFuture(lstValues);
await for(var event in lstStream) {
yield lst;
}
}
This is the result from print(lst):
flutter: [{icon: delte, nome: Junior}, {icon: add, nome: Televisao}, {icon: bulb, nome: BAtata}]
This is the database:
This is the main screen with the main code:
body: StreamBuilder(
stream: _auth.readData(),
initialData: 0,
builder: (context, snapshot) {
if (snapshot.hasError || snapshot.hasError){
return Container(color: Colors.red);
}
if (!snapshot.hasData || !snapshot.hasData){
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasData || snapshot.hasData){
return GridView.count(
The problem is that the values are not being received in the Stream. In the main page. Whenever I try to use snapshot.data I get nothing. At the moment the only think is loading is the progress circular indicator, I'm not receiving the content from the Stream I have created.
Personally, I rather work with streams and rxdart than methods such as yield.
Within my firebase projects I use a construction like this:
// Get a database reference for the user
Future<DatabaseReference> _getUserRef() async {
final FirebaseUser user = await _auth.currentUser();
return FirebaseDatabase.instance
.reference()
.child('users')
.child(user.uid);
}
// Get a reference to a specific user node. In you cause buttons
Future<DatabaseReference> _getButtonsRef() async {
return (await _getUserRef()).child('buttons');
}
// Get the data as stream
Stream<List<MyButton>> getButtons() { // Not sure what data type you need
return _getButtonsRef().asStream()
.switchMap((ref) => ref.onValue) // Use on value to get new data if any changes
.map((event) => event.snapshot.value != null ? // Map the value to the object you want or return an empty list
MySnapshotMapper.buttonListFromSnapshot(event.snapshot.value) : List<MyButton>()
);
}
In case you wonder about the MySnapshotMapper:
class MySnapshotMapper {
static List<MyButton> buttonListFromSnapshot(Map snapshot) {
return List<MyButton>.from(snapshot.values.map((snap) => MyButton.fromSnapshot(snap)));
}
}
And of course the button:
class MyButton {
// Not sure which fields it should have
String name = '';
double width = 10.0, height = 10;
MyButton.fromSnapshot(Map snap) {
name = snap['name'] ?? ''; // Use the value in the Map or or use a default value if not found
width = snap['width']?.toDouble() || width;
height = snap['height ']?.toDouble() || height ;
}
}
Step 1:
class EmployeeRepository {
final CollectionReference collection =
FirebaseFirestore.instance.collection('employees');
Stream<QuerySnapshot> getStream() {
/// Based on Firebase.auth you can collect user data here and pass as
/// Stream<QuerySnapshot> like below.
return collection.snapshots();
}
Future<List<Employee>> buildData(AsyncSnapshot snapshot) async {
List<Employee> list = [];
/// Based on the user snapShot, you can convert into the List and return to
/// the futurebuilder
await Future.forEach(snapshot.data.docs, (element) async {
list.add(Employee.fromSnapshot(element));
});
return Future<List<Employee>>.value(list);
}
}
Step 2:
EmployeeRepository employeeRepository = EmployeeRepository();
#override
Widget build(BuildContext context) {
Widget loadProgressIndicator() {
return Container(
child: Center(child: CircularProgressIndicator()),
);
}
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('ListView'),
),
body: StreamBuilder<QuerySnapshot>(
stream: employeeRepository.getStream(),
builder: (context, snapShot) {
if (snapShot.hasError ||
snapShot.data == null ||
snapShot.data.docs.length == 0 ||
snapShot.connectionState == ConnectionState.waiting) {
return loadProgressIndicator();
} else {
return FutureBuilder(
future: employeeRepository.buildData(snapShot),
builder: (context, futureSnapShot) {
if (futureSnapShot.hasError ||
futureSnapShot.connectionState ==
ConnectionState.waiting ||
futureSnapShot.data.length == 0) {
return loadProgressIndicator();
} else {
return ListView.builder(
itemBuilder: (context, index) {
final employee = futureSnapShot.data[index];
return ListTile(
title: Text(employee.employeeName),
);
},
);
}
});
}
})));
}
This what I think has happened and which is why the code is not working as expected:
onValue function of the DocumentReference provides a Stream<Event> according to the latest documentation.
Stream<Event> onValue
But since the forEach returns a Future it is counted and used as a Future & then converted to a Stream by using Stream.fromFuture()
Future forEach(void action(T element))
While as forEach Returns a future, when completed it returns null as final value to the future.
Future forEach(void action(T element)) {
_Future future = new _Future();
StreamSubscription<T> subscription =
this.listen(null, onError: future._completeError, onDone: () {
future._complete(null);
}, cancelOnError: true);
subscription.onData((T element) {
_runUserCode<void>(() => action(element), (_) {},
_cancelAndErrorClosure(subscription, future));
});
return future;
}
Finally the lst being returned instead of the event in the final for loop.
await for (var event in lstStream) {
yield lst;
}
You can improve this code to make it work as following.
Stream<List> readData(user) async*{
final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.map((element) {
Map button_list = element.snapshot.value as Map;
List lst = button_list.values.toList();
print(lst);
return lst;
}).toList();
final lstStream = Stream.fromFuture(lstValues);
await for(var event in lstStream) {
yield event;
}
}
Check that I have made following changes:
replaced forEach with map
[Optional change] taken Firebase user as method dependency as it is not required to be fetched on every iteration
[Optional change] moved lst & button_list inside the map execution block
I have not tested this code due to Firebase database dependency, but I have tested the theory on which this solution is based off of.
Here is the sample which I have tested:
Stream<List> readData() async* {
final list = Stream.fromIterable([
['a'],
['a', 'b'],
['a', 'b', 'c'],
['a', 'b', 'c', 'd']
]).map((element) {
print(element);
return element;
}).toList();
final listStream = Stream.fromFuture(list);
await for (var event in listStream) {
yield event;
}
}
I have replaced the Firebase document with a list of strings to make provide as much as resemblance as possible.
So in theory,
Stream.fromIterable([
['a'],
['a', 'b'],
['a', 'b', 'c'],
['a', 'b', 'c', 'd']
]) // Stream<List<String>> which can be similar to a list of documents
can replace
databaseReference.child(user.uid+"/buttons/").onValue // Stream<Event> which has a list of documents
Since FirebaseDatabase does not provide a stream of results you should use, Cloud FireStore
Here is the implementation of your code using cloud_firestore: ^0.16.0.
You will need to use subCollections for replicated the exact structure as RealTime Database.
1.Create a datamodel for the data you want to store and retrieve from firestore to made things easier.
class ButtonData{
final String name, icon;
ButtonData({this.name, this.icon});
}
Create a Stream that returns a list of documents from cloud firestore subCollection.
Stream<List<ButtonData>> getData(){
return users
.doc(FirebaseAuth.instance.currentUser.uid)
.collection('buttons').snapshots().map(buttonsFromQuerySnapshot);
}
Create a function that converts QuerySnapshot from firestore to a list of required objects. buttonsFromQuerySnapshot
List<ButtonData> buttonsFromQuerySnapshot(QuerySnapshot querySnapshot){
return querySnapshot.docs.map((DocumentSnapshot snapshot) {
return ButtonData(name: snapshot.data()['name'].toString(), icon: snapshot.data()['icon'].toString());
}).toList();
}
Use a streamBuilder to show results from the stream.
StreamBuilder<List<ButtonData>>(
stream: getData(),
builder: (context, snapshot){
if (snapshot.hasData){
final List<ButtonData> buttons = snapshot.data;
return ListView.builder(itemBuilder: (context, index){
return Column(
children: [
Text(buttons[index].name),
Text(buttons[index].icon),
],
);
});
}
return const Center(child: CircularProgressIndicator(),);
}),
I would recommend you to store icons as integer values. Here you can
find a list of Material Icons and their integer values.
You can then display icons using their retrieved integer values. See
this answer https://stackoverflow.com/a/59854460/10285344 (Haven't
tried this)
I solved a very similar problem about loading the functions a user can execute according to their profile to build the interface. It's basically handling an async and futures issue. For me, Provider made the deal. I will try to put everything in order and paste my code for reference, note I did not have to make changes in the state, I just needed the initial information:
Create a multiprovider for your app
Define the Provider to call your API to get the initial information of the cards.
Pass this information as a parameter to your widget using Provider.of
Use this provider info in InitState()
Options for managing changes... Copy the provider info into an object you can handle or define API calls to your provider to update changes dynamically (I did not went through this)
Check relevant parts of code you may be interested in:
Provider class and API call:
class UserFunctionProvider {
Future<List<UserFunction>> loadUserFunctions() async {
return await APICall.profileFunctions();
}
}
static Future<List<UserFunction>> profileFunctions() async{
List<UserFunction> functionList = [];
UserFunction oneFunction;
final cfg = new GlobalConfiguration();
final token = window.localStorage["csrf"];
var res = await http.get('${cfg.get('server')}:${cfg.get('port')}/get_user_functions',
headers: {
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token'
}
);
int i = 0;
jsonDecode(res.body).forEach((element) {
oneFunction = new UserFunction.fromJson(element);
oneFunction.tabControllerIndex = i;
i++;
functionList.add(oneFunction);
});
return functionList;
}
Defining a Multiprovider and passing it to the relevant widget (it was home in my case)
void main() async {
GlobalConfiguration().loadFromMap(AppConfiguration.appConfig);
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) {
print(
'${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
});
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
if (kReleaseMode)
exit(1);
};
runApp(
MultiProvider(
providers: [
FutureProvider(create: (_) => UserFunctionProvider().loadUserFunctions()),
],
child: MyApp()
)
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
String myLocale;
try{
myLocale = Platform.localeName;
}catch(e){
myLocale = 'es_ES';
print('Language set to Spanish by default.\n Error retrieving platform language: $e');
}
initializeDateFormatting(myLocale, null);
return MaterialApp(
title: 'Sanofi admin',
theme: ThemeData(primarySwatch: Colors.blue),
home: VerifySession().loadScreen(HomeScreen(Provider.of<List<UserFunction>>(context)))
);
}
}
Receiving the parameter from the provider into the Widget (as listOfUserFunction):
class HomeScreen extends StatefulWidget {
HomeScreen(this.listOfUserFunction);
final List<UserFunction> listOfUserFunction;
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
final log = Logger('HomeScreenState');
TabController tabController;
int active = 0;
UserFunction oneFunction;
#override
void initState() {
super.initState();
tabController = new TabController(vsync: this, length: widget.listOfUserFunction.length, initialIndex: 0)
..addListener(() {
setState(() {
active = tabController.index;
});
});
}
I'm using the Getx controller in my project. I have create the controller for FutureBuilder for displaying list but .Obs is not set on Future Function. I'm sharing the code.
class PPHomeController extends GetxController {
Future<List<PPProductRenterModel>> listNearProduct;
// i want to set .Obs end of the "listNearProduct" but it's not working because of Future.
FetchNearProductList({#required int price}) async {
listNearProduct = CallGetNearProducts();// Http API Result
}
}
{
PPHomeController _homeController = Get.put(PPHomeController());
Widget mainProductListView() {
return FutureBuilder<List<PPProductRenterModel>>
(builder: (context, AsyncSnapshot<List<PPProductRenterModel>> projectSnap){
if(!projectSnap.hasData){
if(projectSnap.connectionState == ConnectionState.waiting){
return Container(
child: Loading(),
);
}
}
return ListView.builder(
itemCount: projectSnap.data.length,
itemBuilder: (context, index) {
PPProductRenterModel model = projectSnap.data[index];
PPPrint(tag: "CheckId",value: model.productId);
return ProductMainItemRow(model);
});
},
future: _homeController.listNearProduct,);
There is a cleaner way for implementing List in GetX without worrying about Type-Casting:
Instantiate it:
final myList = [].obs;
Assign it:
myList.assignAll( listOfAnyType );
(Reference) Flutter error when using List.value :
'value' is deprecated and shouldn't be used. List.value is deprecated.
use [yourList.assignAll(newList)]. Try replacing the use of the
deprecated member with the replacement.
Detailed code example
ProductController.dart
class ProductController extends GetxController {
final productList = [].obs;
#override
void onInit() {
fetchProducts();
super.onInit();
}
void fetchProducts() async {
var products = await HttpServices.fetchProducts();
if (products != null) {
productList.assignAll(products);
}
}
}
HttpServices.dart
class HttpServices {
static var client = http.Client();
static Future<List<Product>> fetchProducts() async {
var url = 'https://link_to_your_api';
var response = await client.get(url);
if (response.statusCode == 200) {
return productFromJson(response.body);
} else {
return null;
}
}
}
product.dart
class Product {
Product({
this.id,
this.brand,
this.title,
this.price,
....
});
....
}
Form the docs:
3 - The third, more practical, easier and preferred approach, just add
.obs as a property of your value:
final items = <String>[].obs;
Following that instruction, this should work:
final listNearProduct = Future.value(<PPProductRenterModel>[]).obs;
E.g.:
// controller
final list = Future.value(<String>[]).obs;
#override
void onInit() {
super.onInit();
fetchList();
}
Future<List<String>> callApi() async {
await Future.delayed(Duration(seconds: 2));
return ['test'];
}
void fetchList() async {
list.value = callApi();
}
// screen
#override
Widget build(BuildContext context) {
return GetX<Controller>(
init: Controller(),
builder: (controller) {
return FutureBuilder<List<String>>(
future: controller.list.value,
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0]); // Output: test
return Text(snapshot.data[0]);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
);
},
);
};
You never actually call FetchNearProductList.
You need to call it in some place, preferably before the FutureBuilder uses that Future.