flutter update value of variable inside ChangeNotifier - flutter

I have a notifier.dart file where I have declared some ChangeNotifiers. One of which is OpacityChangeNotifier.
OpacityChangeNotifier Class:
class OpacityChangeNotifier extends ChangeNotifier {
double _opacity = 1.0;
double get opacity => _opacity;
void changeOpacity(double providedOpacity) {
_opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(_opacity);
}
}
This is for my coloring app where I want the user to start with an opacity of 1.0. Then he/she can change it.
Here's the opacity_picker widget
final _opacityProvider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class OpacityPicker extends ConsumerWidget {
const OpacityPicker({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final opacityPicker = watch(_opacityProvider);
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
opacityPicker.changeOpacity(0.1);
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
opacityPicker.changeOpacity(0.5);
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
opacityPicker.changeOpacity(1.0);
},
icon: Icon(Icons.opacity, size: 20),
),
],
),
);
}
}
Now I want to use this opacity_picker inside another widget called menu_items. I've added a simple Icon button to test if the values(0.1,0.5,1.0) for opacity were getting updated or not.
IconButton(
onPressed: () {
opacity.printOpacity();
},
icon: Icon(Icons.dock_rounded),
)
But it seems the value is remaining the same which I provided as default: 1.0. Any solution on how to update the value I provided or any other way how I can change the opacity?

I'm not familiar with how you're instantiating ChangeNotifierProvider just above your OpacityPicker class. So I'm not sure if that's correct or not.
As Provider is built with InheritedWidget, any part of your app you wish to be "reactive" to changes of Provider state, needs to be a child underneath your ChangeNotifierProvider.
One way to ensure this is to wrap MyApp with your ChangeNotifierProvider. Therefore, your entire app is within the Provider's InheritedWidget scope. Remi (author of Provider) shows this in his example code:
void main() {
runApp(
/// Providers are above [MyApp] instead of inside it, so that tests
/// can use [MyApp] while mocking the providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
He's using MultiProvider but you can see that MyApp() is the child of Provider.
Are you doing something similar in your app?

Related

Update child from parent in Flutter

Currently, I have two sample files Parent.dart and Child.dart.
In Parent.dart file this is what the code is like:
Parent.dart file:
children:
[
isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> Navigator.push(context,MaterialPageRoute(builder: (context)=> Child(
isDisabled: isDisabled, function: ()=> function())),
]
function()
{
setState(()=> isDisabled = !isDisabled);
}
and in Child.dart the code is something like this:
children:
[
widget.isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> widget.function(),
]
I have some data being fetched from a server that is used to populate a list of cards inside listview.builder.
What I'm trying to do is inherent variables from the parent and use their value to update the child. Currently, if I run this parent does change, but the child doesn't until you navigate back from parent to child.
For a better context: Imagine a list of cards. Each has an add-to-list button. Now if you click on the card it goes to another screen "child.dart" where it gives you more details about the item on the card you clicked. Now if you click the add-to-list button on the child screen it should also update the parent.
I tried different ways of achieving this "UI synchrony" for a better user experience. But I didn't find a proper way to implement it.
Things I tried: Provider (but it updates all the items on the list instead of each instance.),
a "hacky" method of editing the data in the list on the client side and updating the widget based on that. (This technique does work, but ewwwww)
I'm not really sure to understand your question.
If your question is how trigger a function in parent from child screen, here is your answer.
I made a working example. I think you were really close.
Another option for state management is riverpod 2.0
Or you can pass value in Navigator.pop and trigger the function in parent.
Parent model
class Parent {
String title;
bool isDisabled = false;
Parent({required this.title});
}
Main.dart
import 'package:flutter/material.dart';
import 'parent.dart';
import 'ParentCard.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({super.key});
List<Parent> parentList = [Parent(title: 'Item 1'), Parent(title: 'Item 2')];
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: ListView.builder(
itemCount: parentList.length,
itemBuilder: (BuildContext context, int position) {
return ParentCard(title: parentList[position].title);
},
),
),
);
}
}
ParentCard.dart
import 'package:flutter/material.dart';
import 'child.dart';
class ParentCard extends StatefulWidget {
String title;
ParentCard({super.key, required this.title});
#override
State<ParentCard> createState() => _ParentCardState();
}
class _ParentCardState extends State<ParentCard> {
bool isDisabled = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Text(widget.title),
isDisabled
? Icon(Icons.public, color: Colors.green)
: Icon(Icons.public, color: Colors.black),
IconButton(
icon: Icon(Icons.plus_one),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ChildCard(isDisabled: isDisabled, handler: handler)),
),
)
],
);
}
handler() {
setState(() => isDisabled = !isDisabled);
}
}
** child.dart**
import 'package:flutter/material.dart';
class ChildCard extends StatefulWidget {
VoidCallback handler;
bool isDisabled;
ChildCard({super.key, required this.isDisabled, required this.handler});
#override
State<ChildCard> createState() => _ChildCardState();
}
class _ChildCardState extends State<ChildCard> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
const Text('child !'),
widget.isDisabled
? const Icon(Icons.public, color: Colors.green)
: const Icon(Icons.public, color: Colors.black),
ElevatedButton(
onPressed: () {
setState(() {
widget.isDisabled = !widget.isDisabled;
});
widget.handler();
},
child: const Text('click to trigger'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('pop it'),
)
]),
);
}
}

