flutter riverpod question: watch a provider from another provider and trigger action on the first provider - riverpod

I am trying to figure out how can i watch a StateNotifierProvider and trigger some methods (defined in the class subclassing StateNotifier) on this provider after having done some async computations in another Provider watching the StateNotifierProvider.
Loking at the example below
i need to perform a reset from the RandomAdderNotifierobject provided by the randomAdderProvider if the doneProvider return true.
I try to reset from the doReset Provider. However the provider has nothing to provide.
The point is that both the doneProvider and the doreset provider are not rebuild on state changes of AdderProvider.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:equatable/equatable.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final randomProvider = Provider<Random>((ref) {
return Random(1234);
});
//immutable state
class RandomAdder extends Equatable {
final int sum;
const RandomAdder(this.sum);
#override
List<Object> get props => [sum];
}
//State notifier extension
class RandomAdderNotifier extends StateNotifier<RandomAdder> {
RandomAdderNotifier(this.ref) : super(const RandomAdder(0));
final Ref ref;
void randomIncrement() {
state = RandomAdder(state.sum + ref.read(randomProvider).nextInt(5));
}
void reset() {
state = RandomAdder(0);
}
}
/// Providers are declared globally and specify how to create a state
final randomAdderProvider =
StateNotifierProvider<RandomAdderNotifier, RandomAdder>(
(ref) {
return RandomAdderNotifier(ref);
},
);
Future<bool> delayedRandomDecision(ref) async {
int delay = ref.read(randomProvider).nextInt(5);
await Future.delayed(Duration(seconds: delay));
print("You waited $delay seconds for a decision.");
return delay > 4;
}
final doneProvider = FutureProvider<bool>(
(ref) async {
ref.watch(randomAdderProvider);
bool decision = await delayedRandomDecision(ref);
print("the decision is $decision");
return decision;
},
);
final doreset = Provider((ref) {
if (ref.watch(doneProvider).value!) {
ref.read(randomAdderProvider.notifier).reset();
}
});
class Home extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
// Consumer is a widget that allows you reading providers.
child: Consumer(builder: (context, ref, _) {
final count = ref.watch(randomAdderProvider);
return Text('$count');
}),
),
floatingActionButton: FloatingActionButton(
// The read method is a utility to read a provider without listening to it
onPressed: () =>
ref.read(randomAdderProvider.notifier).randomIncrement(),
child: const Icon(Icons.add),
),
);
}
}

I think ref.listen is more appropriate for usage within the doreset function than ref.watch.
Similarly to ref.watch, it is possible to use ref.listen to observe a provider.
The main difference between them is that, rather than rebuilding the widget/provider if the listened to provider changes, using ref.listen will instead call a custom function.
As per the Riverpod documentation
For ref.listen we need an additional argument - the callback function that we wish to execute on state changes - Source
The ref.listen method needs 2 positional arguments, the first one is the Provider and the second one is the callback function that we want to execute when the state changes.
The callback function when called will be passed 2 values, the value of the previous State and the value of the new State.
&
We will need to handle an AsyncValue - Source
As you can see, listening to a FutureProvider inside a widget returns an AsyncValue – which allows handling the error/loading states.
In Practice
doreset function
I chose to handle the AsyncValue by only handling the data case with state.whenData()
final doReset = Provider<void>(
(ref) {
final done = ref.listen<AsyncValue<bool>>(doneProvider, (previousState, state) {
state.whenData((value) {
if (value) {ref.read(randomAdderProvider.notifier).reset();}
});
});
},
);
Don't forget to watch either doReset/doneProvider in your Home Widget's build method. Without that neither will kick off (Don't have an explanation for this behaviour)
class Home extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(doReset);
...
Lastly, your random function will never meet the condition for true that you have setup as delay>4, as the max possible delay is 4. Try instead using delay>3 or delay=4.
Also perhaps disable the button to prevent clicks while awaiting updates

and in a case where you are using ChangeNotifier You can pass ref in you provider and use the ref same as we can use in ConsumerWidget.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class YourProvider extends ChangeNotifier {
Ref ref;
YourProvider(this.ref) : super();
callOtherProviderFromThisProvider() {
ref.read(otherProvider).someMethodINeedToTrigger();
}
}
final yourProvider = ChangeNotifierProvider<YourProvider>(
(ref) => YourProvider(ref));

Related

Hydrated Bloc not persisting

I'm trying to get my state to persist using hydrated bloc but it is not working. When i restart the app the state is not persisting
This is the code i have to start the app:
void bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
HydratedBlocOverrides.runZoned(
() => runApp(
RepositoryProvider<void>(
create: (context) => DatabaseCubit(),
child: const RunApp(),
),
),
storage: storage,
);
}
this is the relevent code in the cubit:
class DatabaseCubit extends HydratedCubit<DatabaseState>{
DatabaseCubit() : super(databaseInitial);
#override
DatabaseState? fromJson(Map<String, dynamic> json) {
return DatabaseState.fromMap(json);
}
#override
Map<String, dynamic> toJson(DatabaseState state) {
return state.toMap();
}
I have set up unit tests that make sure my toMap and fromMap functions are working. The tests are passing, here is the code for them:
test('Database state should be converted to and from json', () {
final databaseStateAsJson = databaseState.toMap();
final databaseStateBackToNormal =
DatabaseState.fromMap(databaseStateAsJson);
expect(databaseStateBackToNormal, databaseState);
});
Please tell me what i am doing wrong
I've just solved a similar problem.
Try digging a little into hydrated_bloc.dart hydrate() method and you might find out why it refuses to load your state :).
Could be type conversion problem between collections - as it was in my case.

