Flutter GetX why are controllers not auto-deleted when navigating away - flutter

Maybe I am misunderstanding something here, but I have followed the docs on https://pub.dev/packages/get to set up a controller for each of my pages.
e.g standard page stuff:
Pages:
PageIntro -> PageLogin -> PageHome etc.
Controllers:
- IntroController
- LoginController
- HomeController
I've defined each of my pages the same way, e.g.:
class PageIntro extends StatelessWidget {
#override
Widget build(BuildContext context) {
final IntroController c = Get.put(IntroController(context));
return Scaffold(
body: _buildContent(), // you get the idea.
);
}
}
I was expecting that when navigating away from PageIntro to PageLogin, that the IntroController would be deleted, but it's not.
I know I can manually delete controllers with:
Get.delete<IntroController>();
But I shouldn't have to.
Unless I'm missing something/doing something really stupid, this seems more hassle, more messy that just having PageIntro extend StatefulWidget, then doing whatever I need to do in the dispose override method as usual.
Please can someone more knowledgable on this inform me?
Thanks.

In your case, binding is the way to go.
On the other hand, controllers are removed when the route is removed from the stack. In your case, you maybe pushing to the next screen rather than replacing it.

I think it will help you to avoid this error. You should call in inside of build method.
#override
Widget build(BuildContext context) {
var controller = ControllerStateDispatcher<RecentScreenController>(
RecentScreenController(),true) // if you want to save state, false if you want reset state)
.get<RecentScreenController>();
}
abstract class Cleaner {
void clear();
}
abstract class GetControllerProvider<T extends GetxController> {
R get<R extends GetxController>();
}
class ControllerStateDispatcher<T extends GetxController> extends Cleaner
implements GetControllerProvider {
bool shouldSaveState;
T controller;
ControllerStateDispatcher(this.controller, [this.shouldSaveState = false]);
#override
void clear() {
Get.delete<T>();
}
#override
DispatcherController get<DispatcherController extends GetxController>() {
if (!shouldSaveState) clear();
controller = Get.put(controller);
return controller as DispatcherController;
}
}

Related

Flutter: How to trigger a bloc event in the controller?

