How to start Flutter app with loading, using state pattern with provider - flutter

I'm building my first app, and for state management I'm using ValueChangeNotifier and Provider with the state pattern. But when I start my app, I get the following error:
Exception has occurred.
FlutterError (setState() or markNeedsBuild() called during build.
This _InheritedProviderScope<EvaluationStore?> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
_InheritedProviderScope<EvaluationStore?>
The widget which was currently being built when the offending call was made was:
Builder)
I don't know how to show my problem without showing my project's classes, so I apologize if this gets too long.
I created a model class.
class EvaluationModel {
final String token;
final DateTime creation;
final String technicians;
final String customer;
final String responsible;
final String compressor;
final int horimeter;
final int oilType;
final int oil;
final int oilFilter;
final int airFilter;
final int separatorFilter;
final int revitalize;
final int revitalization;
final String? technicalAdvice;
final bool uploaded;
// continues with the basic methods of a data class...
}
So I created a service class that is responsible for the EvaluationModel methods, where I created a method to fill my list with data coming from a MySQL database.
class EvaluationService {
Future<List<EvaluationModel>> fetchEvaluations(
String creationStart,
String creationEnd,
String technicians,
String customer,
String compressor) async {
List<EvaluationModel> evaluations = <EvaluationModel>[];
EvaluationModel evaluation;
final MySqlConnection conn = await Database.getDbConnection();
final Results result = await conn.query(
await rootBundle.loadString('lib/assets/evaluation_select.sql'),
[creationStart, creationEnd, technicians, customer, compressor]);
await conn.close();
for (var row in result) {
evaluation = EvaluationModel(
token: row['token'],
creation: row['creation'],
technicians: row['technicians'],
customer: row['customer'],
responsible: row['responsible'],
compressor: row['compressor'],
horimeter: row['horimeter'],
oilType: row['oiltype'],
oil: row['oil'],
oilFilter: row['oilfilter'],
airFilter: row['airfilter'],
separatorFilter: row['separatorfilter'],
revitalize: row['revitalize'],
revitalization: row['revitalization'],
technicalAdvice: row['technicalAdvice'],
uploaded: true);
evaluations.add(evaluation);
}
return evaluations;
}
}
Then I created the EvaluationState and EvaluationStore class to manage the state of my page.
abstract class EvaluationState {}
class InitialEvaluationState extends EvaluationState {}
class LoadingEvaluationState extends EvaluationState {}
class SuccessEvaluationState extends EvaluationState {
final List<EvaluationModel> evaluations;
SuccessEvaluationState(this.evaluations);
}
class ErrorEvaluationState extends EvaluationState {
final String message;
ErrorEvaluationState(this.message);
}
class EvaluationStore extends ValueNotifier<EvaluationState> {
final EvaluationService service;
EvaluationStore(this.service) : super(InitialEvaluationState());
Future fetchEvaluations(String creationStart, String creationEnd,
String technicians, String customer, String compressor) async {
value = LoadingEvaluationState();
try {
final evaluations = await service.fetchEvaluations(
creationStart, creationEnd, technicians, customer, compressor);
value = SuccessEvaluationState(evaluations);
} catch (e) {
value = ErrorEvaluationState(e.toString());
}
}
}
So, to work with the Provider I did it like this in the MyApp class.
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider(create: (_) => EvaluationService()),
ChangeNotifierProvider(
create: (context) => EvaluationStore(context.read()))
],
child: MaterialApp(
title: 'Avaliação',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
),
home: const EvaluationsPage(),
),
);
}
And finally, on the page I'm treating it like this:
class EvaluationsPage extends StatefulWidget {
const EvaluationsPage({Key? key}) : super(key: key);
#override
State<EvaluationsPage> createState() => _EvaluationsPageState();
}
class _EvaluationsPageState extends State<EvaluationsPage> {
#override
void initState() {
super.initState();
context
.read<EvaluationStore>()
.fetchEvaluations('0001-01-01', '9999-12-31', '%', '%', '%');
}
#override
Widget build(BuildContext context) {
final store = context.watch<EvaluationStore>();
final state = store.value;
Widget? child;
if (state is LoadingEvaluationState) {
child = const Center(child: CircularProgressIndicator());
}
if (state is ErrorEvaluationState) {
child = Center(child: Text(state.message));
}
if (state is SuccessEvaluationState) {
child = ListView.builder(
itemCount: state.evaluations.length,
itemBuilder: (context, index) {
return ListTile(title: Text(state.evaluations[index].customer));
});
}
return Scaffold(
appBar: AppBar(title: const Text('Avaliações')),
body: child ?? Container(),
);
}
}
Note: If I remove the line "value = LoadingEvaluationState();" from the Evaluation Store class, the app runs normally.
If anyone can help me, I can even make the project available.
I'm a beginner, I'm totally stuck, I don't know what to try.