Correct Flutter widget sequence to pull data on app load

I am running into an issue with flutter when I try to read data from local storage when the app loads.
I have an inherited widget that holds authentication information for the current user. When the app loads I want to look into local storage for session tokens. If the session tokens exist I would like to update the inherited widget with this information.
My screens are dynamic. If it knows the user is authenticated it takes them to the requested screen, otherwise it takes them to the register screen.
The issue I am running into is that I cannot update the inherited widget's state from an initState() method from a widget that depends on the inherited widget (My router widget)
How can I read from local storage when the app loads and update the inherited widget?
Error when running app:
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building _InheritedAuthContainer:
flutter: inheritFromWidgetOfExactType(_InheritedAuthContainer) or inheritFromElement() was called before
flutter: RootState.initState() completed.
flutter: When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
flutter: widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
flutter: or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
flutter: inherited widget.
flutter: Typically references to inherited widgets should occur in widget build() methods. Alternatively,
flutter: initialization based on inherited widgets can be placed in the didChangeDependencies method, which
flutter: is called after initState and whenever the dependencies change thereafter.
Router Widget (Root)
class Root extends StatefulWidget {
#override
State createState() => RootState();
}
class RootState extends State<Root> {
static Map<String, Widget> routeTable = {Constants.HOME: Home()};
bool loaded = false;
bool authenticated = false;
#override
void initState() {
super.initState();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: (routeSettings) {
WidgetBuilder screen;
if (loaded) {
if (authenticated) {
screen = (context) => SafeArea(
child: Material(
type: MaterialType.transparency,
child: routeTable[routeSettings.name]));
} else {
screen = (conext) => SafeArea(
child: Material(
type: MaterialType.transparency, child: Register()));
}
} else {
screen = (context) => new Container();
}
return new MaterialPageRoute(
builder: screen,
settings: routeSettings,
);
});
}
}
Inherited Widget method that checks for auth and updates itself which triggers a rerender of my router widget
Future<bool> isAuthenticated() async {
if (user == null) {
final storage = new FlutterSecureStorage();
List results = await Future.wait([
storage.read(key: 'idToken'),
storage.read(key: 'accessToken'),
storage.read(key: 'refreshToken'),
storage.read(key: 'firstName'),
storage.read(key: 'lastName'),
storage.read(key: 'email')
]);
if (results != null && results[0] != null && results[1] != null && results[2] != null) {
//triggers a set state on this widget
updateUserInfo(
identityToken: results[0],
accessToken: results[1],
refreshToken: results[2],
firstName: results[3],
lastName: results[4],
email: results[5]
);
}
}
return user != null && (JWT.isActive(user.identityToken) || JWT.isActive(user.refreshToken));
}
Main
void main() => runApp(
EnvironmentContainer(
baseUrl: DEV_API_BASE_URL,
child: AuthContainer(
child: Root()
)
)
);
What is a correct way of checking local storage on app load and updating the inherited widget that holds this information?
Actually you cannot access InheritedWidget from an initState method. Instead try accessing it from didChangeDependencies.
Example:
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
Another way would be to schedule the data fetch in initState with SchedulerBinding. You can find the docs here
SchedulerBinding.instance.addPostFrameCallback((_) {
// your login goes here
});
Note: remember the didChangeDependencies will be called whenever the state or dependencies of any parent InheritedWidget changes. Please look at the docs here.
Hope this helps!
While the answer by #hemanth-raj is correct, I would actually advocate a slightly different way of doing this. Instead of constructing the AuthContainer with no data, you could actually do the user session loading before you construct your widgets and pass the data in directly. This example uses the scoped_model plugin to abstract away the inherited widget boilerplate (which I highly recommend over writing inherited widgets manually!) but is otherwise pretty similar to what you've done.
Future startUp() async {
UserModel userModel = await loadUser();
runApp(
ScopedModel<UserModel>(
model: userModel,
child: ....
),
);
}
void main() {
startup();
}
This is more or less what I do in my app and I haven't had any problems with it (although you'd probably want to put in some error handling if there's any chance of loadUser failing)!
This should made your userState code much cleaner =).
And an FYI, what I've done in my UserModel is have a bool get loggedIn => ... that knows which information needs to be in there to tell whether the user is logged in or not. That way I don't need to track it separately but I still get a nice simple way to tell from outside the model.
Do like this as example given in this :
void addPostFrameCallback(FrameCallback callback) {
// Login logic code
}
Schedule a callback for the end of this frame.
Does not request a new frame.
This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
The callbacks are executed in the order in which they have been added.
Post-frame callbacks cannot be unregistered. They are called exactly once.