Is there a neat way to pop a page and schedule a callback to be called after navigation is complete?

I want to call Navigator.of(context).pop() one or several times and then run a callback after navigation has completed, but I have struggled to find a neat solution. I've put together an example app to illustrate the problem I'm having:
Screens A, B, and C all access a nullable value on the Model Provider
ScreenA can set value to a non-null value
ScreenB requires value to be non-null to build
ScreenC can set value to null and pop you back to ScreenA
When you press the button on ScreenC to go back to ScreenA, it navigates successfully (the app doesn't crash) but you throw an Error because it tries to build ScreenB after the first pop.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Model(),
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ScreenA(),
),
);
}
}
class Model extends ChangeNotifier {
int? value = 0;
Future<void> updateValue(int? newValue) async {
await Future.delayed(const Duration(milliseconds: 30));
value = newValue;
notifyListeners();
}
}
class ScreenA extends StatelessWidget {
const ScreenA({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
Text('ScreenA - value: ${context.watch<Model>().value}'),
ElevatedButton(
child: const Text('Set value'),
onPressed: () => context.read<Model>().updateValue(Random().nextInt(100)),
),
ElevatedButton(
child: const Text('Go to B'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenB(
nonNullValue: context.watch<Model>().value ?? (throw Error()),
),
),
),
),
],
),
);
}
}
class ScreenB extends StatelessWidget {
const ScreenB({Key? key, required this.nonNullValue}) : super(key: key);
final int nonNullValue;
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
Text('ScreenB - value: $nonNullValue'),
ElevatedButton(
child: const Text('Set value'),
onPressed: () => context.read<Model>().updateValue(Random().nextInt(100)),
),
ElevatedButton(
child: const Text('Go to C'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => const ScreenC(),
),
),
),
],
),
);
}
}
class ScreenC extends StatelessWidget {
const ScreenC({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
const Spacer(),
const Text('ScreenC'),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
context.read<Model>().updateValue(null);
},
child: const Text('Reset app')),
const Spacer(),
],
),
);
}
}
Widget centredScreenContent(List<Widget> widgets) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: widgets,
),
);
I've found two solutions, but neither feels neat:
Make ScreenB take a nullable value in its constructor, and have its build return something like value == null ? Container() : ActualContents(nonNullValue: value!). I don't like this though. If we know that in BAU use, ScreenB cannot be built while value == null, then we'd like to log an error if that happens in production so we can investigate the problem. We can't do this if our navigation back from ScreenC also hits this state though.
Add a sufficiently long delay to the callback so that it runs after the navigation is completed, e.g. in the example app, if you change Model.updateValue to have a 300ms delay, then it doesn't error. This also feels like an unpleasant solution, if the delay is too long we risk the app behaving sluggishly, if it's too short then we don't solve the problem at all.
I would make ScreenB(int? nullableParam) and handle the widget builder with additional assert nullableParam == null just to log the error.
But what i think the real solution you are looking for is context.read<Model>().value instead of watch - i can't think of a scenario where you want to page parameter depend on any listenable state
solution
ElevatedButton(
child: const Text('Go to B'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenB(
nonNullValue: context.read<Model>().value ?? (throw Error()),
This way the page after first build will not be rebuild with null when popped.
#Edit
I see 2 problems:
passing Listenable value to Page parameter
purposely setting value to null where other part of application purposely is not handling it
The first one can be solved with the solution above
The second you have to either assure the passed value will not be null on Navigator.pop() - the solution above does that. Or handle the null value in the ScreenB widget (as you suggested with conditional build)

Flutter riverpod Change notifier not updating value

I'm using riverpod to manage states of some variables in my app like opacity, stroke width and color for my coloring app.
Here's my opacity class inside notifier.dart:
class OpacityChangeNotifier extends ChangeNotifier {
OpacityChangeNotifier([this.opacity = 1.0]);
double opacity;
void changeOpacity(double providedOpacity) {
opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(opacity);
}
}
This is from my OpacityPicker.dart:
final _opacityProvider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class OpacityPicker extends ConsumerWidget {
const OpacityPicker({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(0.1);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(0.5);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 30),
),
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(1.0);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 40),
),
],
),
);
}
}
finally this is my menu_items.dart:
final _opacityChangeProvider =
ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class UtilityItems extends ConsumerWidget {
const UtilityItems({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final opacityNotifier = watch(_opacityChangeProvider);
return SingleChildScrollView(
child: Container(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
print(opacityNotifier.opacity);
},
icon: Icon(Icons.dock_rounded),
)
],
),
OpacityPicker(),
],
),
),
);
}
}
Everything is working fine inside OpacityPicker.dart. When I'm pressing the opacity button the selected opacity is getting printed. But when I'm pressing the Icons.dock_rounded in menu_items.dart shouldn't I get the updated value?
It's showing the default value for opacity which is 1.0
I've looked it up and all I got was some issues related to changeNotifier not working back in September 2020.
What am I missing here?
N.B: I've imported all the files correctly. And I want the value of opacity to change to the user selected one on pressed. So I need my menu_item.dart widget to know that.
Building off #puelo comment, you shouldn't be redefining your ChangeNotifierProvider. The way you have it is two isolated providers with two separate ChangeNotifiers that have no knowledge of eachother.
I would recommend making provider a static member of your ChangeNotifier like so:
class OpacityChangeNotifier extends ChangeNotifier {
OpacityChangeNotifier([this.opacity = 1.0]);
static final provider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
double opacity;
void changeOpacity(double providedOpacity) {
opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(opacity);
}
}
To access:
context.read(OpacityChangeNotifier.provider);
This helps to keep your imports clean as well as avoiding redundant naming and reduces the likelihood someone else working on the project would end up creating another provider for that notifier.
Use that provider instead of defining it twice and that should solve your primary issue.
You should also always use context.read inside function handlers like onPressed. In your menu_items.dart you should refactor as follows:
class UtilityItems extends ConsumerWidget {
const UtilityItems({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return SingleChildScrollView(
child: Container(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
final opacity = context.read(OpacityChangeNotifier.provider).opacity;
print(opacity);
},
icon: Icon(Icons.dock_rounded),
)
],
),
OpacityPicker(),
],
),
),
);
}
}