the error occured because while execute the initState method, you call rebuild .
simple solution:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
context
.read<EvaluationStore>()
.fetchEvaluations('0001-01-01', '9999-12-31', '%', '%', '%');
}
});

Related

How do I set my state when the widget builds with riverpod?

To preface, I am completely new to riverpod so my apologies if I get some terminology wrong.
I have an edit feature that I'm implementing with riverpod that I'm migrating over to from flutter_blocs. Basically, with the bloc implementation, I am fetching my data from my server and when the widget builds, the BlocConsumer will emit an event to set my data from the server into my bloc state so that I can display and edit it in a TextInput.
Bloc implementation:
BlocConsumer<JournalBloc, JournalState>(
bloc: BlocProvider.of<JournalBloc>(context)
..add(
SetModelState(
title: journalEntry.title,
rating: journalEntry.rating.toDouble(),
),
),
builder: (context, state) {
return Column(
children: [
TextInput(
labelText: 'label',
value: state.editEntryTitle.value,
onChanged: (title) => context.read<JournalBloc>().add(EditJournalEntryTitleChanged(title: title))
)
],
);
}
Now with Riverpod, where I'm stuck on is I don't know how to set my values from my server into my state when the widget renders. I have my controller, and I have my providers.
My controller:
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
EditJournalEntryController({required this.journalService})
: super(EditJournalEntryState());
void setSelectedJournalType(JournalType journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
final editJournalEntryController =
StateNotifierProvider<EditJournalEntryController, EditJournalEntryState>((ref) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService);
});
My state:
class EditJournalEntryState implements Equatable {
final AsyncValue<void> value;
final JournalType? selectedJournalType;
final int? rating;
bool get isLoading => value.isLoading;
EditJournalEntryState({
this.selectedJournalType,
this.rating,
this.value = const AsyncValue.data(null),
});
EditJournalEntryState copyWith({
MealType? selectedJournalType,
int? rating,
AsyncValue? value,
}) {
return EditJournalEntryState(
selectedJournalType: selectedJournalType ?? this.selectedJournalType,
rating: rating ?? this.rating,
value: value ?? this.value,
);
}
#override
List<Object?> get props => [selectedJournalType, rating, value];
#override
bool? get stringify => true;
}
What I have tried
In my UI, I am referencing my controller and then using my notifier to set my state:
class EditJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(
builder: ((context, ref, child) {
final state = ref.watch(editJournalEntryController);
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)
notifier.setRating(journalEntry.rating)
But for obvious reasons I get this error:
At least listener of the StateNotifier Instance of 'EditFoodJournalEntryController' threw an exception
when the notifier tried to update its state.
The exceptions thrown are:
Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies
Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.
To fix this problem, you have one of two solutions:
- (preferred) Move the logic for modifying your provider outside of a widget
life-cycle. For example, maybe you could update your provider inside a button's
onPressed instead.
- Delay your modification, such as by encasuplating the modification
in a `Future(() {...})`.
This will perform your upddate after the widget tree is done building.
Ideally I want to render that value from state and not from the variable I have.
I feel like I've ran into a wall. Is there an ideal way of handling with this?
you are trying to modify a provider while the widget tree is building.
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)//here
notifier.setRating(journalEntry.rating)//here
this will cause the issue, you need to initialize the editJournalEntryController with an initial state you can do it by .family provider modifier
Edit:
Something like this
final editJournalEntryController = StateNotifierProvider.family<
EditJournalEntryController,
EditJournalEntryState,
JournalEntry>((ref, journalEntry) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService,journalentry: journalEntry);
});
EditJournalEntryController will be
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
final JournalEntry journalentry;
EditJournalEntryController({required this.journalService,required this.journalentry})
: super(EditJournalEntryState(selectedJournalType: journalentry.type,rating: journalentry.rating));
void setSelectedJournalType(String journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
will be called like
class EditFoodJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, child) {
final state = ref.watch(editJournalEntryController(journalEntry));
return Container();
});
}
}

