Build function in StatelessWidget keeps refiring - flutter

Consider the following StatelessWidget:
class SwitchScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final testService = Provider.of<TestService>(context); // Line 1
Future( () {
Navigator.of(context).push( // Segment 2
MaterialPageRoute(builder: (context) => TestScreen()) // Segment 2
); // Segment 2
});
return Scaffold(body: Center( child: Text("lol") ) );
}
}
The widget is directly below the root in the widget tree and wrapped by a ChangeNotifierProvider:
void main() => runApp(new Main());
class Main extends StatefulWidget {
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SampleProgram',
home: ChangeNotifierProvider<TestService>(
builder: (_) { return TestService(); } ,
child: SwitchScreen(),
),
);
}
}
The service associated with the provider, TestService, is currently empty. TestScreen is simply another StatelessWidget which includes AppBar wrapped inside a Scaffold.
I would expect the program to finish rendering the SwitchScreen, navigate to TestScreen to fulfill the future, and finally render the AppBar inside TestScreen. However, every time it enters the TestScreen view, something appears to trigger a rebuild of SwitchScreen. The app then bounces back to SwitchScreen, moves to TestScreen to fulfill the future, and repeats this process. By using debug Print statements I'm sure that the build method of SwitchScreen is called immediately after TestScreen finishes rendering.
The interesting thing is that if I comment out Line 1, the build method won't be re-triggered. Similarly if I replace the entirety of Segment 2 with anything else, say a print statement, the build method won't keep firing either. I suspected that Navigator is resulting in some value change in TestService, forcing SwitchScreen to rebuild, so I overrode the notifyListeners method in TestService since this method is the only way SwitchScreen can be affected by TestService.
class TestService with ChangeNotifier {
#override
void notifyListeners() {
print("Triggering SwitchScreen's build method");
}
}
But no string is printed out. Right now I'm very curious about what's causing the rebuilding and what roles do the Provider and the Navigator play in this. Any help would be really appreciated.

Instead of calling
final testService = Provider.of<TestService>(context);
Use
final testService = Provider.of<TestService>(context, listen: false);
Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called.

Related

Im trying to create a splash screen on flutter. Its showing error like these

lib/Splash.dart:36:28: Error: Type 'DiagnosticPropertiesBuilder' not found.
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
^^^^^^^^^^^^^^^^^^^^^^^^^^^
lib/Splash.dart:36:28: Error: 'DiagnosticPropertiesBuilder' isn't a type.
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
^^^^^^^^^^^^^^^^^^^^^^^^^^^
lib/Splash.dart:38:20: Error: The method 'DiagnosticsProperty' isn't defined for the class '_SplashState'.
'_SplashState' is from 'package:g1/Splash.dart' ('lib/Splash.dart').
Try correcting the name to the name of an existing method, or defining a method named 'DiagnosticsProperty'.
properties.add(DiagnosticsProperty('initState', initState));
^^^^^^^^^^^^^^^^^^^
Try this packages flutter_native_splash , animated_splash_screen or splashscreen
Flutter actually gives a simpler way to add Splash Screen to our application. We first need to design a basic page as we design other app screens. You need to make it a StatefulWidget since the state of this will change in a few seconds.
import 'dart:async';
import 'package:flutter/material.dart';
import 'home.dart';
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
Timer(
Duration(seconds: 3),
() => Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (BuildContext context) => HomeScreen())));
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Image.asset('assets/splash.png'),
),
);
}
}
Logic Inside the initState(), call a Timer() with the duration, as you wish, I made it 3 seconds, once done push the navigator to Home Screen of our application.
Note: The application should show the splash screen only once, the user should not go back to it again on back button press. For this, we use Navigator.pushReplacement(), It will move to a new screen and remove the previous screen from the navigation history stack.

what does it mean when we see people calling widget in dart?

