Navigating to a new screen when stream value in BLOC changes - ios

In Flutter how would I call Navigator.push when the value of a stream changes? I have tried the code below but get an error.
StreamBuilder(
stream: bloc.streamValue,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data == 1) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SomeNewScreen()),
);
}
return Text("");
});

You should not use StreamBuilder to handle navigation.
StreamBuilder is used to build the content of a screen and nothing else.
Instead, you will have to listen to the stream to trigger side-effects manually. This is done by using a StatefulWidget and overriding initState/dispose as such:
class Example extends StatefulWidget {
final Stream<int> stream;
const Example({Key key, this.stream}) : super(key: key);
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
StreamSubscription _streamSubscription;
#override
void initState() {
super.initState();
_listen();
}
#override
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.stream != widget.stream) {
_streamSubscription.cancel();
_listen();
}
}
void _listen() {
_streamSubscription = widget.stream.listen((value) {
Navigator.pushNamed(context, '/someRoute/$value');
});
}
#override
void dispose() {
_streamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Note that if you're using an InheritedWidget to obtain your stream (typically BLoC), you will want to use didChangeDependencies instead of initState/didUpdateWidget.
This leads to:
class Example extends StatefulWidget {
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
StreamSubscription _streamSubscription;
Stream _previousStream;
void _listen(Stream<int> stream) {
_streamSubscription = stream.listen((value) {
Navigator.pushNamed(context, '/someRoute/$value');
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final bloc = MyBloc.of(context);
if (bloc.stream != _previousStream) {
_streamSubscription?.cancel();
_previousStream = bloc.stream;
_listen(bloc.stream);
}
}
#override
void dispose() {
_streamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}

You can extend StreamBuilder with custom listener like this:
typedef StreamListener<T> = void Function(T value);
class StreamListenableBuilder<T> extends StreamBuilder<T> {
final StreamListener<T> listener;
const StreamListenableBuilder({
Key key,
T initialData,
Stream<T> stream,
#required this.listener,
#required AsyncWidgetBuilder<T> builder,
}) : super(key: key, initialData: initialData, stream: stream, builder: builder);
#override
AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
listener(data);
return super.afterData(current, data);
}
}
Then connect listener for navigation this way:
StreamListenableBuilder(
stream: bloc.streamValue,
listener: (value) {
if (value==1) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SomeNewScreen()),
);
}
},
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Container();
});

Related

How to initialize state using the Provider package?