DocumentSnapshot returning null and new screen stuck on loading

When this widget is called, its stuck on loading animation, and when hot reloaded, it shows the transactions
`
class TransactionList extends StatefulWidget {
final int groupIndex;
final String groupUid;
const TransactionList(
{super.key, required this.groupIndex, required this.groupUid});
#override
State<TransactionList> createState() => _TransactionListState();
}
class _TransactionListState extends State<TransactionList> {
#override
Widget build(BuildContext context) {
final groupTransaction = Provider.of<DocumentSnapshot?>(context);
if (groupTransaction == null) return const Loading();
return transactionItemsBuilder(groupTransaction);
}`
edit:
this is firebase instance created,
class DatabaseServices {
final String? uid;
DatabaseServices({this.uid});
final CollectionReference groupCollection =
FirebaseFirestore.instance.collection('groups');
Stream<DocumentSnapshot> get transactions {
return groupCollection.doc(uid).snapshots();
}
}
and this is my stream provider
StreamProvider<DocumentSnapshot?>.value(
initialData: null,
value: DatabaseServices(uid: widget.item[widget.index].id)
.transactions,
child: TransactionList(
groupIndex: widget.index,
groupUid: widget.item[widget.index].reference.id),
),
Solved this by providing initial data to the stream provider. The data was obtained earlier.

Storing certain value in Widget build / Flutter

