Flutter Provider doesn't rebuild widget - flutter

here's a piece of my code,
but please read the question before jumping to conclusions:
class RescheduleCard extends StatelessWidget {
#override
Widget build(BuildContext context)=> Consumer<AppSnapshot>(
builder: (context, appSnapshot, _)=>
(appSnapshot.reschedule!=null)
?RescheduleBuilder(
model: appSnapshot.reschedule,
controllers: appSnapshot.controllers,
)
:Carouselcard(
title: carouselPageTitle(title: rescheduleTitle,test: appSnapshot.test),
colors: yellows,
so I'm sort of new to provider and I'm used to Bloc,
my api once receive some call the provider and set a model;
edit: here's the provider 256 lines tripped down to what concern
the "reschedule"...
class AppSnapshot with ChangeNotifier {
RescheduleModel _reschedule;
RescheduleModel get reschedule => _reschedule;
void cleanReschedule() {
reschedule=null;
notifyListeners();
}
set reschedule(RescheduleModel reschedule) {
_reschedule=reschedule;
notifyListeners();
}
}
re-edit: on top of everything:
void main() async {
final AppSnapshot _appSnapshot = AppSnapshot();
await _appSnapshot.load();
runApp(ChangeNotifierProvider(
builder: (context) => _appSnapshot,
child: Initializer()));
}
I'm expecting the "Consumer" to rebuild my widget,
but doesn't!
I'm sure the data is there, because the widget is inside a carousel
and as soon as I move it it rebuilds properly.
what am I missing?
thank you
re-re-edit: here's an example of another Carousel Card where provider works quilckly and changes apply in realtime
Consumer<AppSnapshot>(
builder: (context, appSnapshot, _)=> Carouselcard(
title: carouselPageTitle(title: optionsTitle,test: appSnapshot.test),
colors: lightBlues,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
((appSnapshot.id??'')!='') // ID WIDGET
? ListTile(
leading: CustomIcon(
icon: deleteIcon,
onTap: () => appSnapshot.deleteID(),
),
title: _customCard(onTap:()=> _showID(id: appSnapshot.id,context: context)),
subtitle: Text(tap2delete,textScaleFactor:0.8),
)
: IdTile(),

According to this article the FutureProvider does not listen for changes, it will only rebuild Consumers 1 time, when Future is complete.
The article even explains how the author tested this.
The developer of the Dart Provider package also explains the use case for FutureProvider.

Specificies the type in main.dart
void main() async {
final AppSnapshot _appSnapshot = AppSnapshot();
await _appSnapshot.load();
runApp(ChangeNotifierProvider< **AppSnapshot** >(
builder: (context) => _appSnapshot,
child: Initializer()));
}

Related

Update List Item After Delete Action in Flutter

Hello I am new to flutter and I have a problem to update the list after executing a deleting an item from the database. Some said to use setState, but I still don't know how to implement it in my code. Tried to call seState right after the delete action, but still nothing happened. Still have some trouble to understand which component to update in Flutter. Thank you.
class ProfileView extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _ProfileViewState();
}
}
class _ProfileViewState extends State<ProfileView> {
late Future<List<Patient>> _patients;
late PatientService patientService;
#override
void initState() {
super.initState();
patientService = PatientService();
_patients = patientService.getPatient();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: <Widget>[
Flexible(
child: SizedBox(
child: FutureBuilder<List<Patient>>(
future: _patients,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.hasError) {
print(snapshot);
return Center(
child: Text("Error"),
);
} else if (snapshot.hasData){
List<Patient> patients = snapshot.data;
return _buildListView(patients);
} else {
return Center(
child: Container(),
);
}
},
),
),
)
],
),
);
}
Widget _buildListView(List<Patient> patients) {
return ListView.separated(
separatorBuilder: (BuildContext context, int i) => Divider(color: Colors.grey[400]),
itemCount: patients.length,
itemBuilder: (context, index) {
Patient patient = patients[index];
return Row(
children: <Widget>[
Flexible(
child: SizedBox(
child: ListTile(
title: Text(patient.firstName),
subtitle: Text(patient.phone),
trailing: IconButton(
icon: new Icon(const IconData(0xf4c4, fontFamily: 'Roboto'), size: 48.0, color: Colors.red),
onPressed: () {
patientService.deletePatient(patient.id.toString());
}),
),
)
)
],
);
}
);
}
}
You can achieve that by removing the initialization of the future from the initState and simply give the future of patientService.getPatient() to the FutureBuilder, like this:
future: patientService.getPatient()
And call setState after making sure the patient have been successfully deleted.
The explanation behind doing that is when you delete your patient from the DB, yes it is removed from there, but the UI didn't get an update about the list of patients after the delete. And the reason why calling setState in your case doesn't make a change is because you are assigning the future in initState which is called once and only once when the widget is initialized. So when you call setState the future won't be called again hence no new data is fetched.
So what I did is just remove the initialization of the future from initState and give it to the FutureBuilder, which will be rebuild whenever you call setState.
Even though this works, it isn't the ideal solution. Because you are rebuilding your whole widget every time a delete is made which is kinda of heavy considering the FutureBuilder, so what I suggest is checking out some state mangement solutions like Bloc or Mobx or even the Provider package (which isn't a state mangement according to its creator).
Hope that makes clear !
Happy coding !
call setState() inside the onPressed method.
onPressed: () {
patientService.deletePatient(patient.id.toString());
setState((){});
}),
If you are not saving a local copy of the list from which you are deleting an item then this works
Although if the delete method deletes on from wherever you are fetching the items then you will need to call
_patients = patientService.getPatient();
before calling setState()
I think your deletePatient is asynchronous method. And you are calling this method without await and after this function setState is called thus widget is getting updated before delete is completed.
If deletePatient is asynchronous then add await before calling it and add setState after it.
onPressed: () async {
await patientService.deletePatient(patient.id.toString());
setState((){});
})