I have seen many times people calling widget. sth inside the code.
May I know what it is actually doing?
For example code below, (highlighted part is my confusion)
class _MyOwnClassState extends State<MyOwnClass> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: makeWidgetChildren(**widget.jsonObject)**),
),
);
}
}
In flutter's StatefulWidget, we have the following architecture.
You have a StatefulWidget like this,
class MyOwnClass extends StatefulWidget {
State createState () => _MyOwnClassState();
}
And you have a State class for your StatefulWidget like this,
class _MyOwnClassState extends State<MyOwnClass> {
}
Now, State class is meant to house variables that tend to change in order for your UI to be rebuilt.
So you can have variables in your State that you can update using setState.
But what if you had some data that doesn't change and you want to avoid putting them inside the State class.
That's where your StatefulWidget comes to play.
You can store variables in your MyOwnClass and the widget variable inside the State class gives you a way to access them.
For example,
class MyOwnClass extends StatefulWidget {
int numberThatDoesntChange = 1;
State createState () => _MyOwnClassState();
}
You can access them in your State class like this,
class _MyOwnClassState extends State<MyOwnClass> {
Widget build(BuildContext context) {
return Text('$widget.numberThatDoesntChange');
}
}
Apart from this, your StatefulWidget has many more internal instance members that you can access inside of your State class using the widget variable.
The widget refers to the actual view that renders on the screen. It extends the StatefulWidget class of the flutter framework and overrides the createState() method. The createState() method is used to create the instance of state class. We will look into createState().
The state class is used to maintain the state of the widget so that it can be rebuilt again. It extends the State class of the flutter framework and overrides the build method.
The framework calls build() method again and again whenever setState() method is called. The setState() method notifies the framework that the internal state of this object has changed and it should be rebuilt. Suppose we change the value of text in StatefulWidget then we need to call setState().
Edit As Nisanth pointed outh in his comment - I missed your question completely; please ignore the below....
Let me try my answer, I don't think others are getting your point.
In your exapmle, Column(children: x) expect a list of Widgets.
You have two options - either provide this list directly:
class _MyOwnClassState extends State<MyOwnClass> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: <Widget>[SomeWidget()]),
),
);
}
}
Or if you have more complex code that generates widget - based on input parameters, or you have the same widget generated multiple times and you want to avoid the code duplication - you would create the separate function to do the job.
Something like:
class _MyOwnClassState extends State<MyOwnClass> {
List<Widget> makeWidgetChildren(int param) {
/*
some very complex logic here
/*
if (param>3 && param<4) {
return List<Widget>.generate(4, (index)=>SomeWidget1(index));
} else {
return <Widget>[Center()];
}
}
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: makeWidgetChildren(**widget.jsonObject)**),
),
);
}
}
So basically, it is just to make the code nicer; and to avoid having code repeated over and over again in the build function.

How to reload the page whenever the page is on screen - flutter

Is there any callbacks available in flutter for every time the page is visible on screen? in ios there are some delegate methods like viewWillAppear, viewDidAppear, viewDidload.
I would like to call a API call whenever the particular page is on-screen.
Note: I am not asking the app states like foreground, backround, pause, resume.
Thank You!
Specifically to your question:
Use initState but note that you cannot use async call in initState because it calls before initializing the widget as the name means. If you want to do something after UI is created didChangeDependencies is great. But never use build() without using FutureBuilder or StreamBuilder
Simple example to demostrate:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(home: ExampleScreen()));
}
class ExampleScreen extends StatefulWidget {
ExampleScreen({Key key}) : super(key: key);
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
List data = [];
bool isLoading = true;
void fetchData() async {
final res = await http.get("https://jsonplaceholder.typicode.com/users");
data = json.decode(res.body);
setState(() => isLoading = false);
}
// this method invokes only when new route push to navigator
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: isLoading
? CircularProgressIndicator()
: Text(data?.toString() ?? ""),
),
);
}
}
Some lifecycle method of StatefulWidget's State class:
initState():
Describes the part of the user interface represented by this widget.
The framework calls this method in a number of different situations:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
The framework replaces the subtree below this widget with the widget
returned by this method, either by updating the existing subtree or by
removing the subtree and inflating a new subtree, depending on whether
the widget returned by this method can update the root of the existing
subtree, as determined by calling Widget.canUpdate.
Read more
didChangeDependencies():
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an
InheritedWidget that later changed, the framework would call this
method to notify this object about the change.
This method is also called immediately after initState. It is safe to
call BuildContext.dependOnInheritedWidgetOfExactType from this method.
Read more
build() (Stateless Widget)
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the
tree in a given BuildContext and when the dependencies of this widget
change (e.g., an InheritedWidget referenced by this widget changes).
Read more
didUpdateWidget(Widget oldWidget):
Called whenever the widget configuration changes.
If the parent widget rebuilds and request that this location in the
tree update to display a new widget with the same runtimeType and
Widget.key, the framework will update the widget property of this
State object to refer to the new widget and then call this method with
the previous widget as an argument.
Read more
Some widgets are stateless and some are stateful. If it's a stateless widget, then only values can change but UI changes won't render.
Same way for the stateful widget, it will change for both as value as well as UI.
Now, will look into methods.
initState(): This is the first method called when the widget is created but after constructor call.
#override
void initState() {
// TODO: implement initState
super.initState();
}
didChangeDependecies() - Called when a dependency of this State object changes.Gets called immediately after initState method.
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
didUpdateWidget() - It gets called whenever widget configurations gets changed. Framework always calls build after didUpdateWidget
#override
void didUpdateWidget (
covariant Scaffold oldWidget
)
setState() - Whenever internal state of State object wants to change, need to call it inside setState method.
setState(() {});
dispose() - Called when this object is removed from the tree permanently.
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
You don't need StatefulWidget for calling the api everytime the screen is shown.
In the following example code, press the floating action button to navigate to api calling screen, go back using back arrow, press the floating action button again to navigate to api page.
Everytime you visit this page api will be called automatically.
import 'dart:async';
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ApiCaller())),
),
);
}
}
class ApiCaller extends StatelessWidget {
static int counter = 0;
Future<String> apiCallLogic() async {
print("Api Called ${++counter} time(s)");
await Future.delayed(Duration(seconds: 2));
return Future.value("Hello World");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Api Call Count: $counter'),
),
body: FutureBuilder(
future: apiCallLogic(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator();
if (snapshot.hasData)
return Text('${snapshot.data}');
else
return const Text('Some error happened');
},
),
);
}
}
This is the simple code with zero boiler-plate.
The simplest way is to use need_resume
1.Add this to your package's pubspec.yaml file:
dependencies:
need_resume: ^1.0.4
2.create your state class for the stateful widget using type ResumableState instead of State
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends ResumableState<HomeScreen> {
#override
void onReady() {
// Implement your code inside here
print('HomeScreen is ready!');
}
#override
void onResume() {
// Implement your code inside here
print('HomeScreen is resumed!');
}
#override
void onPause() {
// Implement your code inside here
print('HomeScreen is paused!');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Go to Another Screen'),
onPressed: () {
print("hi");
},
),
),
);
}
}
If you want to make an API call, then you must be (or really should be) using a StatefulWidget.
Walk through it, let's say your stateful widget receives some id that it needs to make an API call.
Every time your widget receives a new id (including the first time) then you need to make a new API call with that id.
So use didUpdateWidget to check to see if the id changed and, if it did (like it does when the widget appears because the old id will be null) then make a new API call (set the appropriate loading and error states, too!)
class MyWidget extends StatefulWidget {
Suggestions({Key key, this.someId}) : super(key: key);
String someId
#override
State<StatefulWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
dynamic data;
Error err;
bool loading;
#override
Widget build(BuildContext context) {
if(loading) return Loader();
if(err) return SomeErrorMessage(err);
return SomeOtherStateLessWidget(data);
}
#override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// id changed in the widget, I need to make a new API call
if(oldWidget.id != widget.id) update();
}
update() async {
// set loading and reset error
setState(() => {
loading = true,
err = null
});
try {
// make the call
someData = await apiCall(widget.id);
// set the state
setState(() => data = someData)
} catch(e) {
// oops an error happened
setState(() => err = e)
}
// now we're not loading anymore
setState(() => loading = false);
}
}
I'm brand new to Flutter (literally, just started playing with it this weekend), but it essentially duplicates React paradigms, if that helps you at all.
Personal preference, I vastly prefer this method rather than use FutureBuilder (right now, like I said, I'm brand new). The logic is just easier to reason about (for me).

What's the design benefit of Flutter's (Widget)State/StatefulWidget pattern?

My docs and Flutter videos, the explanation of the design of the StatefulWidget (+(Widget)State) is that it:
promotes a declarative design (good)
formalizes the process by which Flutter to efficiently decide which components need to be re-rendered (also good)
From the example:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {...}
}
However:
since we have to explicitly remember call setState in order to invalidate the state, is this really a declarative design?
Flutter doesn't automatically detect changes in the State object and decide to call build (although it could have), and so it doesn't really formalize/automate/make-safe the invalidation of view components. Since we have to explicitly call setState, what's the benefit of the Flutter's (Widget)State/StatefulWidget pattern over, let's say:
class MyHomePage extends StatefulWidget // Define dirty method
{
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
int _counter = 0;
_incrementCounter() {
_counter++;
this.dirty(); // Require the view to be rebuilt. Arranges generateView to be called.
}
#override
Widget generateView(BuildContext context) {return ... rendering description containing updated counter ... ;}
}
... which would place the same burden of marking the UI dirty on the programmer, is no less decalrative, and avoids additional abstraction that obfuscates the intention of the program.
What have I missed? What's the benefit of separating of StatefulWidget from (Widget)State in Flutter?
[Before people chime in with MVC comments, note that the Flutter model rather explicitly only manages only the widget's state and its tightly coupled to the UI's Widget through the build method - there is no separation of concern here and it doesn't have a lot to say about larger application state that's not attached to a view.]
[Also, moderators, these not the same questions: Why does Flutter State object require a Widget?, What is the relation between stateful and stateless widgets in Flutter?. My question is one about what's the benefit of the present design, not how this design works.]
Update: #Rémi Rousselet -- Here's a declarative example with only a new state class needing to be declared. With some work, you could even get rid of that (though it may not be better).
This way of declaring interaction with need didn't require (the user) declaring two new circularly type-referencing class, and the widget that is changing in response to state is decoupled from the state (its constructed a pure function of the state and does not need to allocate the state).
This way of doing things doesn't survive hot-reload. (sad face).
I suspect this is more of an issue with hot-reload, but if there's a way to make it work it would be great,
import 'dart:collection';
import 'package:flutter/material.dart';
////////////////////////////////
// Define some application state
class MyAppState with ChangeSubscribeable<MyAppState> {
/***
* TODO. Automate notifyListeners on setter.
* Binds changes to the widget
*/
int _counter;
get counter => _counter;
set counter(int c) {
_counter = c;
notifyListeners(); // <<<<<< ! Calls ... .setState to invalidate widget
}
increment() {
counter = _counter + 1;
}
MyAppState({int counter: 0}) {
_counter = counter;
}
}
void main() => runApp(MyApp5());
class MyApp5 extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Declare the mutable state.
// Note because the state is not coupled to any particular widget
// its possible to easily share the state between concerned.
// StateListeningWidgets register for, and are notified on changes to
// the state.
var state = new MyAppState(counter: 5);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: Column(
children: [
// When the button is click, increment the state
RaisedButton(
onPressed: () => {
state.increment(),
print("Clicked. New state: ${state.counter}")
},
child: Text('Click me'),
),
// Listens for changes in state.
StateListeningWidget(
state,
// Construct the actual widget based on the current state
// A pure function of the state.
// However, is seems closures are not hot-reload.
(context, s) => new Text("Counter4 : ${s.counter}"),
),
],
))),
);
}
}
// //////////////////////
// Implementation
// This one is the onChange callback should accept the state.
//typedef OnChangeFunc<ARG0> = void Function(ARG0);
typedef OnChangeFunc = void Function();
mixin ChangeSubscribeable<STATE> {
final _listener2Notifier =
new LinkedHashMap<Object, OnChangeFunc>(); // VoidFunc1<STATE>>();
List<OnChangeFunc> get _listeners => List.from(_listener2Notifier.values);
void onChange(listenerKey, OnChangeFunc onChange) {
// onChange(listenerKey, VoidFunc1<STATE> onChange) {
assert(!_listener2Notifier.containsKey(listenerKey));
_listener2Notifier[listenerKey] = onChange;
print("Num listeners: ${_listener2Notifier.length}");
}
void removeOnChange(listenerKey) {
if (_listener2Notifier.containsKey(listenerKey)) {
_listener2Notifier.remove(listenerKey);
}
}
void notifyListeners() {
// _listener2Notifier.forEach((key, value)=>value(state));
// Safer, in-case state-update triggers add/remove onChange:
// Call listener
_listeners.forEach((value) => value());
}
}
typedef StateToWidgetFunction<WIDGET extends Widget,
STATE extends ChangeSubscribeable>
= WIDGET Function(BuildContext, STATE);
void noOp() {}
class _WidgetFromStateImpl<WIDGET extends Widget,
STATE extends ChangeSubscribeable> extends State<StatefulWidget> {
STATE _state;
// TODO. Make Widget return type more specific.
StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;
_WidgetFromStateImpl(this.stateToWidgetFunc, this._state) {
updateState(){setState(() {});}
this._state.onChange(this, updateState);
}
#override
Widget build(BuildContext context) => stateToWidgetFunc(context, this._state);
#override
dispose() {
_state.removeOnChange(this);
super.dispose();
}
}
class StateListeningWidget<WIDGET extends Widget,
STATE extends ChangeSubscribeable> extends StatefulWidget {
STATE _watched_state;
StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;
StateListeningWidget(this._watched_state, this.stateToWidgetFunc) {}
#override
State<StatefulWidget> createState() {
return new _WidgetFromStateImpl<WIDGET, STATE>(
stateToWidgetFunc, _watched_state);
}
}
I've been directed at the ChangeProvider pattern: https://github.com/flutter/samples/blob/master/provider_counter/lib/main.dart
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter Demo Home Page'),),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Consumer<Counter>( // <<< Pure. Hidden magic mutable parameter
builder: (context, counter, child) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),),],),),
floatingActionButton: FloatingActionButton(
onPressed: () =>
// <<< Also a hidden magic parameter
Provider.of<Counter>(context, listen: false).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
... but this also suffers problems:
its not clear to reader of what the state requirements are or how to provide them -- the interface (at least in this github example HomePage) example does not require Counter as a formal parameter. Here we have new HomePage() that has configuration that is not provided in its parameters - this type of access suffers similar problems to global variables.
access to state is by class type, not object reference - so its not clear (or at least straightforward) what to do if you want two objects of the same type (e.g. shippingAddress, billingAddress) that are peers in the model. To resolve this, the state model likely needs to be refactored.
I think I'm with user48956 on this. (Catchy name by the way).
Unfortunately, the Flutter authors seem to have suffixed their View class with the word 'State'. This has rather confused the whole Flutter state management discussions.
I think the purpose of the two classes is actually to make the painting more performant but it comes with a very heavy plumbing cost for us developers.
As to the naming convention:
The dirty flag approach allows the widget painter to optimise their painting without knowing about our state, thereby alleviation the need for two classes.
Also generateView() is kinda meaningful (unless of course, you start using these widgets to hold model-fragments (as per Package:provider).

Flutter event gets lost in stream

I've recently started using state management in flutter and have pretty much settled on BloC. However I do not use the bloc package or any similar dependency for it since my codebase is not that complex and I like writing it on my own. But I've come across an issue i just can't seem to get fixed. In summary, I have a stream that seems to just loose a certain event everytime i put it in the sink.
I've built an example app that is much simpler than my actual codebase, but still has this issue. The app consists of two pages with the first (main)page displaying a list of strings. When you click on one of the list-items, the second page will open up and the string/the item you clicked on will be displayed on this page.
Each of the two pages has an own BloC, but since the two pages need to be somewhat connected to get the selected item from the first to the second page, there is a third AppBloC which gets injected into the other two BloCs. It exposes a sink and a stream to send data between the other two BloCs.
The only third party package used in this example is kiwi (0.2.0) for dependency injection.
my main.dart is pretty simple and looks like this:
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart' as kw; //renamed to reduce confusion with flutter's own Container widget
import 'package:streams_bloc_test/first.dart';
import 'package:streams_bloc_test/second.dart';
import 'bloc.dart';
kw.Container get container => kw.Container(); //Container is a singleton used for dependency injection with Kiwi
void main() {
container.registerSingleton((c) => AppBloc()); //registering AppBloc as a singleton for dependency injection (will be injected into the other two blocs)
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final appBloc = container.resolve(); //injecting AppBloc here just to dispose it when the App gets closed
#override
void dispose() {
appBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp( //basic MaterialApp with two routes
title: 'Streams Test',
theme: ThemeData.dark(),
initialRoute: "first",
routes: {
"first": (context) => FirstPage(),
"first/second": (context) => SecondPage(),
},
);
}
}
then there are the two pages:
first.dart:
import 'package:flutter/material.dart';
import 'package:streams_bloc_test/bloc.dart';
class FirstPage extends StatefulWidget { //First page that just displays a simple list of strings
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final bloc = FirstBloc();
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FirstPage")),
body: StreamBuilder<List<String>>(
initialData: [],
stream: bloc.list,
builder: (context, snapshot) {
return ListView.builder( //displays list of strings from the stream
itemBuilder: (context, i){
return ListItem(
text: snapshot.data[i],
onTap: () { //list item got clicked
bloc.selectionClicked(i); //send selected item to second page
Navigator.pushNamed(context, "first/second"); //open up second page
},
);
},
itemCount: snapshot.data.length,
);
}),
);
}
}
class ListItem extends StatelessWidget { //simple widget to display a string in the list
final void Function() onTap;
final String text;
const ListItem({Key key, this.onTap, this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
padding: EdgeInsets.all(16.0),
child: Text(text),
),
onTap: onTap,
);
}
}
second.dart:
import 'package:flutter/material.dart';
import 'package:streams_bloc_test/bloc.dart';
class SecondPage extends StatefulWidget { //Second page that displays a selected item
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
final bloc = SecondBloc();
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: StreamBuilder( //selected item is displayed as the AppBars title
stream: bloc.title,
initialData: "Nothing here :/", //displayed when the stream does not emit any event
builder: (context, snapshot) {
return Text(snapshot.data);
},
),
),
);
}
}
and finally here are my three BloCs:
bloc.dart:
import 'dart:async';
import 'package:kiwi/kiwi.dart' as kw;
abstract class Bloc{
void dispose();
}
class AppBloc extends Bloc{ //AppBloc for connecting the other two Blocs
final _selectionController = StreamController<String>(); //"connection" used for passing selected list items from first to second page
Stream<String> selected;
Sink<String> get select => _selectionController.sink;
AppBloc(){
selected = _selectionController.stream.asBroadcastStream(); //Broadcast stream needed if second page is opened/closed multiple times
}
#override
void dispose() {
_selectionController.close();
}
}
class FirstBloc extends Bloc { //Bloc for first Page (used for displaying a simple list)
final appBloc = kw.Container().resolve<AppBloc>(); //injected AppBloc
final listItems = ["this", "is", "a", "list"]; //example list items
final _listController = StreamController<List<String>>();
Stream<List<String>> get list => _listController.stream;
FirstBloc(){
_listController.add(listItems); //initially adding list items
}
selectionClicked(int index){ //called when a list item got clicked
final item = listItems[index]; //obtaining item
appBloc.select.add(item); //adding the item to the "connection" in AppBloc
print("item added: $item"); //debug print
}
#override
dispose(){
_listController.close();
}
}
class SecondBloc extends Bloc { //Bloc for second Page (used for displaying a single list item)
final appBloc = kw.Container().resolve<AppBloc>(); //injected AppBloc
final _titleController = StreamController<String>(); //selected item is displayed as the AppBar title
Stream<String> get title => _titleController.stream;
SecondBloc(){
awaitTitle(); //needs separate method because there are no async constructors
}
awaitTitle() async {
final title = await appBloc.selected.first; //wait until the "connection" spits out the selected item
print("recieved title: $title"); //debug print
_titleController.add(title); //adding the item as the title
}
#override
void dispose() {
_titleController.close();
}
}
The expected behavior would be, that everytime I click on one of the list-items, the second page would open up and display that item as its title. But that's not what is happening here.
Executing the above code will look like this. The first time when you click on a list item, everything works just as intended and the string "this" is set as the second page's title. But closing the page and doing so again, "Nothing here :/" (the default string/initial value of the StreamBuilder) gets displayed. The third time however, as you can see in the screencap, the app starts to hang because of an exception:
Unhandled Exception: Bad state: Cannot add event after closing
The exception occurrs in the BloC of the second page when trying to add the recieved string into the sink so it can be displayed as the AppBar's title:
awaitTitle() async {
final title = await appBloc.selected.first;
print("recieved title: $title");
_titleController.add(title); //<-- thats where the exception get's thrown
}
This seems kind of weird at first. The StreamController (_titleController) is only getting closed when the page is also closed (and the page has clearly not gotten closed yet). So why is this exception getting thrown?
So just for fun I uncommented the line where _titleController gets closed. It will probably create some memory leaks, but that's fine for debugging:
#override
void dispose() {
//_titleController.close();
}
Now that there are no more exceptions that will stop the app from executing, the following happens: The first time is the same as before (title gets displayed - expected behavior), but all the following times the default string gets displayed, not matter how often you try it. Now you may have noticed the two debug prints in bloc.dart. The first tells me when an event is added to the AppBloc's sink and the second one when the event is recieved. Here is the output:
//first time
item added: this
recieved title: this
//second time
item added: this
//third time
item added: this
recieved title: this
//all the following times are equal to the third time...
So as you can clearly see, the second time the event somehow got lost somewhere. This also explains the exception I was getting before. Since the title never got to the second page on the second try, the BloC was still waiting for an event to come through the stream. So when i clicked on the item the third time, the previous bloc was still active and recieved the event. Of course then the page and the StreamController were already closed, ergo the exception. So everytime the default string is displayed the following times is basically just because the previous page was still alive and caught the string...
So the part I can't seem to figure out is, where did that second event go? Did i miss something really trivial or get something wrong somewhere? I tested this on the stable channel (v1.7.8) as well as on the master channel (v1.8.2-pre.59) on multiple different android versions. I used dart 2.4.0.
You can try to use Rxdart's BehaviorSubject instead of StreamController in your main AppBloc
final _selectionController = BehaviorSubject<String>();
And your stream listener can be a just stream instead of a broadcast stream
selected = _selectionController.stream;
The reason I am suggesting this is because RxDart's BehaviorSubject makes sure it always emits the last stream at every point in time wherever it is being listened to.