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,
);
}
}
Related
I have an asset file that need to be processed before it can be used. This asset file will be heavily edited and I would like to not to have to restart the application each time I make an edit.
I'm aware of the existence of the reassemble method on the State class. However, this requires having a dummy widget that overrides this method and putting it inside the app somewhere to get notified about hot reload.
class WdHotReloadNotifier extends StatefulWidget
{
final Function callback;
WdHotReloadNotifier(this.callback);
#override
State<StatefulWidget> createState() => WdHotReloadNotifierState(this.callback);
}
class WdHotReloadNotifierState extends State<WdHotReloadNotifier>
{
Function callback;
WdHotReloadNotifierState(this.callback);
#override
void reassemble()
{
super.reassemble();
callback();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Then I can use it like this:
WdHotReloadNotifier((){print("HOT REALOADED 1");}),
WdHotReloadNotifier((){print("HOT REALOADED 2");}),
However, adding these to a single page means that it will work as long as the page is in the stack. And adding them to multiple pages means the hooks will execute more than once.
Is there a way in flutter to get notified globally about a hot reload?
Overriding the reassemble method on a State subclass is what you want.
But you can position the widget to a different location to change the behavior.
Consider the following widget which calls a callback on hot-reload and does nothing else:
class ReassembleListener extends StatefulWidget {
const ReassembleListener({Key key, this.onReassemble, this.child})
: super(key: key);
final VoidCallback onReassemble;
final Widget child;
#override
_ReassembleListenerState createState() => _ReassembleListenerState();
}
class _ReassembleListenerState extends State<ReassembleListener> {
#override
void reassemble() {
super.reassemble();
if (widget.onReassemble != null) {
widget.onReassemble();
}
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
You're free to insert that widget wherever you like.
Be it on a single page:
MaterialApp(
home: ReassembleListener(onReassemble: () => print("Foo"), child: Home()),
)
Or globally by wrapping the whole application:
ReassembleListener(
onReassemble: () => print('foo'),
child: MaterialApp(
home: Home(),
),
)
If I were using a StatefulWidget, then I would be listening to a Stream for example inside the initState method. Where would I do the equivalent in a StatelessWidget (like to use Bloc with streams for state management)? I could do it in the build method but since these are repetitively I wondered if there is a more efficient way than checking for existent listeners like below. I know that this is a redundant and useless example but it's just to show the problem.
import "package:rxdart/rxdart.dart";
import 'package:flutter/material.dart';
final counter = BehaviorSubject<int>();
final notifier = ValueNotifier<int>(0);
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
if (!counter.hasListener)
counter.listen((value) => notifier.value += value);
return MaterialApp(
home: Scaffold(
body: Center(
child:FlatButton(
onPressed: () => counter.add(1),
child: ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => Text(
value.toString()
),
),
)
),
)
);
}
}
There is no clean way to have a StatelessWidget listen to a Listenable/Stream.
You will always need a StatefulWidget.
On the other hand, you can use composition to write that StatefulWidget just once, and be done with it.
Common examples for that pattern are widgets such as ValueListenableBuilder, StreamBuilder, or AnimatedBuilder. But it is possible to do the same thing, for listening too.
You'd use it this way:
class Foo extends StatelessWidget {
Foo({Key key, this.counter}): super(key: key);
final ValueListenable<int> counter;
#override
Widget build(BuildContext context) {
return ValueListenableListener(
valueListenable: counter,
onChange: (value) {
// TODO: do something
},
child: Something(),
);
}
}
Where ValueListenableListener is implemented this way:
class ValueListenableListener<T> extends StatefulWidget {
const ValueListenableListener(
{Key key, this.valueListenable, this.onChange, this.child})
: super(key: key);
final ValueListenable<T> valueListenable;
final ValueChanged<T> onChange;
final Widget child;
#override
_ValueListenableListenerState createState() =>
_ValueListenableListenerState();
}
class _ValueListenableListenerState extends State<ValueListenableListener> {
#override
void initState() {
super.initState();
widget.valueListenable?.addListener(_listener);
_listener();
}
#override
void didUpdateWidget(ValueListenableListener oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.valueListenable != widget.valueListenable) {
oldWidget.valueListenable?.removeListener(_listener);
widget.valueListenable?.addListener(_listener);
_listener();
}
}
#override
void dispose() {
widget.valueListenable?.removeListener(_listener);
super.dispose();
}
void _listener() {
widget.onChange?.call(widget.valueListenable.value);
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
You shouldn't. Not handling variables that might have their values modified is the very purpose of a Stateless widget:
A stateless widget never changes.
UPDATE:
I think this is a problem of understanding Flutter's state management concepts. This new recommended way by the Flutter team should clear some confusions.
You could do something like this:
class ExampleWidget extends StatelessWidget {
bool _initialized = false;
#override
Widget build(BuildContext context) {
if (!_initialized) {
_initialized = true;
// Add listeners here only once
}
return Container();
}
}
But you shouldn't! In fact, your IDE will give you a warning, because this is not the way to go with Stateless widget as it is marked as #immutable. If you need to use lifecycle methods (like initState()) you should make it a Stateful widget. There's no big deal.
This is achievable with flutter_bloc package. The code to be run in initstate can be added inside BlocListener on whatever state you want.
BlocProvider(
create: (BuildContext context) =>
CategoryBlock()..add(LoadCategories()),
child: BlocListener<CategoryBlock, CategoryStates>(
listener: (context, state) {
//Example to add a listener for listview
if (state is LoadCategoriesSuccess) {
itemPositionsListener.itemPositions.addListener(() {
print(itemPositionsListener.itemPositions.value);
});
}
}
You could have your streams being instantiated in a StatefulWidget and then passed down to your StatelessWidgets as an option, so the parent widget would only have a role of controlling the lifecycle of the stream while the child would be using the stream to update the view.
Regarding the earlier answer:
There's no problem in using StreamBuilders inside your StatelessWidgets since the StreamBuilder itself is a a Widget that extends from StatefulWidget and will take care of it's own state and dispose correctly on its own.
I am using a very simple code to check the working of suspending method in the Flutter lifecycle callbacks when I exit the app by pressing back button from the device. But it's not getting called.
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
WidgetsBinding binding = WidgetsBinding.instance;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print("state: $state");
}
#override
void initState() {
super.initState();
binding.addObserver(this);
}
#override
Widget build(BuildContext context) {
return Center(child: Text("Center"));
}
#override
void dispose() {
binding.removeObserver(this);
super.dispose();
}
}
You should try the didPopRoute() lifecycle hook for back button on Android. From the docs:
Called when the system tells the app to pop the current route. For example, on Android, this is called when the user presses the back button.
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
WidgetsBinding binding = WidgetsBinding.instance;
#override
Future<bool> didPopRoute() {
// Android user pressed back button
final bool preventAppFromClosing = true;
return preventAppFromClosing;
}
#override
void initState() {
super.initState();
binding.addObserver(this);
}
#override
void dispose() {
binding.removeObserver(this);
super.dispose();
}
}
I am encountering a problem, where localization works fine, but the applications needs to be restarted in order for the changes to propagate.
Orientation changes
I know about OrientationBuilder, which will call its builder whenever it detects a change in the device's orientation, which in e.g. Android would be considered as a configuration change, just like device language changes.
Language changes
Is there something like LanguageBuilder? I could not find anything on my own and not on flutter.io nor on pub. I have read this tutorial and know about Locale, but I do not see a Stream for Locale.
My problem is that changing the language in iOS and Android native is really smooth. It gets handled automatically and perfectly integrates with services like Firebase Remote Config.
I really wonder if there is some method that will allow me to refresh my localization.
Question
So I am asking how I can refresh my Remote Config when the device language changes.
No there's no Builder for Locale.
Instead, there's an InheritedWidget which you can subscribe to using Localizations.of.
Since it is an InheritedWidget, all widgets that call Localizations.of will automatically refresh on locale change.
EDIT :
A example on how to live reload text using Flutter Locale system :
Let's assume you have the following class that holds translations :
class MyData {
String title;
MyData({this.title});
}
You'd then have a LocalizationsDelegate that contains such data. A dumb implementation would be the following :
class MyLocale extends LocalizationsDelegate<MyData> {
MyData data;
MyLocale(this.data);
#override
bool isSupported(Locale locale) {
return true;
}
#override
Future<MyData> load(Locale locale) async {
return data;
}
#override
bool shouldReload(MyLocale old) {
return old.data != data;
}
}
To use it simply pass it to MaterialApp.localizationsDelegates (be sure to add flutter_localizations to your pubspec.yaml) :
LocalizationsDelegate myLocale = MyLocale(MyData(title: "Foo"));
...
MaterialApp(
localizationsDelegates: [
myLocale,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
You can then freely live reload your translations by replacing myLocale with a new MyLocale instance.
Here's a full example of a click counter app. But where the current count is instead stored inside Locale (because why not ?)
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class MyCount {
String count;
MyCount({this.count});
}
class MyCountLocale extends LocalizationsDelegate<MyCount> {
MyCount data;
MyCountLocale(this.data);
#override
bool isSupported(Locale locale) {
return true;
}
#override
Future<MyCount> load(Locale locale) async {
return data;
}
#override
bool shouldReload(MyCountLocale old) {
return old.data != data;
}
}
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ValueNotifier<int> count = ValueNotifier<int>(0);
LocalizationsDelegate myLocale;
#override
void initState() {
count.addListener(() {
setState(() {
myLocale = MyCountLocale(MyCount(count: count.value.toString()));
});
});
myLocale = MyCountLocale(MyCount(count: count.value.toString()));
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
myLocale,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: MyHomePage(count: count),
);
}
}
class MyHomePage extends StatefulWidget {
final ValueNotifier<int> count;
MyHomePage({this.count});
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
primary: true,
appBar: AppBar(),
body: Column(
children: <Widget>[
FloatingActionButton(
onPressed: () => widget.count.value++,
child: Icon(Icons.plus_one),
),
ListTile(
title: Text(Localizations.of<MyCount>(context, MyCount).count),
),
],
),
);
}
}
Device language changes can be detected using a WidgetsBindingObserver.
It is the simplest to use it with a StatefulWidget in your State (with WidgetsBindingObserver):
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
#override
void didChangeLocales(List<Locale> locale) {
// The device language was changed when this is called.
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
...
}
This means that you can now reload your RemoteConfig in didChangeLocales:
#override
void didChangeLocales(List<Locale> locale) {
_updateRemoteConfig();
}
Future<void> _updateRemoteConfig() async {
final remoteConfig = await RemoteConfig.instance;
await remoteConfig.activateFetched(); // This will apply the new locale.
}
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);
}
}