Provider: setState() or markNeedsBuild() called during build

I'm using Hive to store the whole list of Card items, related to an Extension. In the screen of the selected extension, I am displaying the Cards of this extension, with bunch of filters/sorting.
To do that, I use ValueListenableBuilder to get the current extension and their cards.
And when I filter/sort this list, I want to store them into my Provider class, because I would to reuse this list into another screen.
But we I do context.read<ShowingCardsProvider>().setAll(sortedList), I got this error:
setState() or markNeedsBuild() called during build.
I don't listen ShowingCardsProvider anywhere for now.
Can you explain to me what's wrong here?
Thank you!
main.dart
runApp(MultiProvider(
providers: [
ChangeNotifierProvider.value(value: AuthProvider()),
ChangeNotifierProvider.value(value: UserProvider()),
ChangeNotifierProvider.value(value: ShowingCardsProvider()),
],
child: MyApp(),
));
showing_cards_provider.dart
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:pokecollect/card/models/card.dart';
class CardsProvider extends ChangeNotifier {
final List<CardModel> _items = [];
UnmodifiableListView<CardModel> get items => UnmodifiableListView(_items);
void setAll(List<CardModel>? items) {
_items.clear();
if (items != null || items!.isNotEmpty) {
_items.addAll(items);
}
notifyListeners();
}
void addAll(List<CardModel> items) {
_items.addAll(items);
notifyListeners();
}
void removeAll() {
_items.clear();
notifyListeners();
}
}
extension_page.dart
#override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
body: SafeArea(
child: LayoutBuilder(builder: (context, constraints) {
return CustomScrollView(
slivers: [
SliverAppBar(
...
),
...
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
sliver: ValueListenableBuilder(
valueListenable: ExtensionBox.box.listenable(keys: [_extensionUuid]),
builder: (ctx, Box<Extension> box, child) {
List<CardModel>? cardsList = box
.get(_uuid)!
.cards
?.where((card) => _isCardPassFilters(card))
.cast<CardModel>()
.toList();
var sortedList = _simpleSortCards(cardsList);
context.read<ShowingCardsProvider>().setAll(sortedList);
return _buildCardListGrid(sortedList);
},
),
),
]
);
}),
),
);
}
This is indeed because I'm using ValueListenableBuilder and, while the Widget is building, notifyListeners is called (into my provider).
My hotfix is to do this:
return Future.delayed(
Duration(milliseconds: 1),
() => context.read<ShowingCardsProvider>().setAll(list),
);
Not very beautiful, but it works 😅
If you have a more elegant way, fell free to comment it!

