Flutter event gets lost in stream - flutter

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.

Related

How can I refresh the page when the user comes back to the app (from background)?

Right now the only way my app can refresh (it's a news app so it needs to constantly refresh) is two ways: 1) Refresh by scrolling up or 2) Restart the app after killing it from background.
I want to make it so that when a user just comes back to the app (say I'm using my app, then I go to WeChat to send a text, then I come back), the app is refreshed.
This is the refresh scroll code.
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
Then it calls this function:
Future<void> _refresh() async {
print("Refreshed");
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
),
).then((value) => null);
}
What should I do to achieve what I need?
Is there a way to check if someone has "returned" to my app? Then I can just call the function.
You need to subscribe to the application life cycle. In Flutter it's not build in the same was it is natively. However, there is a good article I stumbled across a last year, trying to accomplish the same thing:
https://medium.com/pharos-production/flutter-app-lifecycle-4b0ab4a4211a
The gist of the implementation looks like this:
class MyHomePage extends StatefulWidget {
#override _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('state = $state');
}
#override
Widget build(BuildContext context) {
return YourWidget();
}
}
A better solution is to give the child widget of your FutureBuilder or StreamBulder a random unique key. This forces the underlying widget to redraw (and trigger the future or stream) when you come back from the background.
key: UniqueKey(),
Example:
return FutureBuilder<PriceData>(
future: DbService.getPriceData(),
builder: (context, priceData) {
if (priceData.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return AnimatedAppear(
key: UniqueKey(), <---- here!

how to get mobile messages using flutter

I have to get messages from mobile so I am using this plugin. It is working but I get no messages, just printing the result Instance of 'SmsMessage' in the console, but I did everything specified in document example. did I make any mistakes?
source code
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:sms/sms.dart';
class MessagesScreen extends StatefulWidget {
MessagesScreen({Key key}) : super(key: key);
#override
_MessagesScreenState createState() => _MessagesScreenState();
}
class _MessagesScreenState extends State<MessagesScreen> {
List _allMessages;
#override
void initState() {
super.initState();
getAllMessages();
}
Future getAllMessages() async {
SmsQuery query = new SmsQuery();
List<SmsMessage> messages = await query.getAllSms;
debugPrint("Total Messages : " + messages.length.toString());
print(messages);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Messages"),
),
body: ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return Text("Test");
},
),
);
}
}
This has nothing to do with SMS messages, you just cannot print a list of custom objects that way.
Go through the list in a loop and print the property of every message that you want to print. Just like you would have to do with any other complex object.
The behavior you describe is actually normal. The messages will be returned as an instance of the SmsMessage class.
When you try to print an object, it is converted to a string. By default the toString method will display Instance of "Class name"; and in your case, that's why you are having that output, you are trying to print a list of objects.
If for each of the messages, you would like to display the body, this code snippet should help you:
Future getAllMessages() async {
SmsQuery query = new SmsQuery();
List<SmsMessage> messages = await query.getAllSms;
debugPrint("Total Messages : " + messages.length.toString());
messages.forEach((element) { print(element.body); });
}

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

Build function in StatelessWidget keeps refiring

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.

Controlling State from outside of a StatefulWidget

