Provider rebuilds the widget, but nothing shows up until a "Hot restart" - flutter

I am building a flutter app and I get some data from a future, I also got the same data with a changenotifier. Well the logic is that while some object doesn't have data because its waiting on the future then display a spinning circle. I have already done this in the app and I have a widget called Loading() when the object has not received data. The problem I have run into is that I get the data, but it doesn't display anything.
the data displays correctly until I perform a hot refresh of the app. a capital R instead of a lowercase r. The difference is that it starts the app and deletes all aggregated data.
when this happens it seems that the data fills the object but I hypothesize that it is becoming not null meaning [] which is empty but not null and is displaying the data "too quickly" this in turn displays nothing for this widget until I restart "r" which shows me the above screenshot.
here is the offending code.
import 'package:disc_t/Screens/LoggedIn/Classes/classTile.dart';
import 'package:disc_t/Screens/LoggedIn/Classes/classpage.dart';
import 'package:disc_t/Screens/LoggedIn/Classes/classpageroute.dart';
import 'package:disc_t/Services/database.dart';
import 'package:disc_t/models/user.dart';
import 'package:disc_t/shared/loading.dart';
import 'package:flutter/material.dart';
import 'package:morpheus/page_routes/morpheus_page_route.dart';
import 'package:provider/provider.dart';
class ClassList extends StatefulWidget {
#override
_ClassListState createState() => _ClassListState();
}
class _ClassListState extends State<ClassList> {
#override
void initState() {
ClassDataNotifier classdatanotif =
Provider.of<ClassDataNotifier>(context, listen: false);
// final user = Provider.of<User>(context);
// getTheClasses(classdatanotif);
// List<ClassData> d = classes;
}
#override
Widget build(BuildContext context) {
ClassDataNotifier classdatanotif = Provider.of<ClassDataNotifier>(context);
List<ClassData> cData = Provider.of<List<ClassData>>(context);
bool rebd = false;
Widget checker(bool r) {
if (cData == null) {
return Loading();
} else {
if (rebd == false) {
setState(() {
rebd = true;
});
rebd = true;
return checker(rebd);
// return Text("Still Loading");
} else {
return PageView.builder(
scrollDirection: Axis.horizontal,
itemCount: cData.length,
// controller: PageController(viewportFraction: 0.8),
itemBuilder: (context, index) {
return Hero(
tag: cData[index],
child: GestureDetector(
onTap: () {
// Navigator.of(context).push(ClassPageRoute(cData[index]));
Navigator.push(
context,
MorpheusPageRoute(
builder: (context) =>
ClassPage(data: cData[index]),
transitionToChild: true));
},
child: ClassTile(
classname: cData[index].classname,
description: cData[index].classdescription,
classcode: cData[index].documentID,
),
),
);
});
}
}
}
return checker(rebd);
}
}
here is how the provider is implemented
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
// final DatabaseService ds = DatabaseService();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(
value: AuthService().user,
// child: MaterialApp(
// home: Wrapper(),
// ),
),
ChangeNotifierProvider<ClassDataNotifier>(
create: (context) => ClassDataNotifier(),
),
FutureProvider(
create: (context) => DatabaseService().fetchClassdata,
)
],
child: MaterialApp(home: Wrapper()),
);
}
}
and here is the function that is ran to get the data
Future<List<ClassData>> get fetchClassdata async {
QuerySnapshot snapshot = await classesCollection.getDocuments();
List<ClassData> _classList = List<ClassData>();
snapshot.documents.forEach((element) async {
QuerySnapshot pre = await Firestore.instance
.collection("Classes")
.document(element.documentID)
.collection("Pre")
.getDocuments();
List<Preq> _preList = List<Preq>();
pre.documents.forEach((preClass) {
Preq preqData = Preq.fromMap(preClass.data);
if (preClass.data != null) {
_preList.add(preqData);
}
});
ClassData data =
ClassData.fromMap(element.data, element.documentID, _preList);
if (data != null) {
_classList.add(data);
}
});
return _classList;
}