How to use provider to create a commit/discard changes pattern?

What would be a best practice for (provider based) state management of modal widgets in flutter, where when user makes an edit changes do not propagate to parent page until user confirms/closes modal widget. Optionally, user has a choice to discard the changes.
In a nutshell:
modal widget with OK and cancel actions, or
modal widget where changes are applied when modal is closed
Currently, my solution looks like this
Create a copy of the current state
Call flutter's show___() function and wrap widgets with a provider (using .value constructor) to expose copy of the state
If needed, update original state when modal widget is closed
Example of case #2:
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
final tmpState = MeState.from(orgState);
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => ChangeNotifierProvider<MeState>.value(
value: tmpState,
builder: (context, _) => _buildEditDialogWidgets(context)),
);
// update original state (no discard option to keep it simple)
orgState.update(tmpState);
}
But there are issues with this, like:
Where should I dispose tmpState?
ProxyProvider doesn't have .value constructor.
If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
UPDATE: In my current app I have a MultiProvider widget at the top of widget tree, that creates and provides multiple filter state objects. Eg. FooFiltersState, BarFiltersState and BazFiltersState. They are separate classes because each these three extends either ToggleableCollection<T> extends ChangeNotifier or ToggleableCollectionPickerState<T> extends ToggleableCollection<T> class. An abstract base classes with common properties and functions (like bool areAllSelected(), toggleAllSelection() etc.).
There is also FiltersState extends ChangeNotifier class that contains among other things activeFiltersCount, a value depended on Foo, Bar and Baz filters state. That's why I use
ChangeNotifierProxyProvider3<
FooFiltersState,
BarFilterState,
BazFilterState,
FiltersState>
to provide FiltersState instance.
User can edit these filters by opening modal bottom sheet, but changes to filters must not be reflected in the app until bottom sheet is closed by taping on the scrim. Changes are visible on the bottom sheet while editing.
Foo filters are displayed as chips on the bottom sheet. Bar and baz filters are edited inside a nested dialog windows (opened from the bottom sheet). While Bar or Baz filter collection is edited, changes must be reflected only inside the nested dialog window. When nested dialog is confirmed changes are now reflected on bottom sheet. If nested dialog is canceled changes are not transferred to the bottom sheet. Same as before, these changes are not visible inside the app until the bottom sheet is closed.
To avoid unnecessary widget rebuilds, Selector widgets are used to display filter values.
From discussion with yellowgray, I think that I should move all non-dependent values out of proxy provider. So that, temp proxy provider can create new temp state object that is completely independent of original state object. While for other objects temp states are build from original states and passed to value constructors like in the above example.
1. Where should I dispose tmpState?
I think for your case, you don't need to worry about it. tmpState is like a temporary variabl inside function showEditDialog()
2. ProxyProvider doesn't have .value constructor.
It doesn't need to because it already is. ProxyProvider<T, R>: T is a provider that need to listen to. In your case it is the orgState. But I think the orgState won't change the value outside of this function, so I don't know why you need it.
3. If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
you can still access the orgState inside _buildEditDialogWidgets and update it by context.read(). But I think you shouldn't use same type twice in the same provider tree (MeState)
Actually when I first see your code, I will think why you need to wrap tmpState as another provider (your _buildEditDialogWidgets contains more complicated sub-tree or something else that need to use the value in many different widget?). Here is the simpler version I can think of.
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => _buildEditDialogWidgets(context,MeState.from(orgState)),
);
}
...
Widget _buildEditDialogWidgets(context, model){
...
onSubmit(){
context.read<MeState>().update(updatedModel)
}
...
}
The simplest way is you can just provide a result when you pop your dialog and use that result when updating your provider.
import 'dart:collection';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class Item {
Item(this.name);
String name;
Item clone() => Item(name);
}
class MyState extends ChangeNotifier {
List<Item> _items = <Item>[];
UnmodifiableListView<Item> get items => UnmodifiableListView<Item>(_items);
void add(Item item) {
if (item == null) {
return;
}
_items.add(item);
notifyListeners();
}
void update(Item oldItem, Item newItem) {
final int indexOfItem = _items.indexOf(oldItem);
if (newItem == null || indexOfItem < 0) {
return;
}
_items[indexOfItem] = newItem;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(_) {
return ChangeNotifierProvider<MyState>(
create: (_) => MyState(),
builder: (_, __) => MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(
builder: (BuildContext context) => Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton(
onPressed: () => _addItem(context),
child: const Text('Add'),
),
Expanded(
child: Consumer<MyState>(
builder: (_, MyState state, __) {
final List<Item> items = state.items;
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, int index) => GestureDetector(
onTap: () => _updateItem(context, items[index]),
child: ListTile(
title: Text(items[index].name),
),
),
);
},
),
),
],
),
),
),
),
),
);
}
Future<void> _addItem(BuildContext context) async {
final Item item = await showDialog<Item>(
context: context,
builder: (BuildContext context2) => AlertDialog(
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context2),
child: const Text('Cancel'),
),
FlatButton(
onPressed: () => Navigator.pop(
context2,
Item('New Item ${Random().nextInt(100)}'),
),
child: const Text('ADD'),
),
],
),
);
Provider.of<MyState>(context, listen: false).add(item);
}
Future<void> _updateItem(BuildContext context, Item item) async {
final Item updatedItem = item.clone();
final Item tempItem = await showModalBottomSheet<Item>(
context: context,
builder: (_) {
final TextEditingController controller = TextEditingController();
controller.text = updatedItem.name;
return Container(
height: 300,
child: Column(
children: <Widget>[
Text('Original: ${item.name}'),
TextField(
controller: controller,
enabled: false,
),
TextButton(
onPressed: () {
updatedItem.name = 'New Item ${Random().nextInt(100)}';
controller.text = updatedItem.name;
},
child: const Text('Change name'),
),
TextButton(
onPressed: () => Navigator.pop(context, updatedItem),
child: const Text('UPDATE'),
),
TextButton(
onPressed: () => Navigator.pop(context, Item(null)),
child: const Text('Cancel'),
),
],
),
);
},
);
if (tempItem != null && tempItem != updatedItem) {
// Do not update if "Cancel" is pressed.
return;
}
// Update if "UPDATE" is pressed or dimissed.
Provider.of<MyState>(context, listen: false).update(item, updatedItem);
}
}