In summary I want the "context.read().add(EnabledEvent())" to work from the controller itself or some other alternative I can use to trigger an event from the controller and update the state:
class SplashCtrl extends GetxController {
#override
void onReady() {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
Another thing I tried was this:
ExampleBloc testBloc = ExampleBloc();
testBloc.add(TestEvent());
in the controller, and it does seem to trigger the event, but the UI doesn't update with the state change. I seem to really need the Context for that to change. Any ideas?
MORE DETAILS AND CONTEXT:
I want to trigger bloc events not just in the page itself, but in the page's controller!
So I have pages in my flutter app with each view having a controller set up like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
SplashCtrl is the controller for this page. This splash view page is one of the first pages that loads it runs functions when it's added and ready (basically instantly), such as checking if the user is logged in and loading their data to the app, like this:
class SplashCtrl extends GetxController {
#override
void onReady() {
// run stuff here
}
I have been able to get away with creating lambda functions in the pages and triggering events based on what they do and toggle like this:
IconSwitchedButtonUi(
value: state.isExampleOn,
icon: Images.toggleIcon,
title: "Is Example enabled?",
onChanged: (value) {
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},
),
but now I need a bloc event to trigger and change the state a bit if a user has a certain value for one of their fields. How do I do that? How do I trigger an event from within the controller? The "context.read" doesn't work because the controller doesn't have context, or does it?
ExampleBloc testBloc = ExampleBloc(); testBloc.add(TestEvent());
This does not work because you were declaring totally new instance of bloc that wasnt connected by any BlocProvider to your widget tree, hence there is no way you could trigger this bloc events that would emit state your BlocBuilder could react to. Since there is no bloc provided to widget tree read() wont find this bloc.
In order for this work you need to:
Pass BuildContext that have some Bloc or Cubit provided by BlocProvider
void foo(BuildContext context) {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
..or listen to bloc changes directly inside controller
class SplashCtrl extends GetxController {
SplashCtrl(MyBloc bloc) {
bloc.stream.listen((event) {
// React to bloc changes here
if (event is MyBlocEvent) {
// Do something
}
}
}
}
Remember that even in this approach bloc instance given to SplashCrtl constructor must be the same that is provided by BlocProvider to a widget tree in order for this to work.
I was able to resolve this by adding a late BuildContext to my controller:
class SplashCtrl extends GetxController {
late BuildContext context;
#override
void onReady() {
// run stuff here
}
After that, in the page itself, I passed the context like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
controller.context = context;
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
Then I was able to use the context within the controller itself!!
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},

Which is better when using provider instance in a new widget?

Let's say I've written my code as below.
I've got a provider called SampleProvider, and I'm using it in my main widget.
class SampleProvider extends ChangeNotifier {}
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
SampleProvider provider = Provider.of<SampleProvider>(context);
}
}
And then, I want to make a new widget and use this provider in the new widget.
There will be two choices.
First, I just instantiate another provider in the new widget as below.
class NewWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
SampleProvider provider = Provider.of<SampleProvider>(context);
}
}
Or, I can send it from the main widget to the new widget as a constructor parameter.
Like this:
class NewWidget extends StatelessWidget {
final SampleProvider provider;
NewWidget(this.provider);
#override
Widget build(BuildContext context) {
}
}
I guess the first option is better because flutter draws a widget based on its build context, but I'm not sure.
I've googled it quite long, but there was no success.
Can anybody tell me whether I am right or wrong? Or Do they have no difference?
Prefer the first solution, it's easier to refactor.
Suppose you need move NewWidget in your widget tree, you also need to modify the "paramter pass" code if you choose second solution, which is not necessary with first solution.
One of Provider pacakage's purpose is avoid passing parameter deep in the widget tree by the way.
Depend on preference not like first or second one.
Have an exception when obtaining Providers inside initState. What can I do?
This exception happens because you're trying to listen to a provider from a life-cycle that will never ever be called again.
It means that you either should use another life-cycle (build), or explicitly specify that you do not care about updates.
As such, instead of:
initState() {
super.initState();
print(context.watch<Foo>().value);
}
you can do:
Value value;
Widget build(BuildContext context) {
final value = context.watch<Foo>.value;
if (value != this.value) {
this.value = value;
print(value);
}
}
which will print value whenever it changes (and only when it changes).
Alternatively, you can do:
initState() {
super.initState();
print(context.read<Foo>().value);
}
SRC: https://github.com/rrousselGit/provider#i-have-an-exception-when-obtaining-providers-inside-initstate-what-can-i-do
Yes, I believe the first option is the better way, of the top of my head I can't think of any situation in which you would prefer the second option to the first.
If you don't use new widget as children of any other widget , first choice is better .
otherwise , second is better .

Flutter and GetxController - How to manage state unreliability?

I currently have many problems in my app with the get package for Flutter (https://pub.dev/packages/get) and the following state scenario:
For example I have a GetxController UserController. I need this controller in different Widgets, so I initialize it with Get.put() in the first widget and for the other child widgets I'll call it with Get.find(). That works.
But: I have some widgets that sometimes load before the controller got initialized and sometimes after. So I get many "UsersController" not found errors. Maybe there exists some workaround for this problem?
You could initialize your UsersController class using a Bindings class, prior to the start of your app.
Example
class UsersController extends GetxController {
static UsersController get i => Get.find();
int userId = 5;
}
class UsersBinding extends Bindings {
#override
void dependencies() {
Get.put<UsersController>(UsersController());
}
}
void main() async {
UsersBinding().dependencies();
runApp(MyApp());
}
Done this way your UsersController is guaranteed to be available everywhere in your app, prior to any Widgets loading.
class MyHomePage extends StatelessWidget {
final UsersController uc = Get.find();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Bindings'),
),
body: Center(
child: Obx(() => Text('userId: ${uc.userId}')),
),
);
}
}
try to add
GetMaterialApp(
smartManagement: SmartManagement.keepFactory,
)
so that it can store factory of those instanse
or make sure add permanent
Get.put<Repo>(Repo(), permanent: true);
so that it never get deleted from memory
if u need to check class is initialized, u most keep the class in memory with permanent:trueand set tag:'anything you want for check this object' property in Get.put function, and now u can check
bool Get.isRegistered<className>(tag: 'TagInPut')
If there's no specific reason, you can just call Get.put(UserController()) at main function and wrap material app with GetMaterialApp.
if you are using get to provide an instance of something across your app
meaby https://pub.dev/packages/get_it could help you more. just be sure that you use allowReassignment==true so you can update your instance if you want save a full new controller, but if you want to use the same always get_it is perfect for you

Which way to access state in class's methods is correct?

I've watched Pragmatic state management video from google io19, about package:provider/provider.dart and its way to manage state. It looks pretty simple, but I have question about getting access to state in class's methods.
Say somewhere in class I need to update state:
_onTap(data) {
appState.data = data;
}
In class's build method I'm getting state:
this._appState = Provider.of<AppState>(context);
Now I need setter, so I'm doing:
set _appState(newValue) {
appState = newValue;
}
And in the end I need state field in my class:
class Tapable extends StatelessWidget {
var appState;
_onTap(data) {
appState.data = data;
}
set _appState(newValue) {
appState = newValue;
}
#override
Widget build(BuildContext context) {
this._appState = Provider.of<AppState>(context);
return SomeWidget(
onTap: () { _onTap(data) }
)
}
}
Surprisingly it works, but this code smells for me, so I doubt that this is the correct way.
Thanks.
If you have state, such that changing state should update your widget, you should use a StatefulWidget, and use setState() to trigger the rebuild. StatelessWidget is for widgets that are essentially "view only".

Controlling State from outside of a StatefulWidget