TL;DR - Getting providerInfo = null from Consumer<ProviderInfo>(
builder: (context, providerInfo, child),
I have a flutter app that uses scoped_model that works just fine but I want to refactor it so it'll use Provider
The code with scoped_model:
//imports...
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
final MainModel _model = MainModel();// The data class, extends scoped_model.Model class, with all of other models...
bool _isAuthenticated = false;
#override
void initState() {
_model.init();
super.initState();
}
#override
Widget build(BuildContext context) {
return ScopedModel<MainModel>(
model: _model,
child: MaterialApp(
title: "MyApp",
routes: {
'/': (BuildContext context) => _isAuthenticated == false ? AuthenticationPage() : HomePage(_model),
'/admin': (BuildContext context) =>
_isAuthenticated == false ? AuthenticationPage() : AdminPage(_model),
},
// the rest of build...
}
and the code that I tried to refactor to use Provider:
//#lib/main.dart
//imports...
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ProviderInfo>(
builder: (context) {
ProviderInfo(); // the data model.
},
child: Consumer<ProviderInfo>(
builder: (context, providerInfo, child) => MaterialApp(
title: "MyApp",
routes: {
'/': (BuildContext context) {
providerInfo.isAuthenticated == false ? AuthenticationPage() : HomePage(providerInfo);
},
'/admin': (BuildContext context) {
providerInfo.isAuthenticated == false ? AuthenticationPage() : AdminPage(_model);
},
//the rest of build...
},
//#ProviderInfo
class ProviderInfo extends CombinedModel with ProductModel, UserModel, UtilityModel {
ProviderInfo() {
this.init();
}
}
The problem with this code is that in the builder function of Consumer<ProviderInfo> the providerInfo is null (and also after of course, in routes etc...).
what did I do wrong?
how can I refactor it so it'll works fine?
You forgot to return something in the builder of your provider.
Change
ProviderInfo()
To
return ProviderInfo()

Pass parameter to initState

Look at this code - widget to fetch data and display on list:
class _MyEventsFragmentState extends State <MyEventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(true);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
fetchEvent method has parameter to indicate which events I need to fetch. If set to true, - my events, if set to false - all events returned. Above code loads my events and fetchEvents is called inside initState override to avoid unnecesary data reloading.
To fetch all events I defined another class:
class EventsFragment extends StatefulWidget {
#override
_EventsFragmentState createState() => new _EventsFragmentState();
}
class _EventsFragmentState extends State <EventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(false);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
But this is very dumb solution, because code is almost the same. So I tried to pass boolean value to indicate which events to load, something like that:
#override
initState(){
super.initState();
events = fetchEvents(isMyEvents);
}
isMyEvents should be got from EventsFragment constructor. However, it won't be accesible inside initState. Ho to pass it properly? I could access it inside build override, but not inside initState. How to pass it properly and make sure it will be refreshed every time widget instance is created?
[edit]
So this how I solved my problem (it seems to be fine):
class EventsFragment extends StatefulWidget {
const EventsFragment({Key key, this.isMyEvent}) : super(key: key);
final bool isMyEvent;
#override
_EventsFragmentState createState() => new _EventsFragmentState();
}
class _EventsFragmentState extends State <EventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(widget.isMyEvent);
}
#override
void didUpdateWidget(EventsFragment oldWidget) {
if(oldWidget.isMyEvent != widget.isMyEvent)
events = fetchEvents(widget.isMyEvent);
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
Pass such parameter to the StatefulWidget subclass, and use that field instead
class Foo extends StatefulWidget {
const Foo({Key key, this.isMyEvent}) : super(key: key);
final bool isMyEvent;
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
#override
void initState() {
super.initState();
print(widget.isMyEvent);
}
#override
Widget build(BuildContext context) {
return Container(
);
}
}

AppLifcycleState.didChangeLifecycleState( )function is not called when app comes foreground or in background

when i open my app or runs in the background the didChangeAppLifecycleState() is not called and the print statements are not executed.
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
void main(){
runApp(
MaterialApp(
home: Home(),
)
);
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with WidgetsBindingObserver{
AppLifecycleState state;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void didChangeAppLifeCycleState(AppLifecycleState appLifecycleState) {
state = appLifecycleState;
print(appLifecycleState);
print(":::::::");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child:Text("hi")
),
),
);
}
}
}
the print statements in the didChangeAppLifeCycleState() is not executing.
This isn't the answer to the OP's problem, every time that I have had this problem I have just forgotten to call WidgetsBinding.instance.addObserver(this); in initState().
If didChangeAppLifecycleState isn't called for you, don't forget to add your implementing class as a listener.
See the OP's code in initState() and dispose() and make sure you do the same.
There was a typo (lowercase "c" in "Lifecycle"), it should be didChangeAppLifecycleState:
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
state = state;
print(state);
print(":::::::");
}
Update: Don't forget to add observer in initState() and remove observer in dispose()
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
// ...
#override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
Hope it helps!
I had to combine codes from the question and the above 2 answers to get the final result. So let me add a simple sample that illustrates how to use AppLifeCycleState properly.
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// --
print('Resumed');
break;
case AppLifecycleState.inactive:
// --
print('Inactive');
break;
case AppLifecycleState.paused:
// --
print('Paused');
break;
case AppLifecycleState.detached:
// --
print('Detached');
break;
}
}
#override
Widget build(BuildContext context) {
return Container();
}
}

How to dispose bloc when using Inherited Widget?

I have made a simple app using bloc and InheritedWidget.
Following is the code
class Bloc {
final StreamController<bool> _changeColor = PublishSubject<bool>();
Function(bool) get changeColour => _changeColor.sink.add;
Stream<bool> get colour => _changeColor.stream;
void dispose(){
_changeColor.close();
}
}
class Provider extends InheritedWidget {
final bloc = Bloc();
Provider({Key key,Widget child}): super(key: key,child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
static Bloc of(BuildContext context){
return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
}
void dispose(){
bloc?.dispose();
}
}
class _HomePageState extends State<HomePage> {
var bloc;
#override
void initState() {
super.initState();
bloc = Provider.of(context);
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
onPressed: () {
bloc.changeColour(true);
},
child: Text("Change colour"),
),
StreamBuilder(
builder: (context, snapshot) {
var bool = snapshot?.data ?? false;
return Text(
"First text",
style:
TextStyle(color: bool ? Colors.red : Colors.green),
);
},
stream: bloc?.colour,
),
],
);
}
#override
void dispose() {
super.dispose();
}
}
I don't understand how to call dispose method of the bloc when using InheritedWidget. Of course I can create a global variable of bloc and avoid using InheritedWidget to dispose the bloc using the dispose method which is present in the bloc but I really want to use InheritedWidget.
Does using the PublishSubject from rxdart disposes the streamcontroller automatically, is it life cycle aware, I couldn't find anything related to this in the documentation. Is there any debugging process to make sure the streamcontroller is disposed off correctly?
That is not possible using Inheritedwidget. The widget is not made to handle data, but to share it.
You have to wrap your Inheritedwidget into a StatefulWidget and use the dispose of the latter
To add to Remi's answer, the code would look something like this
import 'package:flutter/material.dart';
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
#override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context){
return widget.child;
}
}
class Bloc implements BlocBase {
final StreamController<bool> _changeColor = PublishSubject<bool>();
Function(bool) get changeColour => _changeColor.sink.add;
Stream<bool> get colour => _changeColor.stream;
#override
void dispose() {
_changeColor.close();
}
}
class _HomePageState extends State<HomePage> {
Bloc bloc;
var colour = false;
#override
void initState() {
super.initState();
bloc = BlocProvider.of<Bloc>(context);
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
onPressed: () {
if (colour) {
bloc.changeColour(false);
colour = false;
} else {
bloc.changeColour(true);
colour = true;
}
},
child: Text("Change colour"),
),
StreamBuilder(
builder: (context, snapshot) {
var bool = snapshot?.data ?? false;
return Text(
"First text",
style: TextStyle(color: bool ? Colors.red : Colors.green),
);
},
stream: bloc?.colour,
),
],
);
}
#override
void dispose() {
print("Bloc is disposed");
bloc.dispose();
super.dispose();
}
}