using Provider to avoid rebuild all widget tree in flutter

as my understand, one of Provider benefits is to avoid rebuild the widget tree, by calling build function, but when i try it practically with this simple example:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:providerexamplelast/counterWidget.dart';
void main() => runApp(ChangeNotifierProvider<Provider1>(
create: (_) => Provider1(),
child: MaterialApp(
home: Counter(),
),
));
int n =0;
class Counter extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("${n++}");
var counter = Provider.of<Provider1>(context);
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: ()=> counter.counter(),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("1"),
SizedBox(height: 5,),
countText(),
SizedBox(height: 5,),
Text("3"),
SizedBox(height: 5,),
Text("4"),
],
),
),
);
}
}
Widget countText(){
return Builder(
builder: (context){
var count = Provider.of<Provider1>(context);
return Text("${count.c}");
},
);
}
by using this part :
print("${n++}");
i noticed that (build) function is recall whenever i press the button and call (counter) function from provider?
so the question here it is just (Stateless) widget, how it rebuild again? and then why i need to use Provider if it is not solve this problem?
Edit: I heard about this way:
var counter = Provider.of<Provider1>(context, listen: false);
so is it solve this problem? and how?
var counter = Provider.of<Provider1>(context, listen: false);
The Provider is one of state management mechanism which flutter have, under the hood, Provider keeps track of the changes which are done inside the widget. It doesn't matter to Provider whether it's a Stateless widget or Stateful widget, It's gonna rebuild widget if anything gets change.
listen: false tells the Provider not to rebuild widget even if data gets modified.