I think the logic of your provider is fine, the problem lies in the line
snapshot.documents.forEach((element) async {
...
}
The forEach is not a Future (what is inside it's a future because the async, but the method itself not) so the code runs the first time, it reaches the forEach which does its own future on each value and propagate to the next line of code, the return, but the list is empty because the forEach isn't done yet.
There is a special Future.forEach for this case so you can wait for the value method before running the next line
Future<List<ClassData>> get fetchClassdata async {
QuerySnapshot snapshot = await classesCollection.getDocuments();
List<ClassData> _classList = List<ClassData>();
await Future.forEach(snapshot.documents, (element) async {
QuerySnapshot pre = await Firestore.instance
.collection("Classes")
.document(element.documentID)
.collection("Pre")
.getDocuments();
List<Preq> _preList = List<Preq>();
pre.documents.forEach((preClass) {
Preq preqData = Preq.fromMap(preClass.data);
if (preClass.data != null) {
_preList.add(preqData);
}
});
ClassData data =
ClassData.fromMap(element.data, element.documentID, _preList);
if (data != null) {
_classList.add(data);
}
});
return _classList;
}
Here is a similar problem with provider with a forEach. Maybe it can help you understand a bit better

Related

ChangeNotifierProvider does not update the model

i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.

StreamBuilder / ChangeNotifierProvider- setState() or markNeedsBuild() called during build

Streambuilder, ChangeNotifier and Consumer cannot figure out how to use correctly. Flutter
I've tried and tried and tried, I've searched a lot but I cannot figure this out:
I'm using a Streambuilder this should update a ChangeNotifier that should trigger rebuild in my Consumer widget. Supposedly...
but even if I call the provider with the (listen: false) option I've got this error
The following assertion was thrown while dispatching notifications for
HealthCheckDataNotifier: setState() or markNeedsBuild() called during
build. the widget which was currently being built when the offending call was made was:
StreamBuilder<List>
Important: I cannot create the stream sooner because I need to collect other informations before reading firebase, see (userMember: userMember)
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// I have other provider...
ChangeNotifierProvider<HealthCheckDataNotifier>(create: (context) => HealthCheckDataNotifier())
],
child: MaterialApp(...
then my Change notifier look like this
class HealthCheckDataNotifier extends ChangeNotifier {
HealthCheckData healthCheckData = HealthCheckData(
nonCrewMember: false,
dateTime: DateTime.now(),
cleared: false,
);
void upDate(HealthCheckData _healthCheckData) {
healthCheckData = _healthCheckData;
notifyListeners();
}
}
then the Streambuilder
return StreamBuilder<List<HealthCheckData>>(
stream: HeathCheckService(userMember: userMember).healthCheckData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
healthCheckData = snapshot.data?.first;
}
if (healthCheckData != null) {
timeDifference = healthCheckData!.dateTime.difference(DateTime.now()).inHours;
_cleared = healthCheckData!.cleared;
if (timeDifference < -12) {
healthCheckData!.cleared = false;
_cleared = false;
}
///The problem is here but don't know where to put this or how should be done
Provider.of<HealthCheckDataNotifier>(context, listen: false).upDate(healthCheckData!);
}
}
return Builder(builder: (context) {
return Provider<HealthCheckData?>.value(
value: healthCheckData,
builder: (BuildContext context, _) {
return const HealthButton();
},
);
});
} else {
return const Text('checking health'); //Scaffold(body: Loading(message: 'checking...'));
}
});
and finally the Consumer (note: the consumer is on another Route)
return Consumer<HealthCheckDataNotifier>(
builder: (context, hN, _) {
if (hN.healthCheckData.cleared) {
_cleared = true;
return Container(
color: _cleared ? Colors.green : Colors.amber[900],
Hope is enough clear,
Thank you so very much for your time!
it is not possible to setState(or anything that trigger rerender) in the builder callback
just like you don't setState in React render
const A =()=>{
const [state, setState] = useState([])
return (
<div>
{setState([])}
<p>will not work</p>
</div>
)
}
it will not work for obvious reason, render --> setState --> render --> setState --> (infinite loop)
so the solution is similar to how we do it in React, move them to useEffect
(example using firebase onAuthChange)
class _MyAppState extends Stateful<MyApp> {
StreamSubscription<User?>? _userStream;
var _waiting = true;
User? _user;
#override
void initState() {
super.initState();
_userStream = FirebaseAuth.instance.authStateChanges().listen((user) async {
setState(() {
_waiting = false;
_user = user;
});
}, onError: (error) {
setState(() {
_waiting = false;
});
});
}
#override
void dispose() {
super.dispose();
_userStream?.cancel();
}
#override
Widget build(context) {
return Container()
}
}

Flutter - Async function not being waited for

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

Parameters from Stream<List> not being received in the MainPage

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;
});
});
}

Flutter nested provider not getting the latest values despite notifyListeners() being called

Say, I have 2 widgets, A and B, where B is nested inside A. Both widgets are wrapped using Consumer. However, only widget A is able to get latest values from the provider, whereas widget B remains as the initial state.
class WidgetA extends StatelessWidget {
Widget build(BuildContext context) {
final FooProvider fooProvider = Provider.of<FooProvider>(context, listen: false);
fooProvider.fetchData();
return Consumer<FooProvider>(
builder: (context, value, child) {
print(value.modelList[0].name); //able to get latest value whenever changes are made to FooProvider.
return GestureDetector(
onTap: () async {
foodProvider.fetchData();
return showDialog(
context: context,
builder: (BuildContext context) {
return WidgetB(); //NOTICE I'm calling WidgetB here
}
)
},
child: WidgetB(); //NOTICE I'm calling WidgetB here
);
}
)
}
}
class WidgetB extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<FooProvider>(
builder: (context, value, child) {
print(value.modelList[0].name); //unable to get latest in showDialog
return Container();
}
)
}
}
EDIT The code for ChangeNotifier:
It's just a regular Provider doing its work.
List<FooModel> modelList = [];
bool isWithinTimeFrame = false;
Future<void> fetchData(email, token, url) async {
await Service(
email,
token,
).fetchCutOff(url).then((response) {
if (response.statusCode == 200) {
var jsonResponse = json.decode(response.body.toString());
bool isSuccess = jsonResponse["success"];
if (isSuccess) {
dynamic formattedResponse = jsonResponse["data"];
List<FooModel> modelList = formattedResponse
.map<FooModel>((json) => FooModel.fromJson(json))
.toList();
setModelList(modelList);
setIsWithinTimeFrame(computeTime(modelList));
} else {}
} else {}
});
}
void setModelList(value) {
modelList = value;
notifyListeners();
}
void setIsWithinTimeFrame(value) {
isWithinTimeFrame = value;
notifyListeners();
}