setState doesn't update the user interface

I've been facing some problems related to the setState function while using Stateful Widgets that updates itself with the help of Timers. The code below show 2 main classes that replicate how I came to find this error. The Text Widget "Lorem" should be inserted within 10 seconds - and it is - but it's never shown. I tried to debug the array "Items" and it does contain the "lorem" Text Widget after 5 seconds, as it should. The "build" function runs but doesn't make any difference in the UI.
class textList extends StatefulWidget {
#override
State<StatefulWidget> createState() =>
new _textListState();
}
class _textListState extends State<textList>
with TickerProviderStateMixin {
List<Widget> items = new List();
Widget lorem = new textClass("Lorem");
Timer timer;
#override
void initState() {
super.initState();
items.add(new textClass("test"));
items.add(new textClass("test"));
timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
setState(() {
items.removeAt(0);
items.add(lorem);
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
Iterable<Widget> content = ListTile.divideTiles(
context: context, tiles: items).toList();
return new Column(
children: content,
);
}
}
class textClass extends StatefulWidget {
textClass(this.word);
final String word;
#override
State<StatefulWidget> createState() =>
new _textClass(word);
}
class _textClass extends State<textClass>
with TickerProviderStateMixin {
_textClass(this.word);
String word;
Timer timer;
#override
void initState() {
super.initState();
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
word += "t";
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return new Text(word);
}
}
This is not how I came to find this error but this is the simplest way to replicate it. The main idea is: The children texts should keep updating themselves (in this case, adding "t"s in the end) and, after 5 seconds, the last of them should be replaced for the Text Widget "Lorem", what does happen in the list but not in the UI.
Here's what's wrong:
A State should never have any constructor arguments. Use the widget property to get access to final properties of the associated StatefulWidget.
Flutter is reusing your _textClass instance because the class name and keys match. This is a problem since you only set widget.word in initState so you're not picking up the new word configuration information. You can fix this either by giving the StatefulWidget instances unique keys to disambiguate them and cause the old State to be disposed, or you can keep around the old State and implement didUpdateWidget. The latter approach is shown below.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(title: new Text('Example App')),
body: new textList(),
),
));
}
class textList extends StatefulWidget {
#override
State<StatefulWidget> createState() =>
new _textListState();
}
class _textListState extends State<textList>
with TickerProviderStateMixin {
List<Widget> items = new List();
Widget lorem = new textClass("Lorem");
Timer timer;
#override
void initState() {
super.initState();
items.add(new textClass("test"));
items.add(new textClass("test"));
timer = new Timer.periodic(new Duration(seconds: 5), (Timer timer) {
setState(() {
items.removeAt(0);
items.add(lorem);
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
Iterable<Widget> content = ListTile.divideTiles(
context: context, tiles: items).toList();
return new Column(
children: content,
);
}
}
class textClass extends StatefulWidget {
textClass(this.word);
final String word;
#override
State<StatefulWidget> createState() =>
new _textClass();
}
class _textClass extends State<textClass>
with TickerProviderStateMixin {
_textClass();
String word;
Timer timer;
#override
void didUpdateWidget(textClass oldWidget) {
if (oldWidget.word != widget.word) {
word = widget.word;
}
super.didUpdateWidget(oldWidget);
}
#override
void initState() {
super.initState();
word = widget.word;
timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) {
setState(() {
word += "t";
});
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return new Text(word);
}
}

Resources