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

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.

Related

Dispose widget when navigating to new route

I have two screens in my app.
Screen A runs a computationally expensive operation while opened, and properly disposes by cancelling animations/subscriptions to the database when dispose() is called to prevent memory leaks.
From Screen A, you can open another screen (Screen B).
When I use Navigator.pushNamed, Screen A remains in memory, and dispose() is not called, even though Screen B is now shown.
Is there a way to force disposal of Screen A when it is not in view?
Example code where first route is never disposed:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
I know it's a bit late but I think you should override the deactivate method. Since we are changing the page we are not actually destroying it, that's why the dispose isn't being called.
If you'd like more information this page lists the lifecycle of the stateful widgets.
From the link:
'deactivate()' is called when State is removed from the tree, but it might be
reinserted before the current frame change is finished. This method exists basically
because State objects can be moved from one point in a tree to another.
call Navigator.pushReplacement when routing between first and second screen.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => FirstRoute()),
);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
Try this
In flutter new versions deactivate won't be called when you push a new widget on top of another widget. Also there is an open issue related to this topic on flutter github: https://github.com/flutter/flutter/issues/50147
The best way to handle this issue is to add RouteObserver<PageRoute> to your material app and override didPushNext and didPushNext functions.
There is a very helpful medium article related to this topic which you can find here: https://medium.com/koahealth/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea
As Article said create your own RouteAwareWidget, you can add these two call backs to the fields of the widget:
didPopNext
didPushNext
class RouteAwareWidget extends StatefulWidget {
final Widget child;
final VoidCallback? didPopNext;
final VoidCallback? didPushNext;
const RouteAwareWidget({
Key? key,
required this.child,
this.didPopNext,
this.didPushNext,
}) : super(key: key);
#override
State<RouteAwareWidget> createState() => RouteAwareWidgetState();
}
class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPush() {}
#override
void didPopNext() {
dPrint('didPopNext');
widget.didPopNext == null ? null : widget.didPopNext!();
super.didPopNext();
}
#override
void didPushNext() {
dPrint('didPushNext');
widget.didPushNext == null ? null : widget.didPushNext!();
super.didPushNext();
}
#override
Widget build(BuildContext context) => widget.child;
}
Create a global RouteObserver<PageRoute> and add it to your material app:
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
MaterialApp(
navigatorObservers: [routeObserver],
debugShowCheckedModeBanner: false,
routes: _routes,
)
then in your routs you should wrap your routes with RouteAwareWidget and add custom function you want:
final _routes = {
HomePage.routeName: (context) => RouteAwareWidget(
child: const HomePage(),
didPushNext: () => sl<CameraBloc>().add(Dispose()),
didPopNext: () => sl<CameraBloc>().add(Init()),
),
MyQuestions.routeName: (context) => const RouteAwareWidget(
child: MyQuestions(),
),
};
didPushNext will be called when you push a widget on top of HomePage and didPopNext will be called when you pop the last widget above HomePage.
With Navigator.pushReplacement(), if using MaterialPageRoute, then setting
maintainState:false
will ensure that dispose() is called.
A light weight solution for a single route case is using a callback function triggered from the SecondRoute.
Trigger the callback from the WidgetsBinding.instance.addPostFrameCallback() within the initState() on the SecondRoute
More information on WidgetsBinding and when they run can be found here: Flutter: SchedulerBinding vs WidgetsBinding.
WidgetsBinding & SchedulerBinding will be printed only once as we called it initState(), but it will be called when build method finished it’s rendering.
import 'package:flutter/material.dart';
class FirstRoute extends StatefulWidget {
const FirstRoute({super.key});
#override
State<FirstRoute> createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen A')),
body: Center(
child: TextButton(
child: const Text('Go to Screen B'),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => SecondRoute(_callbackFn),
),
);
_secondRouteDone();
},
),
),
);
}
_callbackFn() {
print("Widget B Loaded, Free up memory, dispose things, etc.");
}
_secondRouteDone() {
print("SecondRoute Popped, Reinstate controllers, etc.");
}
}
class SecondRoute extends StatefulWidget {
final Function() notifyIsMountedFn;
const SecondRoute(this.notifyIsMountedFn, {super.key});
#override
State<SecondRoute> createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
void initState() {
super.initState();
// Notify FirstRoute after paint
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.notifyIsMountedFn();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen B')),
);
}
}
Not only to call 'deactivate()' but also to use 'Navigator.pushReplacement()' for page moving is necessary. Not working if you are using 'Navigator.push()'.

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

Flutter: Where to add listeners in StatelessWidget?

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.

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

get height of a Widget using its GlobalKey in flutter

I am struggling getting the height of a Widget using its GlobalKey.
the function that is getting the height is called after the Layout is rendered to make sure the context is available but key.currentState and also key.currentContext still returns null.
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget{
#override
State<StatefulWidget> createState() => new TestPageState();
}
class TestPageState extends State<TestPage>{
final TestWidget testWidget = new TestWidget();
#override
initState() {
//calling the getHeight Function after the Layout is Rendered
WidgetsBinding.instance
.addPostFrameCallback((_) => getHeight());
super.initState();
}
void getHeight(){
final GlobalKey key = testWidget.key;
//returns null:
final State state = key.currentState;
//returns null:
final BuildContext context = key.currentContext;
//Error: The getter 'context' was called on null.
final RenderBox box = state.context.findRenderObject();
print(box.size.height);
print(context.size.height);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: testWidget,
);
}
}
class TestWidget extends StatefulWidget{
#override
Key get key => new GlobalKey<TestWidgetState>();
#override
State<StatefulWidget> createState() => new TestWidgetState();
}
class TestWidgetState extends State<TestWidget>{
#override
Widget build(BuildContext context) {
return new Container(
child: new Text("Test", style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),),
);
}
}
You need to assign that key to a widget using super in the widget constructor. Not add it as a field.
That Key also must be constant.
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => new TestPageState();
}
class TestPageState extends State<TestPage> {
final key = new GlobalKey<TestWidgetState>();
#override
initState() {
//calling the getHeight Function after the Layout is Rendered
WidgetsBinding.instance.addPostFrameCallback((_) => getHeight());
super.initState();
}
void getHeight() {
//returns null:
final State state = key.currentState;
//returns null:
final BuildContext context = key.currentContext;
//Error: The getter 'context' was called on null.
final RenderBox box = state.context.findRenderObject();
print(box.size.height);
print(context.size.height);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new TestWidget(key: key),
);
}
}
class TestWidget extends StatefulWidget {
TestWidget({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => new TestWidgetState();
}
class TestWidgetState extends State<TestWidget> {
#override
Widget build(BuildContext context) {
return new Container(
child: new Text(
"Test",
style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
),
);
}
}
Define your constructor like this:
const MyWidget(GlobalKey key) : super(key:key);.
The framework stores the BuildContext and State object in the Widget.key field which is passed into the object by constructor instead of an arbitary key field.
Sometimes better approach is to use LayoutBuilder. In this case the code looks like
return LayoutBuilder(
builder: (context, constraints) {
print('Available container sizes - ${constraints.maxWidth} - ${constraints.maxHeight}');
return Container(
child: new Text(
"Test",
style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
),
);
},
);
This approach gives info dynamically, often useful with animations.

Resources