I've a question:
In my Widget build(BuildContext context), I want to store a certain value,
final userName = book.owner
(book is the reference to the certain value from Firestore)
But it's done not in the right way to my lack of knowledge. I'd appreciate if someone could guide through that.
Thank you in advance!
Snippet of my code
class BookView extends StatefulWidget {
final Book book;
BookView({Key key, #required this.book}) : super(key: key);
DatabaseMethods databaseMethods = new DatabaseMethods();
var userName;
#override
_BookViewState createState() => _BookViewState(book);
}
class _BookViewState extends State<BookView> {
Book book;
_BookViewState(this.book);
String userName;
#override
void initState() {
userName = book.owner;
super.initState();
}
// final Book book;
createChatroomAndStartConversation({var userName}) {
if (userName != Constants.myName) {
String roomId = getChatRoomId(userName, Constants.myName);
List<String> users = [userName, Constants.myName];
Map<String, dynamic> chatRoomMap = {
"Users": users,
"roomId": roomId,
};
DatabaseMethods().createChatRoom(roomId, chatRoomMap);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationScreen(roomId, userName)),
);
} else {
print("You cannot send msg to your self");
}
}
#override
Widget build(BuildContext context) {
//widget.book;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
...
FlatButton(
child: Text(
"Get contact with",
style: TextStyle(color: Colors.white),
),
color: Colors.blue,
onPressed: () {
createChatroomAndStartConversation(
userName: userName);
...
}
Snippet of Value not in range: 1
getChatRoomId(String a, String b) {
if (a.substring(0, 1).codeUnitAt(0) > b.substring(0, 1).codeUnitAt(0)) {
return "$b\_$a";
} else {
return "$a\_$b";
}
}
It's not a good practice to store any data in build() method, because this method is invoked too many times to do the such kind of move. Consider using StatefulWidget to store any state you have in the widget, for the very beginning. When you use this widget, you can define this all in such way:
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String userName;
#override
void initState() {
userName = book.owner;
super.initState()
}
#override
Widget build(BuildContext context) {
return Container(child: Text(userName),);
}
}
Here, in initState() you can retrieve value from book and set it to userName. But for more complex and bigger applications, consider using StateManagement solutions and some kind of architectural patterns i.e. Riverpod, Provider, MobX, BLoC.. Because changing the state via setState() method will cause rebuilding whole child widget tree, which could freeze whole UI in complex app.
UPD to 'Snippet of my code':
According to your code, if you are using a 'book' from Widget, not its state - use widget.book, in such way you have access to widget members, because of this you don't need a constructor of state. So, due to these changes, your code might looks like:
class BookView extends StatefulWidget {
final Book book;
BookView({Key key, #required this.book}) : super(key: key);
// You DON'T need this here, because you are retrieving these methods
// inside your state via DatabaseMethods constructor
DatabaseMethods databaseMethods = DatabaseMethods();
#override
_BookViewState createState() => _BookViewState(book);
}
class _BookViewState extends State<BookView> {
String userName;
#override
void initState() {
// Using widget.book to retrieve Book object from state's widget
userName = widget.book.owner;
super.initState();
}
createChatroomAndStartConversation({var userName}) {
if (userName != Constants.myName) {
String roomId = getChatRoomId(userName, Constants.myName);
// Also, it's just a recommendation, try to omit local variables types
// because they are already known with List type (String). Also, this
// all is about chatRoomMap
var users = <String>[userName, Constants.myName];
final chatRoomMap = <String, dynamic>{
"Users": users,
"roomId": roomId,
};
DatabaseMethods().createChatRoom(roomId, chatRoomMap);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationScreen(roomId, userName)),
);
} else {
print("You cannot send msg to your self");
}
}
#override
Widget build(BuildContext context) {
// your widgets here
}
}
UPD 2:
Second trouble and issue with 'Snippet of Value not in range: 1'. I could to reproduce it with given value of 'a' as empty string. So, your function invocation is like getChatRoomId('', 'user123'), because of empty 'userName', substring function can't take values from range [0, 1), so exception is raised.

Widget continuously being reloaded

Please check out this 36 seconds video for more clarity, cause it was getting too verbose explaning things : https://www.youtube.com/watch?v=W6WdQuLjrCs
My best guess
It's due to the provider.
App structure ->
Outer Page -> NoteList Page
The Outer Page code :
class OuterPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
var noteList;
final _pageOptions = [
NoteList(),
AnotherPageScreen(),
];
#override
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
var customFabButton;
if (_selectedTab == 0) {
customFabButton = FloatingActionButton(
// Password section
onPressed: () {
navigateToDetail(context, Note('', '', 2), 'Add Note');
},
child: Icon(Icons.add),
);
~~~ SNIP ~~~
The Notes Tab aka NoteList page code :
class NoteList extends StatefulWidget {
NoteList();
#override
NoteListState createState() => NoteListState();
}
class NoteListState extends State<NoteList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context).getNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context).count > 0
? NoteListScreen(_scaffoldKey)
: CircularProgressIndicator());
}
}
For full code : check here : https://github.com/LuD1161/notes_app/tree/reusable_components
Update 1 - Possible solution is FutureBuilder
I know that there's a possible solution with FutureBuilder but I think even Provider is apt for this use case.
Moreover is it an anti-pattern here ?
Also, please don't suggest another package for the same thing, if possible try limiting the solution to Provider or base libraries.
Update 2 - Not possible with FutureBuilder
FutureBuilder can't be used here because there's a delete button in the list tile and hence when the note gets deleted the note list won't get updated.
The issue is coming because of the getNotes function you are calling from build method. You are calling notifyListeners from that function. It again re-builds the widget and calls the build method again and this cycle continues.
You either need to set false to the listen property of provider, but it will break your functionality. To fix this, you have to move the getNotes call from build function to initState like following:
#override
void initState() {
super.initState();
postInit(() {
Provider.of<NotesProvider>(context).getNotes();
});
}
Implement postInit (Reference: Flutter Issue 29515):
extension StateExtension<T extends StatefulWidget> on State<T> {
Stream waitForStateLoading() async* {
while (!mounted) {
yield false;
}
yield true;
}
Future<void> postInit(VoidCallback action) async {
await for (var isLoaded in waitForStateLoading()) {}
action();
}
}
Note: Instead of writing the postInit code, you can also use after_init package for same purpose.
Several other posts discussing similar kind of issues:
How to correctly fetch APIs using Provider in Flutter
Using provider in fetching data onLoad