I'm trying to understand the best practice for controlling a StatefulWidget's state outside of that Widgets State.
I have the following interface defined.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:
When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this event and perform some action ( DB call, service request, etc ).
The controller can call showActivity or set message to have the view show progress with a message.
Because a Stateful Widget does not expose its State as a property, I don't know the best approach for accessing and modifying the State's attributes.
The way I would expect to use this would be something like this:
Widget createStartupPage() {
var page = new StartupPage();
page.onAppSelected.listen((app) {
page.showActivity = true;
//Do some work
page.showActivity = false;
});
}
I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.
Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability, and forward-thinking towards Flutter, we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.
There are multiple ways to interact with other stateful widgets.
1. findAncestorStateOfType
The first and most straightforward is through context.findAncestorStateOfType method.
Usually wrapped in a static method of the Stateful subclass like this :
class MyState extends StatefulWidget {
static of(BuildContext context, {bool root = false}) => root
? context.findRootAncestorStateOfType<_MyStateState>()
: context.findAncestorStateOfType<_MyStateState>();
#override
_MyStateState createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return Container();
}
}
This is how Navigator works for example.
Pro:
Easiest solution
Con:
Tempted to access State properties or manually call setState
Requires to expose State subclass
Don't use this method when you want to access a variable. As your widget may not reload when that variable change.
2. Listenable, Stream and/or InheritedWidget
Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.
In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.
This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :
First, we have an InheritedWidget that expose a count :
class Count extends InheritedWidget {
static of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Count>();
final int count;
Count({Key key, #required Widget child, #required this.count})
: assert(count != null),
super(key: key, child: child);
#override
bool updateShouldNotify(Count oldWidget) {
return this.count != oldWidget.count;
}
}
Then we have our State that instantiate this InheritedWidget
class _MyStateState extends State<MyState> {
int count = 0;
#override
Widget build(BuildContext context) {
return Count(
count: count,
child: Scaffold(
body: CountBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
),
),
);
}
}
Finally, we have our CountBody that fetch this exposed count
class CountBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text(Count.of(context).count.toString()),
);
}
}
Pros:
More performant than findAncestorStateOfType
Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
Automic reload of the children when the value change
Cons:
More boilerplate
Stream can be complicated
3. Notifications
Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.
An example of Notification would be :
class MyNotification extends Notification {
final String title;
const MyNotification({this.title});
}
To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.
MyNotification(title: "Foo")..dispatch(context)
Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.
Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onTitlePush,
child: Container(),
);
}
bool onTitlePush(MyNotification notification) {
print("New item ${notification.title}");
// true meaning processed, no following notification bubbling.
return true;
}
}
An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController
Pros:
Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
More than one widget can subscribe to that same notification
Prevents children from accessing unwanted State properties
Cons:
May not fit your use-case
Requires more boilerplate
You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:
class StartupPage extends StatefulWidget {
static StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<StartupPageState>());
#override
StartupPageState createState() => new StartupPageState();
}
class StartupPageState extends State<StartupPage> {
...
}
You can then access the state by calling StartupPage.of(context).doSomething();.
The caveat here is that you need to have a BuildContext with that page somewhere in its tree.
There is another common used approach to have access to State's properties/methods:
class StartupPage extends StatefulWidget {
StartupPage({Key key}) : super(key: key);
#override
StartupPageState createState() => StartupPageState();
}
// Make class public!
class StartupPageState extends State<StartupPage> {
int someStateProperty;
void someStateMethod() {}
}
// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();
// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();
I do:
class StartupPage extends StatefulWidget {
StartupPageState state;
#override
StartupPageState createState() {
this.state = new StartupPageState();
return this.state;
}
}
class DetectedAnimationState extends State<DetectedAnimation> {
And outside just startupPage.state
While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.
In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):
class StartupPage extends StatefulWidget {
static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
#override
_StartupPageState createState() => new _StartupPageState();
}
class _StartupPageState extends State<StartupPage> {
...
}
The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.
You can use eventify
This library provide mechanism to register for event notifications with emitter
or publisher and get notified in the event of an event.
You can do something like:
// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();
var controlNumber = 50;
List<Widget> buttonsGenerator() {
final List<Widget> buttons = new List<Widget>();
for (var i = 0; i < controlNumber; i++) {
widgets.add(new MaterialButton(
// Generate 10 Buttons afterwards
onPressed: () {
controlNumber = 10;
emitter.emit("updateButtonsList", null, "");
},
);
}
}
class AState extends State<ofYourWidget> {
#override
Widget build(BuildContext context) {
List<Widget> buttons_list = buttonsGenerator();
emitter.on('updateButtonsList', null, (event, event_context) {
setState(() {
buttons_list = buttonsGenerator();
});
});
}
...
}
I can't think of anything which can't be achieved by event driven programming. You are limitless!
"Freedom cannot be bestowed — it must be achieved."
- Elbert Hubbard
Have you considered lifting the state to the parent widget? It is a common, though less ideal than Redux, way to manage state in React as far as I know, and this repository shows how to apply the concept to a Flutter app.