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
Related
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));
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.
I'm pretty new to Flutter and experimenting with the SDK. I'm working on a simple app that has a countdown in the background and want to trigger an event at certain intervals. For example, when clock reaches one minute remaining, send a push notification. In general, I'm trying to get a feel for how to monitor certain activities such as time and usage of the app and once certain conditions are met, trigger other things. Is it as simple as an if-else statement placed in the right place?
What kind of thing am I looking for to implement this?
Thanks in advance.
I prefer to use streams for such tasks
Stream<int> timer = Stream.periodic(Duration(seconds: 1), (int count) => count);
...
_MyTextWidget(timer)
and my widget
class _MyTextWidget extends StatefulWidget {
_MyTextWidget(this.stream);
final Stream<int> stream;
#override
State<StatefulWidget> createState() => _MyTextWidgetState();
}
class _MyTextWidgetState extends State<_MyTextWidget> {
int secondsToDisplay = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: widget.stream,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return snapshot.hasData ? Text(snapshot.data.toString()) : Text('nodata');
});
}
}
The following is a better example of an event listener based on Streams that I have found to work.
In the widget you want to listen to..
class CmVideoPlayer extends StatefulWidget {
String titlePlaying;
StreamController changeController = StreamController<VideoPlayerEvent>();
CmVideoPlayer({Key key, #required this.titlePlaying})
: assert(titlePlaying != null), super(key: key);
#override
_CmVideoPlayerState createState() => _CmVideoPlayerState();
}
See the line "StreamController changeController = StreamController();" that uses a small class VideoPlayerEvent to carry the message.
class VideoPlayerEvent {
var eventType;
var eventMessage;
VideoPlayerEvent(this.eventType, this.eventMessage);
}
Then in the STATEFULLWIDGET...
Refer the the stream as
class _CmVideoPlayerState extends State<CmVideoPlayer> {
void Member() {
widget.changeController.add(new VideoPlayerEvent('state', 'finished'));
}
}
As it is inside the _CmVideoPlayerState class, and using the ability to reach into the parent class via the widget variable.
Then in the area of the code using the widget, and to listen for the messages..
To listen for the messages
CmVideoPlayer myPlayer = CmVideoPlayer();
myPlayer.changeController.stream.listen((e) {
print('Reciever event from CmVideoPlayer: ' + e.eventMessage.toString());
}
That should do it.
HOWEVER, this only allows ONE listener at a time. After I got this going, I moved on. But plan to implement a multi listener down the track.
Maybe some one can expand on this. I am keeping it as simple as possible. If some one has a multi listener example. Please post here.
You can use ValueNotifier for this.
When value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
Helpful Medium link
I'm new to Flutter and I'm trying to accomplish a simple thing:
I want to create a signup functionality using BLoC pattern and streams.
For the UI part I have a stepper, that on the very last step should fire a request to the server with the collected data.
I believe I have everything working until the StreamBuilder part. StreamBuilders are meant to return Widgets, however, in my case I don't need any widgets returned, if it's a success I want to navigate to the next screen, otherwise an error will be displayed in ModalBottomSheet.
StreamBuilder is complaining that no widget is returned.
Is there anything else that could be used on the View side to act on the events from the stream?
Or is there a better approach to the problem?
If you don't need to render anything, don't use StreamBuilder to begin with.
StreamBuilder is a helper widget used to display the content of a Stream.
What you want is different. Therefore you can simply listen to the Stream manually.
The following will do:
class Foo<T> extends StatefulWidget {
Stream<T> stream;
Foo({this.stream});
#override
_FooState createState() => _FooState<T>();
}
class _FooState<T> extends State<Foo<T>> {
StreamSubscription streamSubscription;
#override
void initState() {
streamSubscription = widget.stream.listen(onNewValue);
super.initState();
}
void onNewValue(T event) {
Navigator.of(context).pushNamed("my/new/route");
}
#override
void dispose() {
streamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
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!