On app homepage I set up Model2 which make API call for data. User can then navigate to other page (Navigator.push). But I want make API call from Model2 when user press back (_onBackPress()) so can refresh data on homepage.
Issue is Model2 is not initialise for all user. But if I call final model2 = Provider.of<Model2>(context, listen: false); for user where Model2 is not initialise, this will give error.
How I can call Provider only on condition? For example: if(user == paid)
StatefulWidget in homepage:
#override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<Model1, Model2>(
initialBuilder: (_) => Model2(),
builder: (_, model1, model2) => model2
..string = model1.string,
),
child: Consumer<Model2>(
builder: (context, model2, _) =>
...
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(context: context)),
In Page2:
Future<void> _onBackPress(context) async {
// if(user == paid)
final model2 = Provider.of<Model2>(context, listen: false);
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return
// if(user == paid)
Provider.value(value: model2, child:
AlertDialog(
title: Text('Back'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Go back'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
// if(user == paid)
await model2.getData();
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Alternative method (maybe more easy): How to call provider on previous page (homepage) on Navigator.of(context).pop();?
TLDR: What is best solution for call API so can refresh data when user go back to previous page (but only for some user)?
You can wrap your second page interface builder in a WillPopScope widget, and then, pass whatever method you want to call to the onWillPop callback of the WillPopScope widget. This way, you can make your API call when user presses the back button. Find more about the WillPopScope widget on this WillPopScope Flutter dev documentation article.
tldr; Establish and check your single point of truth before the call to the Provider
that may result in a null value or evaluate as a nullable reference.
Perhaps you can change the architecture a bit to establish a single (nullable or bool) reference indicating whether the user has paid. Then use Darts nullability checks (or just a bool) to implement the behavior you want. This differs from your current proposal in that there would be no need to call on the Provider to instantiate the model. Just add a single point of truth to your User object that is initialized to null or false, and then change that logic only when the User has actually paid.
Toggling widgets/behavior in this way could be a solution.
Alternatives considered:
Packaging critical data points into a separate library so that the values can be imported where needed.
Other state management methods for key/value use.
If you want to simply hide/show parts of a page consider using the OffStage class or the Visibility class
Ref
Dart null-checking samples
Related
I'm pushing a new route like so in my app
Navigator.of(context)
.push<void>(
FilterTypesPage.routeFullScreen(context),
).then(
(value) {
log('PAGGGE POPPED');
},
),
static Route routeFullScreen(BuildContext context) {
return MaterialPageRoute<void>(
settings: const RouteSettings(name: routeName),
builder: (_) => BlocProvider.value(
value: BlocProvider.of<FeatureBloc>(context),
child: const FilterTypesPage(),
),
fullscreenDialog: true);
}
for some reason log('PAGGGE POPPED'); doesn't get called on page close
I'd like to trigger a bloc event or a function when I close this page
You should just call
Navigator.pop(context, someData);
from your RouteSettings where someData is the data you want to pass from the RouteSettings to the former page.
Then from your former page, you can perform your event handling inside the then block. The value inside the then block is the data that was passed from the RouteSettings page.
Alternatively, you can also use async-await instead of then in your former page.
onPressed: () async {
final someData = await Navigator.of(cotext).push(.....);
// Now perform your event handling which will be invoked after you pop `RouteSettings` page.
}
How can I change the visibility of a button on screen "X" from a button on screen "Y".
One popular approach (using the provider architecture) would be something like this:
Define a provider that handles all the logic and holds your data:
class MyProvider extends ChangeNotifier {
bool showMyButton = false;
MyProvider() {}
void showButton() {
showMyButton = true;
// This line notifies all consumers
notifyListeners();
}
void refresh() {
notifyListeners();
}
}
To access the provider everywhere you need to register it:
void main() => runApp(
// You can wrap multiple providers like this
MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (_) => MyProvider()),
],
child: const MyApp(),
),
);
On the button that you want to control you can use a Consumer to listen to the providers values:
Consumer<MyProvider>(builder: (_, model, __) {
return Visibility(
visible: model.showMyButton,
child: MaterialButton(...),
);
})
Now in your second screen you can access that provider with:
Provider.of<MyProvider>(context, listen: false)
.showButton();
However you might have to call notifyListener one more time when returning from screen Y to screen X:
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenY()));
Provider.of<MyProvider>(context, listen: false).refresh();
Keep in mind that there is a lot more to provider so please have a look at their official docs.
Also be aware of the fact that there are easier ways to just pass data between screens but you will often arrive at a point where you will need a better way of managing state and provider provides just that ;)
You can pass the data via push and pop of navigation. Or else use ChangeNotifier class to notify the state of button easily.
I am using riverpod ^1.0.0. I have created a StateClass which extends Equatable. In my StateNotifier i set state depending on events and outcomes. One being an async http request which upon success sets
state=SalesOrderListSuccess(salesOrderListItems: _items);
Upon http client failure however i set state to:
state = SalesOrderListError(error: response.data);
This works, upon success it renders the list in below UI builder. And it also using ref.listen and shows the snackbar. However, because the state changes from SalesOrderListSuccess and i am using ref.watch it seems that it cant keep the former known list and UI. How can i show the snackbar above the last known SalesOrderListSuccess/UI without rendering an entire new Error Page that is empty of all the items i have already managed to render in the list ?
Basically i dont want the list to change, just show a snackbar above last known list before the http client error happend.
Here the current widget: (this requires the SalesOrderListSuccess state in order to show the list).
#override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
final selectedtrack = ref.read(selectedProductIdProvider2.notifier);
ref.listen(todoListProvider, (previous, count) {
print(previous);
print(count);
switch (count.runtimeType) {
case SalesOrderListError:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ohh no some error happend')),
);
}
});
return ListView.builder(
shrinkWrap: true, // 1st add
physics: ClampingScrollPhysics(),
itemCount: (todos as SalesOrderListSuccess).salesOrderListItems.length,
itemBuilder: (context, index) {
final current=
(todos as SalesOrderListSuccess).salesOrderListItems[index];
return ListTile(
title: Text('${current.title}'),
onTap: () {
selectedtrack.state = index;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(),
),
);
});
},
);
}
Hi have a look at riverpod_messages.
I had your same problems and I have written a package for this
https://pub.dev/packages/riverpod_messages/versions/1.0.0
Let me know!
I am new to flutter and recently I decided to look into providers to be able to reuse some sqlite DB calls I have using FutureBuilder as suggested in this other question.
After reading about it I started using riverpod_hooks but I got stuck.
Currently I have a provider to get the data from sqlite:
final bloodPressureProvider = FutureProvider((_) => _findAllBloodPressures());
Future<List<BloodPressure>> _findAllBloodPressures() async {
var _bloodPressure = BloodPressure.forSearchOnly();
var result = await _bloodPressure.findAll();
return result;
}
Which I can successfully use to render a chart and display a list:
class BloodPressureList extends HookWidget {
#override
Widget build(BuildContext context) {
final bpList = useProvider(bloodPressureProvider);
return Scaffold(
drawer: NutriDrawer(context).drawer(),
appBar: AppBar(
title: Text('Blood Pressure History'),
),
body: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(0, 25, 15, 5),
child: bpList.when(
data: (bloodPressureList) => _buildLineChart(bloodPressureList),
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('Ooooopsss error'),
),
),
Expanded(
child: bpList.when(
data: (bloodPressureList) => _getSlidableListView(bloodPressureList, context),
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('Ooopsss error'),
),
),
],
), ...
The issue I currently have is to make the notification to the widgets whenever a new element is added to the DB which is done in another screen/widget, before using providers I would use setState but now I understand I should implement possibly a StateNotifier but I am missing the proper way to do it integrated with the asynchronous call returned from the DB used in the provider fragment above.
Any pointers in the right direction is greatly appreciated.
Update with solution: Based on the accepted answer below I used context.refresh and it did exactly what I was looking for:
var result = Navigator.push(context, MaterialPageRoute(builder: (context) {
return BloodPressureDetail(title, bloodPressure);
}));
result.then((_) => context.refresh(bloodPressureProvider));
So when navigating back from the edit/add screen the provider context is refreshed and the state of the chart and list get updated automatically.
By default FutureProvider cache the result of the future so it's only fetched the first time it's accessed.
If you want to get a new value each time you go to the screen use FutureProvider.autoDispose.
If you want to manually refresh the provider you can use context.refresh(bloodPressureProvider) with FutureProvider.
And if you don't want to access you db for fetching values but still want to have screens synchronized you can implement a stateNotifier which will represent your db state
final dbState =
StateNotifierProvider<dbState>((ProviderReference ref) {
final dbState = DbState();
dbState.init();
return dbState;
});
class dbState extends StateNotifier<AsyncValue<List<String>>> {
/// when the constructor is called the initial state is loading
dbState() : super(AsyncLoading<List<String>>());
/// fetch the values from the database and set the state to AsyncData
///(could be good to add a try catch and set the sate to an error if one
/// occurs)
void init()async{
state = AsyncLoading<List<String>>();
final fetchedValues = await fetchValues();
state = AsyncData<List<String>>(fetchedValues);
}
}
I'm using Provider with ChangeNotifier to alert a Consumer once a new photo is uploaded to a server replacing an old photo. The problem is that the URL stays the same as the photo is merely overwritten and keeps the same name. Hence the Consumer doesn't recognize that anything has changed and doesn't refresh the old photo with the new one.
How can I trick the ChangeNotifier into refreshing the URL? Heres' the Consumer in my build;
Consumer<SocialProvider>(
builder: (context, socialProvider, child) {
return Image.network(socialProvider.currentavatar,
);
}),
Here's where the image is chosen in the Gallery and uploaded to overwrite the old image on the server.
GestureDetector(
onTap: () async {
await socialProvider.loadCurrentUserId();
await _listener.openGallery(socialProvider.currentuserid);
String updatedavatar = "http://example.com/same_photo.jpg";
socialProvider.updateAvatar(updatedavatar);
},
And here's the code in the Provider with ChangeNotifier;
Future<void> updateAvatar(String avatar) async {
var box = await Hive.openBox('currentuser');
box.put('currentavatar', avatar);
currentavatar = avatar;
notifyListeners();
}
Any ideas how to trick Consumer into believing the url has changed so that it is refreshed?
I believe the Consumer will rebuild when someone call notifyListeners(). Your issue may be in Image.network(socialProvider.currentavatar,) that flutter reuse same render object when everything is not change. You can try to add key:UniqueLey() to force rebuild the widget every time it build.
Update with some assumption.
Here is the simple code I try to rebuild your environment:
class SimpleProvider with ChangeNotifier {
Future<void> reload() async => notifyListeners();
}
class TestConsumer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => SimpleProvider(),
child: Consumer<SimpleProvider>(
builder: (_, simpleProvider, child) {
print('Consumer build');
return child; // Here is your Image?
},
child: Builder(
builder: (context) {
print('Builder build');
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<SimpleProvider>().reload();
},
),
body: Container(),
);
},
),
),
),
);
}
}
Every time I click the button, SimpleProvider will call the notifyListeners()
(Future is not necessary) and the builder function in Consumer will be called. Here come the questions:
Is the builder really called when you use notifyListeners?
I assume the builder function is called. You can still double check this part. Otherwise you must provide how you call it.
If builder is called, why the Image Widget inside it is not rebuilt?
It is part of flutter design that it reuse as much as it could, for better performance. Sometimes it confuse people that the rebuild is not happen. That's where I guess your problem is here. You can check the basic mechanism from video here:
https://www.youtube.com/watch?v=996ZgFRENMs&ab_channel=Flutter