setState doesn't update the user interface - dart

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

Related

How to maintain Flutter Global BloC state using Provider on Hot Reload?

I seem to lose application state whenever I perform a hot reload.
I am using a BloC provider to store application state. This is passed at the App level in the main.dart and consumed on a child page. On the initial load of the view, the value is shown. I can navigate around the application and the state persists. However, when I perform a hot reload, I lose the values and seemingly the state.
How can I fix this issue so that state is preserved on Hot Reload?
Bloc Provider
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 MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return BlocProvider<ApplicationStateBloc>(
bloc: ApplicationStateBloc(),
child: MaterialApp(
title: 'Handshake',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: LoadingPage(),
)
);
}
}
class ProfileSettings extends StatefulWidget {
#override
_ProfileSettingsState createState() => _ProfileSettingsState();
}
class _ProfileSettingsState extends State<ProfileSettings>{
ApplicationStateBloc _applicationStateBloc;
#override
void initState() {
super.initState();
_applicationStateBloc = BlocProvider.of<ApplicationStateBloc>(context);
}
#override
void dispose() {
_applicationStateBloc?.dispose();
super.dispose();
}
Widget emailField() {
return StreamBuilder<UserAccount>(
stream: _applicationStateBloc.getUserAccount,
builder: (context, snapshot){
if (snapshot.hasData) {
return Text(snapshot.data.displayName, style: TextStyle(color: Color(0xFF151515), fontSize: 16.0),);
}
return Text('');
},
);
}
#override
Widget build(BuildContext context) {
return BlocProvider<ApplicationStateBloc>(
bloc: _applicationStateBloc,
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: <Widget>[
emailField(),
.... // rest of code
class ApplicationStateBloc extends BlocBase {
var userAccountController = BehaviorSubject<UserAccount>();
Function(UserAccount) get updateUserAccount => userAccountController.sink.add;
Stream<UserAccount> get getUserAccount => userAccountController.stream;
#override
dispose() {
userAccountController.close();
}
}
I was facing the same problem. Inherited widgets make it hard disposing bloc's resources.
Stateful widget, on the other hand, allows disposing, but in the implementation you're using it doesn't persist the bloc in the state causing state loss on widgets rebuild.
After some experimenting I came up with an approach that combines the two:
class BlocHolder<T extends BlocBase> extends StatefulWidget {
final Widget child;
final T Function() createBloc;
BlocHolder({
#required this.child,
#required this.createBloc
});
#override
_BlocHolderState createState() => _BlocHolderState();
}
class _BlocHolderState<T extends BlocBase> extends State<BlocHolder> {
T _bloc;
Function hello;
#override
void initState() {
super.initState();
_bloc = widget.createBloc();
}
#override
Widget build(BuildContext context) {
return BlocProvider(
child: widget.child,
bloc: _bloc,
);
}
#override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
Bloc holder creates bloc in createState() and persists it. It also disposes bloc's resources in dispose().
class BlocProvider<T extends BlocBase> extends InheritedWidget {
final T bloc;
const BlocProvider({
Key key,
#required Widget child,
#required T bloc,
})
: assert(child != null),
bloc = bloc,
super(key: key, child: child);
static T of<T extends BlocBase>(BuildContext context) {
final provider = context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider;
return provider.bloc;
}
#override
bool updateShouldNotify(BlocProvider old) => false;
}
BlocProvider, as the name suggests, is only responsible for providing the bloc to nested widgets.
All the blocs extend BlocBase class
abstract class BlocBase {
void dispose();
}
Here's a usage example:
class RouteHome extends MaterialPageRoute<ScreenHome> {
RouteHome({List<ModelCategory> categories, int position}): super(builder:
(BuildContext ctx) => BlocHolder(
createBloc: () => BlocMain(ApiMain()),
child: ScreenHome(),
));
}
You are losing the state because your bloc is being retrieved in the _ProfileSettingsState's initState() thus, it won't change even when you hot-reload because that method is only called only once when the widget is built.
Either move it to the build() method, just before returning the BlocProvider
#override
Widget build(BuildContext context) {
_applicationStateBloc = BlocProvider.of<ApplicationStateBloc>(context);
return BlocProvider<ApplicationStateBloc>(
bloc: _applicationStateBloc,
child: Scaffold(
backgroundColor: Colors.white,
....
or to the didUpdateWidget method which is called anytime the widget state is rebuild.
Have in mind that if you are using a non-broadcast stream in your bloc you may get an exception if you try to listen to a stream that is already being listened to.

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

Navigating to a new screen when stream value in BLOC changes

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

Reset State of children widget

Here is the summary of the code I'm having a problem with:
Parent widget
class HomePage extends StatefulWidget {
#override
State<HomePage> createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
final GlobalKey<AsyncLoaderState> _asyncLoaderState = GlobalKey<AsyncLoaderState>();
List<DateTime> rounds;
List<PickupModel> pickups;
DateTime currentDate;
Widget build(BuildContext context) {
var _asyncLoader = AsyncLoader(
key: _asyncLoaderState,
initState: () async => await _getData(),
renderLoad: () => Scaffold(body: Center(child: CircularProgressIndicator())),
renderError: ([error]) => Text('Sorry, there was an error loading'),
renderSuccess: ({data}) => _buildScreen(context),
);
return _asyncLoader;
}
Widget _buildScreen(context) {
return Scaffold(
body: PickupList(pickups),
);
}
Future<Null> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
);
if (picked != null && picked != currentDate) {
currentDate = picked;
pickups = await api.fetchPickupList(currentDate);
setState(() {
});
}
}
_getData() async {
rounds = await api.fetchRoundsList();
currentDate = _getNextRound(rounds);
pickups = await api.fetchPickupList(currentDate);
}
}
Children Widget
(Listview builds tiles)
class PickupTile extends StatefulWidget{
final PickupModel pickup;
PickupTile(this.pickup);
#override
State<StatefulWidget> createState() {
return PickupTileState();
}
}
class PickupTileState extends State<PickupTile> {
Duration duration;
Timer timer;
bool _isUnavailable;
bool _isRunning = false;
bool _isDone = false;
#override
void initState() {
duration = widget.pickup.duration;
_isUnavailable = widget.pickup.customerUnavailable;
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
// UI widgets
]
}
So I have a parent widget an initial list of pickups which are displayed in the children PickupTile. One can change the date of the pickups displayed using _selectDate. This fetches a new list of Pickups which are stored in the parent State, and the children are rebuilt with their correct attributes. However, the State of the children widget (duration, isRunning, isDone...) is not reset so they stay on screen when changing the date.
If feel like I'm missing something obvious but I can't figure out how to reset the State of the children Widget or create new PickupTiles so that when changing the date I get new separate States.

How to open Scaffold's Drawer on page load?

Logging into our Flutter app opens to dashboard that has a Scaffold with a Drawer full of menu items.
I'd like to perform some A/B testing with having the Drawer open on page load or at least animating the Drawer being opened immediately on load.
I'm aware of Scaffold.of(context).openDrawer() but I'm not sure where to place this code so that it will run immediately after the build() method. I'm also not aware of any fields on either Drawer or Scaffold which would load with the Drawer open.
Thanks for your time and help.
You need to wait after the first frame is loaded.
_onLayoutDone(_) {
//your logic here
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone);
super.initState();
}
I wrote a post about this, you can take a look if you want : https://medium.com/#diegoveloper/flutter-widget-size-and-position-b0a9ffed9407
Override initState.
#override
void initState() {
super.initState();
// use this
Timer.run(() => Scaffold.of(context).openDrawer());
}
Store a state variable to hide and show drawer - isDrawerBeingShown.
Based on the state variable toggle the state of drawer. It is set to false by default so it will be displayed for the first time.
void _showDrawer(BuildContext context) async it must be marked as async so that it runs after build method.
Create showDrawerUtility method to show drawer on demand when ever required.
Edit:
Use GlobalKey
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class MainScreen extends StatefulWidget {
MainScreen({Key key }) : super(key: key);
#override
State<MainScreen> createState() => new MainScreenState();
}
class MainScreenState extends State<MainScreen> {
bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if(!isDrawerBeingShown) {
_scaffoldKey.currentState.openDrawer();
setState(() => isDrawerBeingShown = true);
}
}
#override
Widget build(BuildContext context) { // build method goes here}
}
follow my code
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class openDrawerOnLoadPage extends StatefulWidget {
openDrawerOnLoadPage({Key? key}) : super(key: key);
#override
_openDrawerOnLoadPageState createState() => _openDrawerOnLoadPageState();
}
class _openDrawerOnLoadPageState extends State<openDrawerOnLoadPage> {
late bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if (!isDrawerBeingShown) {
EasyDebounce.debounce('openDrawer', Duration(milliseconds: 100),
() async {
_scaffoldKey.currentState!.openDrawer();
setState(() => isDrawerBeingShown = true);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
);
}
}

Resources