Using Scoped Model to maintain app state in flutter

I need help creating the architecture for my application. I am using Flutter and scoped_model to maintain state.
It's an application that has a login, that displays news in one part of the application, and shows a photo gallery among others. I would like to split this entire thing into separate Models. LoginModel that holds Login state (like username, token, name etc.). NewsModel that contains news retrieved from the API. GalleryModel to hold names of photos etc. I am not sure if this is the best practice to maintain state using scoped_model.
For eg, what If a text box depends on both LoginModel and NewsModel? I am not sure, but I guess it's not possible to retrieve state from two separate models.
Also, the main reason I am maintaining separate Models to hold state is that I don't want the Login part of the app to get refreshed when I bring news. I guess that's how it goes when I put the entire state in a single model.
The scoped_model library is designed to work with multiple models in play at the same time. That's part of the reason that ScopedModel and ScopedModelDescendant are generics and have a type parameter. You can define multiple models near the top of your Widget tree using ScopedModel<LoginModel> and ScopedModel<NewsModel> and then consume those models lower in the tree using ScopedModelDescendant<LoginModel> and ScopedModelDescendant<NewsModel>. The descendants will go looking for the appropriate model based on their type parameter.
I knocked together a quick example. Here are the models:
class ModelA extends Model {
int count = 1;
void inc() {
count++;
notifyListeners();
}
}
class ModelB extends Model {
int count = 1;
void inc() {
count++;
notifyListeners();
}
}
And here's what I'm displaying in the app:
ScopedModel<ModelA>(
model: ModelA(),
child: ScopedModel<ModelB>(
model: ModelB(),
child: ScopedModelDescendant<ModelA>(
builder: (_, __, a) => ScopedModelDescendant<ModelB>(
builder: (_, __, b) {
return Center(
child: Column(
children: [
GestureDetector(
onTap: () => a.inc(),
child: Text(a.count.toString()),
),
SizedBox(height:100.0),
GestureDetector(
onTap: () => b.inc(),
child: Text(b.count.toString()),
),
],
),
);
},
),
),
),
)
It seems to be working just fine. A non-nested approach works as well:
ScopedModel<ModelA>(
model: ModelA(),
child: ScopedModel<ModelB>(
model: ModelB(),
child: Column(
children: [
ScopedModelDescendant<ModelA>(
builder: (_, __, model) => GestureDetector(
onTap: () => model.inc(),
child: Text(model.count.toString()),
),
),
SizedBox(height: 100.0),
ScopedModelDescendant<ModelB>(
builder: (_, __, model) => GestureDetector(
onTap: () => model.inc(),
child: Text(model.count.toString()),
),
),
],
),
),
)
I wanted to give you a simple example on ScopedModel.
pubspec.yaml file must include :-
dependencies:
scoped_model: ^1.0.1
then,
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() => runApp(MyApp()); //main method
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(), //new class MyHomePage
);
}
}
//-----------------------------------CounterModel [used by ScopedModel]
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
//-----------------------------------ends
//-----------------------------------MyHomePage class
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return ScopedModel( // ScopedModel used on top of the widget tree [it is wrapping up scaffold]
model: CounterModel(), // providing the CounterModel class as model
child: Scaffold(
appBar: AppBar(),
body: Container(
child: ScopedModelDescendant<CounterModel>( // ScopedModelDescendant accessing the data through ScopedModel
builder: (context, _, model) => Text("${model._counter}"), // fetching data from model without thinking of managing any state.
),
),
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, _, model) => FloatingActionButton(
onPressed: model.increment, // calling function of model to increment counter
),
),
),
);
}
}
//-----------------------------------ends