Flutter - How to architect multiple nested BLoC? - dart

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.

Related

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

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

How do you make local storage persist after the app has been closed in Flutter?

I'm using the package localstorage - https://pub.dartlang.org/packages/localstorage#-installing-tab-
This is my code:
import 'package:flutter/material.dart';
import 'package:localstorage/localstorage.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => _MyHomePageState();
}
final LocalStorage storage = new LocalStorage("level");
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
//storage.setItem("level", 0);
printStorage();
}
void printStorage() {
print("level stored: " + storage.getItem("level").toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("LocalStorage Example"),
),
body: Center(
),
);
}
}
When storage.setItem("level", 0) is not commented out the app works fine and prints out "level stored: 0". However after closing the app, commenting out storage.setItem("level", 0), saving the app and rerunning the app the app prints out "level stored: null".
How do you make the storage persist from the last time the app was run?
I am using the Xcode iPhone simulator.
Any help is greatly appreciated!
By looking at the package’s source code it looks like you’re not giving it enough time to load the data from the underlying async call. That’s why it exposes a flag property for you to check if it’s ready to perform read/write operations.
Just use it before printing as so:
storage.ready.then((_) => printStorage());
I had similar questions, but after hours of testing and digging around, I can confirm that the localstorage package does persist data even after the app has been closed. If you want to see the actual database file for testing purposes (you can see the contents change live during iOS simulation) look here on macOS:
/Users/YOURUSERNAME/Library/Developer/CoreSimulator/Devices/SOMEDEVICENUMBER/data/Containers/Data/Application/SOMEAPPLICATIONNUMBER/Documents/level.json
So this means it just becomes a matter of waiting the few nanoseconds for the localstorage connection to be ready before trying to read and use the values. Being new to Dart/Flutter, for me this means experimenting with the correct combination and location of async/await, or Future/then.
For example, this change will work correctly in your sample app:
void printStorage() async {
//this will still print null:
print("before ready: " + testDb.getItem("level").toString());
//wait until ready
await storage.ready;
//this will now print 0
print("after ready: " + testDb.getItem("level").toString());
}
In my actual app, if I forget to correctly place an "await" or "then", I get a lot of null errors before the actual value gets retrieved/assigned.
BTW in your file (level.json) you have only one element, {“level”, “0”}. For this scenario with simple data needs, using the shared_preferences package is an alternative consideration. But I like localstorage because it is clean and simple.

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

How do I insert widgets at the top of a ListView?

Brief Note:
In all of my code examples you will see things like material.Widget instead of just Widget. This is because I like to name my imports like this for example:
import 'package:flutter/material.dart' as material;
My Question:
I am trying to make a ListView show newly added Widgets at the top of the list. I have a simple stateful widget that only contains a list of models (classes that contain necessary information to construct the widgets that are going into the ListView), and should construct ListView's children based off of that list.
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
The behavior that I expect from this is that whenever the build method is called (and setState is properly used), that the ListView should be reconstructed properly and contain child widgets in the order of the list. It successfully does this if I simply add new models to cardModels sequentially:
cardModels.add(new CardModel(nextID++));
You can see widgets get added sequentially with their id's incrementing properly:
However, I want newer widgets to be inserted at the top (which would be shown with higher ids at the top). In order to accomplish this, I try inserting new models at the beginning of the list:
cardModels.insert(0, new CardModel(nextID++));
Unfortunately, instead of seeing the correct widgets, I just get widget with id 0 over and over again:
I know that the list of models is being updated correctly because I can print it out and see the ids in descending order. I am assuming that there is something about how flutter detects changes to widgets that is causing this behavior, but after a lot of reading, I still have not been able to figure it out. Any help would be much appreciated. Also, I call set state in the widget that ends up building the page (the widget containing the ListView) as one of its children:
btn = new FloatingActionButton(
onPressed: () => setState(_pageController.addCard),
tooltip: 'Upload File',
child: new Icon(Icons.file_upload),
);
_pageController is the object that modifies the list of models. If this is not enough information, let me know and I am happy to provide more code or answer any questions.
There is a simple quick fix, using the didWidgetUpdate override,
by checking if the cardModels object in the oldWidget is the same in the cardModels being passed as a parameter.
like so
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
Full example below
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
#override
void initState(){
cardModels = widget.cardModels;
super.initState();
}
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
Turns out, the Card widget I made did not need to be stateful. I changed it to stateless and it solved the problem. No idea why having them as stateful would break it though.
On FloatingActionButton change the function to insert new item and then invoke setState like:
cardModels.insert(0, new CardModel(nextId++)) ;
setState(() {
cardModels = cardModels;
});
Hope that helped!

Resources