How to perserve toggle switch state when changing tabs for flutter?

I'm building three pages of alarms (stored in different lists) that I have displayed via a Scaffold and TabViewer. Each alarm is stored as a row with a toggle switch to enable it. The rows for each page are stored as a List. Despite making sure to use set state when changing values and even trying to assign unique keys nothing I do seems to preserve the state of the switches when I changed tabs.
This is my first time coding in Flutter/Dart or designing an app for mobile in general. As such I'm still learning about some basic features of this language.
I've tried adding keys to everything using Uniquekey() to generate up keys didn't work so I've removed them.
I've made sure all variable changes are inside set state functions.
I've tried to store the variable inside the immutable super class of AlarmToggle which is both ill-advised and doesn't work anyways.
I haven't tired using PageStorageKey as I'm not sure how they'd be implemented in my code but I feel this is likely the only solution.
class Alarms {
List<Widget> allAlarms = []; // Store all alarms for the object
buildAlarm(
GlobalKey<ScaffoldState>
pageKey,
[int hour,
int minute,
List<bool> alarmDaysOfWeek]) {
TimeOfDay alarmTime = TimeOfDay(hour: hour, minute: minute);
AlarmRow _newAlarm = new AlarmRow(UniqueKey(), alarmTime, alarmDaysOfWeek);
allAlarms.add(_newAlarm);
}
void removeAlarm(GlobalKey<ScaffoldState> pageKey) {allAlarms.removeLast();}}
class AlarmRow extends StatefulWidget {
final TimeOfDay _alarmTime;
final List<bool> _alarmDaysofWeek;
final UniqueKey key;
AlarmRow(this.key, this._alarmTime, this._alarmDaysofWeek);
AlarmRowState createState() => new AlarmRowState();
}
class AlarmRowState extends State<AlarmRow> {
bool _alarmIsActive;
AlarmRowState(){_alarmIsActive = _alarmIsActive ?? false;}
void toggleChanged(bool state) {this.setState(() {_alarmIsActive = state;});}
#override
Widget build(BuildContext context) {
return new Container(
child: Row(
children: <Widget>[
new AlarmIcon(_alarmIsActive),
new Column(
children: <Widget>[
new AlarmTime(widget._alarmTime),
new AlarmWeekly(widget._alarmDaysofWeek),
],
),
new AlarmToggle(
_alarmIsActive,
() => toggleChanged(!_alarmIsActive),
),
],
),
);
} // Build
} // Class
No matter what I seem to try the _alarmIsActive variable in AlarmRow() gets reset to null each time the tab is changed. I'm trying to preserve its state when changing pages.
The solution is as jdv stated to use AutomaticKeepAliveClientMixin, it's not hard to use but I figured I'd include the instruction that I found after searching here in case anyone searches and finds this and doesn't know automatically how to implement it like myself.
class AlarmRowState extends State<AlarmRow> with AutomaticKeepAliveClientMixin {
#override bool get wantKeepAlive => true;
It's implemented in the state class with any modifiable variables you want to preserve. The 'with' after the 'extends' adds a Mixin which is a sort of class inheritance. Finally, it requires you to set the 'wantKeepAlive' to true and it compiles and state is no longer lost while the widget is not being rendered.
Why a stateful widget loses state while it's not rendered is something I'm still searching for. But at least I have a solution.
I hope this will help you #Ender ! check for this code, you can create a Global shared preference and use it, as shown below:-
class AlarmRow extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _AlarmRowState();
}
class _AlarmRowState extends State<AlarmRow>{
bool alarmIsActive;
#override
void initState() {
alarmIsActive = Global.shared.alarmIsActive;
super.initState();
}
#override
Widget build(BuildContext context) {
....
.....
....
....
body: new Container(
child: Switch(
value: alarmIsActive,
onChanged: (bool isEnabled) {
setState(() {
alarmIsActive = isEnabled;
Global.shared.alarmIsActive = isEnabled;
isEnabled =!isEnabled;
});
},
.....
......
),
),
);
}
}
class Global{
static final shared =Global();
bool alarmIsActive = false;
}
Switch enabled and will maintain its state