Managing state in Flutter using Provider

I'm trying to implement Provider state management on counter application to understand Provider's functionality better. I have added two buttons with respect to two different text widget. So, now whenever I click any of the two widget both the Text widgets get update and give same value. I want both the widgets independent to each other.
I have used ScopedModel already and got the desire result but now I want to try with provider.
Image Link : https://i.stack.imgur.com/ma3tR.png
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("====Home Page Rebuilt====");
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
],
)),
);
}
}
class CustomWidget extends StatelessWidget {
final String number;
const CustomWidget({Key key, this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
print("====Number Page Rebuilt====");
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return Text(
value.count.toString(),
style: Theme.of(context).textTheme.headline3,
);
},
),
FlatButton(
color: Colors.blue,
onPressed: () =>
Provider.of<CounterModel>(context, listen: false).increment(),
child: Text("Click"),
),
],
);
}
}
If you want them independent from each other, then you need to differentiate them somehow. I have a bit of a different style to implement the Provider and it hasn't failed me yet. Here is a complete example.
You should adapt your implementation to something like this:
Define your provider class that extends ChangeNotifier in a CounterProvider.dart file
import 'package:flutter/material.dart';
class CounterProvider extends ChangeNotifier {
/// You can either set an initial value here or use a UserProvider object
/// and call the setter to give it an initial value somewhere in your app, like in main.dart
int _counter = 0; // This will set the initial value of the counter to 0
int get counter => _counter;
set counter(int newValue) {
_counter = newValue;
/// MAKE SURE YOU NOTIFY LISTENERS IN YOUR SETTER
notifyListeners();
}
}
Wrap your app with a Provider Widget like so
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// don't forget to import it here too
import 'package:app/CounterProvider.dart';
void main() {
runApp(
MaterialApp(
initialRoute: '/root',
routes: {
'/root': (context) => MyApp(),
},
title: "Your App Title",
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// Makes data available to everything below it in the Widget tree
/// Basically the entire app.
ChangeNotifierProvider<CounterProvider>.value(value: CounterProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
Access and update data anywhere in the app
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// MAKE SURE TO IMPORT THE CounterProvider.dart file
import 'package:app/CounterProvider.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
CounterProvider counterProvider;
#override
Widget build(BuildContext context) {
/// LISTEN TO THE CHANGES / UPDATES IN THE PROVIDER
counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
_showCounterButton(1),
_showCounterButton(2),
],
),
),
);
}
Widget _showCounterButton(int i) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Text(
i == 1
? counterProvider.counter1.toString()
: counterProvider.counter2.toString(),
style: Theme.of(context).textTheme.headline3,
),
FlatButton(
color: Colors.blue,
onPressed: () {
/// UPDATE DATA IN THE PROVIDER. BECAUSE YOU're USING THE SETTER HERE,
/// THE LISTENERS WILL BE NOTIFIED AND UPDATE ACCORDINGLY
/// you can do this in any other file anywhere in the Widget tree, as long as
/// it it beneath the main.dart file where you defined the MultiProvider
i == 1
? counterProvider.counter1 += 1
: counterProvider.counter2 += 1;
setState(() {});
},
child: Text("Click"),
),
],
);
}
}
If you want, you can change the implementation a bit. If you have multiple counters, for multiple widgets, then just create more variables in the CounterProvider.dart file with separate setters and getters for each counter. Then, to display/update them properly, just use a switch case inside the _showCounterButton() method and inside the onPressed: (){ switch case here, before setState((){}); }.
Hope this helps and gives you a better understanding of how Provider works.