I'm trying to understand the best practice for controlling a StatefulWidget's state outside of that Widgets State.
I have the following interface defined.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:
When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this event and perform some action ( DB call, service request, etc ).
The controller can call showActivity or set message to have the view show progress with a message.
Because a Stateful Widget does not expose its State as a property, I don't know the best approach for accessing and modifying the State's attributes.
The way I would expect to use this would be something like this:
Widget createStartupPage() {
var page = new StartupPage();
page.onAppSelected.listen((app) {
page.showActivity = true;
//Do some work
page.showActivity = false;
});
}
I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.
Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability, and forward-thinking towards Flutter, we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.
There are multiple ways to interact with other stateful widgets.
1. findAncestorStateOfType
The first and most straightforward is through context.findAncestorStateOfType method.
Usually wrapped in a static method of the Stateful subclass like this :
class MyState extends StatefulWidget {
static of(BuildContext context, {bool root = false}) => root
? context.findRootAncestorStateOfType<_MyStateState>()
: context.findAncestorStateOfType<_MyStateState>();
#override
_MyStateState createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return Container();
}
}
This is how Navigator works for example.
Pro:
Easiest solution
Con:
Tempted to access State properties or manually call setState
Requires to expose State subclass
Don't use this method when you want to access a variable. As your widget may not reload when that variable change.
2. Listenable, Stream and/or InheritedWidget
Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.
In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.
This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :
First, we have an InheritedWidget that expose a count :
class Count extends InheritedWidget {
static of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Count>();
final int count;
Count({Key key, #required Widget child, #required this.count})
: assert(count != null),
super(key: key, child: child);
#override
bool updateShouldNotify(Count oldWidget) {
return this.count != oldWidget.count;
}
}
Then we have our State that instantiate this InheritedWidget
class _MyStateState extends State<MyState> {
int count = 0;
#override
Widget build(BuildContext context) {
return Count(
count: count,
child: Scaffold(
body: CountBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
),
),
);
}
}
Finally, we have our CountBody that fetch this exposed count
class CountBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text(Count.of(context).count.toString()),
);
}
}
Pros:
More performant than findAncestorStateOfType
Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
Automic reload of the children when the value change
Cons:
More boilerplate
Stream can be complicated
3. Notifications
Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.
An example of Notification would be :
class MyNotification extends Notification {
final String title;
const MyNotification({this.title});
}
To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.
MyNotification(title: "Foo")..dispatch(context)
Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.
Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onTitlePush,
child: Container(),
);
}
bool onTitlePush(MyNotification notification) {
print("New item ${notification.title}");
// true meaning processed, no following notification bubbling.
return true;
}
}
An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController
Pros:
Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
More than one widget can subscribe to that same notification
Prevents children from accessing unwanted State properties
Cons:
May not fit your use-case
Requires more boilerplate
You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:
class StartupPage extends StatefulWidget {
static StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<StartupPageState>());
#override
StartupPageState createState() => new StartupPageState();
}
class StartupPageState extends State<StartupPage> {
...
}
You can then access the state by calling StartupPage.of(context).doSomething();.
The caveat here is that you need to have a BuildContext with that page somewhere in its tree.
There is another common used approach to have access to State's properties/methods:
class StartupPage extends StatefulWidget {
StartupPage({Key key}) : super(key: key);
#override
StartupPageState createState() => StartupPageState();
}
// Make class public!
class StartupPageState extends State<StartupPage> {
int someStateProperty;
void someStateMethod() {}
}
// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();
// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();
I do:
class StartupPage extends StatefulWidget {
StartupPageState state;
#override
StartupPageState createState() {
this.state = new StartupPageState();
return this.state;
}
}
class DetectedAnimationState extends State<DetectedAnimation> {
And outside just startupPage.state
While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.
In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):
class StartupPage extends StatefulWidget {
static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
#override
_StartupPageState createState() => new _StartupPageState();
}
class _StartupPageState extends State<StartupPage> {
...
}
The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.
You can use eventify
This library provide mechanism to register for event notifications with emitter
or publisher and get notified in the event of an event.
You can do something like:
// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();
var controlNumber = 50;
List<Widget> buttonsGenerator() {
final List<Widget> buttons = new List<Widget>();
for (var i = 0; i < controlNumber; i++) {
widgets.add(new MaterialButton(
// Generate 10 Buttons afterwards
onPressed: () {
controlNumber = 10;
emitter.emit("updateButtonsList", null, "");
},
);
}
}
class AState extends State<ofYourWidget> {
#override
Widget build(BuildContext context) {
List<Widget> buttons_list = buttonsGenerator();
emitter.on('updateButtonsList', null, (event, event_context) {
setState(() {
buttons_list = buttonsGenerator();
});
});
}
...
}
I can't think of anything which can't be achieved by event driven programming. You are limitless!
"Freedom cannot be bestowed — it must be achieved."
- Elbert Hubbard
Have you considered lifting the state to the parent widget? It is a common, though less ideal than Redux, way to manage state in React as far as I know, and this repository shows how to apply the concept to a Flutter app.