flutter bloc pattern navigation back results in bad state

I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example

Flutter - How to architect multiple nested BLoC?

Suppose there is a top-level BLoC called PreferenceBloc, and inside that BLoC is another BLoC called PageBloc. If the logic inside PageBloc requires on a value stream from PreferenceBloc (i.e., create a new page needs to know page configuration now), how should I architect this?
Code sample:
class PreferencesBloc{
final preferencesService=PreferencesService();
// Output interfaces of Bloc
ValueObservable<String> get mainDir => _mainDir.distinct().shareValue(seedValue: '/');
final _mainDir = BehaviorSubject<String>(seedValue: '/');
// Input interfaces of Bloc...
// .........
}
class PageBloc{
final List<PageInfo> _pageInfos=<PageInfo>[];
// Output interfaces of Bloc...
// .........
// Input interfaces of Bloc...
Sink<int> get pageCreation => _pageCreationController.sink;
final _pageCreationController = StreamController<int>();
pageBloc(){
_pageCreationController.stream.listen(_handleCreation);
}
void _handleCreation(int pos){
_pageInfo.insert(pos, PageInfo('I need the mainDir here!')); //Need info from PreferencesBloc!!!
}
}
class PreferencesProvider extends InheritedWidget{
final PreferencesBloc preferencesBloc;
//...
}
class PageProvider extends InheritedWidget{
final PageBloc pageBloc;
//...
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return PreferencesProvider(
child: PageProvider(
child: MaterialApp(
home: Scaffold(
body: Text("test"),
),
),
),
);
}
}
Edit: To sum up, it is convenient to communicate between Bloc and widget in flutter, but is there a good way to communicate between Bloc and Bloc?
This question was asked at Nov 18. Currently, there is already a great bloc library that support nested bloc. You can use https://pub.dartlang.org/packages/flutter_bloc in the official pub dart. So far, i have been using it for developing a quite complex app, and it is great.

Resources