Handling variables for stateful widget

I have ListView widget whose contents are loaded dynamically.
So I decided to make myStatelessWidget.
My basic ideas are
Keep variable articles to be shown on ListView in the StatefulWidget or State.
Pass the contents from outside.
So for now, I write like this, but it has error.
Is my basic idea is correct? or where should I fix?
//// to pass the argument from outside.
new BodyLayout(articles: myarticles),
////
class BodyLayout extends StatefulWidget {
// List<Article> articles // ???I should put here/?
BodyLayout({articles});
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
// List<Article> articles // ???I should put here/?
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.articles.length, // the getter 'articles' is not defined error....
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.articles[index].title),
onTap: () => onTapped(context,widget.articles[index].url),
);
},
);
}
}
You only need to use a stateful widget if you are going to call the setState() method to rebuild the widget with some new state. One case in which you might do that, if you need to retrieve the list of articles from some api or database call, is to have the widget return a loading indicator if the articles list is null, make the async call to retrieve the articles in the state class's initState() method, and when it is returned, rebuild the widget by calling setState() with the retrieved list of articles. Like this, maybe:
/// to pass the argument from outside.
new BodyLayout(),
///
class BodyLayout extends StatefulWidget {
BodyLayout();
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
List<Article> articles;
bool loading = true;
#override
void initState(){
_getArticles();
}
void getArticles() async {
articles = await Repository.instance.getArticles(); //some async method to retrieve the articles
setState((){
loading = false;
}); // after the articles are retrieved you can call setState to rebuild the widget
}
#override
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
onTap: () => onTapped(context, articles[index].url),
);
},
);
}
}
If you have the list of articles to begin with and don't need to rebuild the list, you can just make that a stateless widget and pass in the list of articles.
The error you indicated that you got, seems to be because articles is not actually defined as a variable for that class. Dart supports multiple syntax options for passing instance variables like this but this is how I would define that variable and ensure that it is being passed in when the widget is created (could be stateless or stateful widget):
//// to pass the argument from outside.
new BodyLayout(articles: myarticles),
////
class BodyLayout extends StatelessWidget {
final List<Article> articles
BodyLayout({this.articles}) : assert(articles != null);
#override
Widget build(BuildContext context){ ... };
}
If you want to convert your widget to a StatelessWidget, then you can just delete the createState and move the stuff in the build method of the state class into the widget class. This works just fine if your widget doesn't maintain an internal state, but if it has interactive elements (like buttons or such) you will want to delegate them to the parent widget caller via a callback.
To define properties for your custom widget, define the fields as final and instantiate them in the class constructor with this.fieldName. For example:
class BodyLayout extends StatefulWidget {
BodyLayout({
this.articles,
this.onArticleTapped,
});
final List<Article> articles; // Defining the articles property
final void Function(String) onArticleTapped; // Defining the on-tapped callback
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.articles[index].title),
onTap: () => onArticleTapped(widget.articles[index].url),
);
},
);
}
}
You can then use it like such:
...
BodyLayout(
articles: [some list of articles],
onArticleTapped: (url) => <do something with url>
),