Are obs stream being closed automatically by GetxControllers?

I am using the following package https://pub.dev/packages/get. Do I need to close my .obs in the onClose of a GetxController? I can't find anything about this in the docs. And looking at my memory it appears that the are being destroyed automatically.
In my understanding of GetX + Flutter so far...
No, you shouldn't have to remove .obs in the close() method of GetxControllers. Disposal of observables from a Controller are done automatically when the Controller is removed from memory.
GetX disposes/removes GetxControllers (and their observables) when the widget in which they are contained are popped off the widget stack / removed from the widget tree (by default, but can be overridden).
You can see this in the override of dispose() methods of various Get widgets.
Here's a snippet of dispose() that's run when GetX widgets are popped/removed:
#override
void dispose() {
if (widget.dispose != null) widget.dispose(this);
if (isCreator || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
GetInstance().delete<T>(tag: widget.tag);
}
}
subs.cancel();
_observer.close();
controller = null;
isCreator = null;
super.dispose();
}
When you use Bindings or Get.to() you're using GetPageRoute's which do cleanup by Route names:
#override
void dispose() {
if (Get.smartManagement != SmartManagement.onlyBuilder) {
WidgetsBinding.instance.addPostFrameCallback((_) => GetInstance()
.removeDependencyByRoute("${settings?.name ?? routeName}"));
}
super.dispose();
}
Test App
Below is a test App you can copy/paste into Android Studio / VSCode and run to watch the debug or run window output for GETX lifecycle events.
GetX will log the creation & disposal of Controllers in and out of memory.
The app has a HomePage and 3 ChildPages using Get Controllers in 3 ways, all which remove itself from memory:
GetX / GetBuilder
Get.put
Bindings
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
// MyCounterBinding().dependencies(); // usually where Bindings happen
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Dispose Ex',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('GetX/Builder Child'),
onPressed: () => Get.to(ChildPage()),
),
RaisedButton(
child: Text('Get.put Child'),
onPressed: () => Get.to(ChildPutPage()),
),
RaisedButton(
child: Text('Binding Child'),
onPressed: () => Get.to(ChildBindPage()),
),
],
),
),
);
}
}
/// GETX / GETBUILDER
/// Creates Controller within the Get widgets
class ChildPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
GetX<ChildX>(
init: ChildX(),
builder: (cx) => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
),
GetBuilder<ChildX>(
init: ChildX(),
builder: (cx) => RaisedButton(
child: Text('Increment'),
onPressed: cx.inc,
),
),
],
),
),
);
}
}
/// GET.PUT
/// Creates Controller instance upon Build, usable anywhere within the widget build context
class ChildPutPage extends StatelessWidget {
//final ChildX cx = Get.put(ChildX()); // wrong place to put
// see https://github.com/jonataslaw/getx/issues/818#issuecomment-733652172
#override
Widget build(BuildContext context) {
final ChildX cx = Get.put(ChildX());
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
Obx(
() => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
),
RaisedButton(
child: Text('Increment'),
onPressed: cx.inc,
)
],
),
),
);
}
}
class MyCounterBinding extends Bindings {
#override
void dependencies() {
Get.lazyPut(() => ChildX(), fenix: true);
}
}
/// GET BINDINGS
/// Normally the MyCounterBinding().dependencies() call is done in main(),
/// making it available throughout the entire app.
/// A lazyPut Controller /w [fenix:true] will be created/removed/recreated as needed or
/// as specified by SmartManagement settings.
/// But to keep the Bindings from polluting the other examples, it's done within this
/// widget's build context (you wouldn't normally do this.)
class ChildBindPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
MyCounterBinding().dependencies(); // just for illustration/example
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
Obx(
() => Text('Counter: ${ChildX.i.counter}', style: TextStyle(fontSize: 20),),
),
RaisedButton(
child: Text('Increment'),
onPressed: ChildX.i.inc,
)
],
),
),
);
}
}
class ChildX extends GetxController {
static ChildX get i => Get.find();
RxInt counter = 0.obs;
void inc() => counter.value++;
}
Notes
Get.to vs. Navigator.push
When using Get.put() in a child widget be sure you're using Get.to() to navigate to that child rather than Flutter's built-in Navigator.push.
GetX wraps the destination widget in a GetPageRoute when using Get.to. This Route class will dispose of Controllers in this route when navigating away / popping the widget off the stack. If you use Navigator.push, GetX isn't involved and you won't get this automatic cleanup.
Navigator.push
onPressed: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => ChildPutPage())),
Get.to
onPressed: () => Get.to(ChildPutPage()),
Based from the code of the super implementation of onClose, by default it does nothing currently.
https://github.com/jonataslaw/getx/blob/7146b6a53c0648104e4f623385deaff055e0036a/lib/get_instance/src/lifecycle.dart#L56
And from the comments, it says:
/// Called before [onDelete] method. [onClose] might be used to
/// dispose resources used by the controller. Like closing events,
/// or streams before the controller is destroyed.
/// Or dispose objects that can potentially create some memory leaks,
/// like TextEditingControllers, AnimationControllers.
/// Might be useful as well to persist some data on disk.
void onClose() {}
from that I think you need to manually close your streams in YourController::onClose() override function.
It appears you can use obs safely when using GetWorkers. Run this code and you'll notice that when you click the buttons a few time there will only be one print per page switch.
void main(){
runApp(GetMaterialApp(home: TestWidget(),));
}
class TestWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('next'),
onPressed: () => Get.to<SomeWidget>(SomeWidget()),
),
);
}
}
class SomeWidget extends StatelessWidget {
RxBool isSubscribed = false.obs;
SomeWidget() {
ever(isSubscribed, (_) => print('test'));
}
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('back'),
onPressed: () {
isSubscribed.value = !isSubscribed.value;
Get.back();
},
),
);
}
}