Describe the bug
When a controller is initialized in a stateless widget. it is stored in Get Instance. Then we use GetX
The controller instance should be scooped to that widget.
For example, we already have GetX
Also, we need an option to get the same controller with a different instance.
To Reproduce
Run the sample code given below.
Press the Add button.
Press View Attach Button now.
Expected behavior
Two different Widgets with the same controller (Need separate instance for each widget using Get.put)
It triggers an update on both widgets (Attachment 1 and Attachment 2)
Flutter Version:
Flutter 1.17.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 8af6b2f038 (2 months ago) • 2020-06-30 12:53:55 -0700
Engine • revision ee76268252
Tools • Dart 2.8.4
Getx Version:
get: ^3.4.0
Describe on which device you found the bug:
Samsung J6+
Minimal reproduce code
class TwoController extends GetxController {
final attachments1 = <String>[].obs;
final attachments2 = <String>[].obs;
}
class PageOne extends StatelessWidget {
final controller = Get.put(TwoController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.yellow,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text("Attachment 1"),
AttachWidget(
attachments: controller.attachments1,
),
Text("Attachment 2"),
AttachWidget(
attachments: controller.attachments2,
),
],
),
)),
);
}
}
class AttachController extends GetxController {
final isButtonVisible = true.obs;
}
class AttachWidget extends StatelessWidget {
final RxList<String> attachments;
AttachWidget({
Key key,
this.attachments,
}) : super(key: key);
final _ = Get.put(AttachController());
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Obx(() {
if (_.isButtonVisible.value)
return RaisedButton(
onPressed: () {
attachments.add("${attachments.length + 1}Attach");
_.isButtonVisible.value = false;
},
child: Text("Add Attach"),
);
return SizedBox();
}),
RaisedButton(
onPressed: () => _.isButtonVisible.value = true,
child: Text("View Attach Button"),
),
Obx(() => ListView.builder(
itemCount: attachments.length,
shrinkWrap: true,
itemBuilder: (ctx, int index) => Row(
children: <Widget>[
Expanded(child: Text(attachments[index])),
IconButton(
icon: Icon(Icons.close),
onPressed: () => attachments.removeAt(index),
),
],
),
)),
],
);
}
}
Hi @manojeeva, I think I understand your point, but might not be a good idea to do it the way you did it in the provided example.
Having the controller as a final field of a StatelessWidget will "recreate" it every time that StatelessWidget rebuilds.
Although not a big issue, if you have final observables variables inside the controller, you will recreate them as well, even though the multiple calls to the same Get.put(instance) will not override the instance already in memory... it's like a waste of resources, and potential memory leaks. This is where lazyPut() shines, as the creation of the instance is delayed until you actually use it with Get.find().
Second, as you probably know, GetBuilder() and GetX() are actually there to help you in these cases, as they have they own "lifecycle", so using init: Controller(), global: false will assure you the Controller lifetime (onInit, onReady, onClose) as long as the Widget is "mounted" in tree (like if you wanna use internal GetxControllers for each item in your "AttachWidget" sample).
Third, Get.create(()=>Controller()) is a way to create a new controller every time you run Get.find(), seems like a weird usecase, but if you use it with class MyWidget extends GetWidget<Controller> , the controller will be cached and will be unique inside that Widget.
Fourth, regarding your example, did you try to use Get.put(Controller(), tag: "controller1") ? it's a way to have multiple instances stored with the same Type... like an "id".
So, let me know if that answer helps you.
Answering your sample...
in PageOne:
...
children: <Widget>[
Text("Attachment 1"),
AttachWidget(
attachments: controller.attachments1,
tag: 'attach1',
),
Text("Attachment 2"),
AttachWidget(
attachments: controller.attachments2,
tag: 'attach2',
),
],
...
in AttachWidget:
class AttachWidget extends StatelessWidget {
final RxList<String> attachments;
final String tag;
AttachWidget({
Key key,
this.attachments,
@required this.tag,
}) : super(key: key) {
Get.put(AttachController(), tag: tag);
}
get _ => Get.find<AttachController>(tag: tag);
@override
Widget build(BuildContext context) {
return Column(
...
This is perfect.
I was thinking to put some extra variables inside the controller and access it later.
With your solution now I can use the tag to access the controller's instance later.
I tried this approach before but, it didn't work. I should've done wrongly.
Also, I'm not sure where to initialize the controller. Now I can understand it.
One small doubt, you mentioned using the final variable inside the controller will recreate. I can see in the State Management doc, all variables are final here
Can you explain that a bit? Should I use the var keyword?
I'm like to learn more about getx.
class MyWidget extends GetWidget<Controller> This approach looks easy like binding and don't want to worry about initialization.
But is it possible to access the controller outside I mean with multiple instances pick the correct one using the tag?
How do we use the tag with this approach?
There is a tag variable in the GetWidget. I can able to override the tag. But Unable to access the controller outside.
Thanks.
You can't use a tag with GetWidget<Controller>, the widget is just a shortcut to have
get controller => Get.find<Controller>();. (My bad, someone pushed a PR these days)...
What I meant by final was the usage INSIDE a StatelessWidget... as they tend to be "recreated" often, your Controller's might be initialize it a lot of times (the constructor will run), and then will never be used (cause Get already has the first instance active), until you pop() the current screen, in which case, Get route management will dispose the active Controller mentioned before. So, if you can, always use final, generally ...
Maybe, if u don't feel comfortable using Get.put(), Get.lazyPut(), etc, don't overthink so much in terms of the controllers... sometimes is better to pass down references in constructors, mostly with Flutter Widgets. We have to work more on the documentation and show some uses cases of it, maybe is a little complex to understand, because of the auto memory management GetX has.
put() and lazyPut() is totally okay.
The problem is not sure where should I initialize the controller.
In a statelesswidget should I use
final controller =Get.put(Controller());
or
inside the constructor
MyWidget():super() {
Get.put(Controller())
}
And If I want to access the controller outside with different instances I need a tag, right?
And how we going to get if we use final controller =Get.put(Controller()); in statelesswidget ?
We must use the constructor method to put the tag right?
Please tell me which approach is best for the above use-case.
Just want to isolate widget functionality in a particular controller and don't care about what that widget is doing, and when we need something from that widget after navigating few screens we need to get the controller and access the data (will be multiple instance or controller and widget with the same type)
Using GetWidget<Controller> is Initializing the controller and I can straightaway access the controller like binding.
In GetWidget source
I can see a tag variable
while accessing the controller it is getting from the instance as you mentioned.
If possible we can use tag in GetWidget so we can pass the tag in the get controller to get a specific controller
T get controller {
if (_value.isEmpty) _value.add(GetInstance().find<T>());
return _value.first;
Just asking will there is an option.
Thanks for helping me out.
Sorry, is a little late here and Im exhausted, so I will try to be concise: if you can, join the Discord community (find link in README), and we can chat about it, with your particular use case.
I understand the versatility of having Global access to instances might sound great, but, can also be a headache if you abuse it.
There are no best practices at the moment to use Controllers, we will work on that for sure with the samples, as lots of devs gets confused.
So what happened in the Discord chat? :-) We want to know the best solution as well
Uhh...
Chat seems quite welcoming. Sadly, didn't get the solution that I've expected.
Especially chat time zone is not suitable for me. Most of the members come at night according to my TimeZone. Also, the message gets unnoticed.
Tried to figure out with different approaches, I've managed to get the functionality combined with the inherited widget.
Not sure it is correct but got it working.
I've found two use-cases one is asked in this thread. another one is using the controller internally by multiple widgets.
Will share the code later. Thanks.
class FormController extends GetxController {
final focusNodes = Map<String, dynamic>();
final _formKey = GlobalKey<FormState>();
final autoValidate = false.obs;
void storeInputData(String name, FocusNode focusNode) {
focusNodes[name] = focusNode;
}
// other methods
}
class _FormInheritedWidget extends InheritedWidget {
final String id;
_FormInheritedWidget({
Key key,
@required Widget child,
@required this.id,
}) : super(key: key, child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
class CForm extends StatelessWidget {
final List<Widget> children;
final String id;
CForm({
Key key,
@required this.children,
@required this.id,
}) : super(key: key) {
Get.put(FormController(), tag: id);
}
FormController get controller => Get.find(tag: id);
static String getFormID(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_FormInheritedWidget>()
.id;
}
@override
Widget build(BuildContext context) {
return _FormInheritedWidget(
id: id,
child: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
key: controller._formKey,
child: Column(
children: children,
),
),
);
}
}
class CTextFormField extends StatefulWidget {
final String labelText;
final List<Validation> validator;
final FormFieldValidator<String> customValidator;
final TextInputType keyboardType;
final bool obscureText;
//other fields
}
class _TextFieldBoxState extends State<CTextFormField> {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
FormController formCtrl;
@override
void didChangeDependencies() {
formCtrl = Get.find(tag: CForm.getFormID(context));
formCtrl.storeInputData(widget.name, focusNode);
super.didChangeDependencies();
}
void onEdittingCompleteCalled() {
if (widget.nextFocusName.isEmpty) {
focusNode.unfocus();
} else {
formCtrl.focusInput(widget.nextFocusName, context);
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
.....
);
}
As you can see using a form like React.
Holds all the form data and focus nodes.
Now TextFormField needs to get the correct FormController.
Used Inherited widget to access the form id from TextFormField.
Now setting nextFormFieldName will focus the next input without managing the focus node on each page.
formCtrl = Get.find(tag: CForm.getFormID(context));
Using this I can able to get the formCtrl inside TextFormField
Not sure this approach is good.
In future anyone looking for this use-case.
I've used vmodel (just another Getxcontroller) instead of InheritedWidget. I will share code a bit later.
I want to share a bit too.
got the original code and reading the replies I come up with the working solution.
Remember: with GetWidget or GetView you still need to worry about initialization. You still need to use Get.put()somewhere
GetWidget and GetView simpy use theGet.find()` to encounter an instance that were already declared before
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AttachmentsController extends GetxController {
var attachments = <String>[].obs;
final isButtonVisible = true.obs; // merge those two controllers into one,
}
class PageOne extends StatelessWidget {
final controller1 = Get.put(AttachmentsController(), tag: 'attachments 1'); //using tags to have different instances
final controller2 = Get.put(AttachmentsController(), tag: 'attachments 2');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.yellow,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text("Attachment 1"),
AttachWidget(
controller: controller1, // sending the whole controller instance instead of the list
),
Text("Attachment 2"),
AttachWidget(
controller: controller2,
),
],
),
),
),
);
}
}
class AttachWidget extends StatelessWidget {
final AttachmentsController controller;
AttachWidget({
Key key,
this.controller,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Obx(() {
// getting isButtonVisible from the same controller now
if (controller.isButtonVisible.value)
return RaisedButton(
onPressed: () {
controller.attachments
.add("${controller.attachments.length + 1}Attach");
controller.isButtonVisible.value = false;
},
child: Text("Add Attach"),
);
return SizedBox();
}),
RaisedButton(
onPressed: () => controller.isButtonVisible.value = true,
child: Text("View Attach Button"),
),
Obx(
() => ListView.builder(
itemCount: controller.attachments.length,
shrinkWrap: true,
itemBuilder: (ctx, int index) => Row(
children: <Widget>[
Expanded(child: Text(controller.attachments[index])),
IconButton(
icon: Icon(Icons.close),
onPressed: () => controller.attachments.removeAt(index),
),
],
),
),
),
],
);
}
}

I guess This is the correct approach.
We need to use the controller in the main widget and pass it to the child widget.
Now I understand much better-using controllers.
I've mentioned FormController In my previous message.
I'm looking forward to
@intraector has a different approach to this.
I can't get GetX working with tags. Every time it returns the same instance regardless the tag. Need some more time to research.
Appreciated.
Yes, I'm working on it too.
I Will post If I get something.
Sorry for delay.
Okay, so I've imlemented it like this.
```import 'package:Staffield/constants/app_colors.dart';
import 'package:Staffield/constants/app_text_styles.dart';
import 'package:Staffield/views/common/text_feild_handler/text_field_handler_num.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class TextFieldNum extends StatelessWidget {
TextFieldNum(this.tag);
final String tag;
@override
Widget build(BuildContext context) {
TextFieldHandlerNum handler = Get.find(tag: tag);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: handler.txtCtrl,
autofocus: handler.autofocus,
decoration: InputDecoration(
isDense: true,
labelText: handler.label,
labelStyle: AppTextStyles.dataChipLabel,
counter: SizedBox.shrink(),
hintText: handler.hint,
hintStyle: Theme.of(context).textTheme.caption,
),
textInputAction: handler.nextFocus == null ? TextInputAction.done : TextInputAction.next,
maxLines: 1,
keyboardType: TextInputType.number,
inputFormatters: handler.inputFormatters,
onChanged: (_) => handler.onChanged(),
focusNode: handler.focus,
),
GetBuilder
tag: tag,
builder: (vmodel) => AnimatedOpacity(
opacity: vmodel.showError,
duration: Duration(milliseconds: 300),
child: Container(
// color: Colors.amber,
height: 20,
child: Text(
vmodel.errorText,
style: TextStyle(color: AppColors.error, fontSize: 14.0),
),
)),
),
],
);
}
}
###### VModel:
```class VModelEditEntry extends GetxController {
VModelEditEntry(String uid) {
init(uid);
}
TextFieldHandlerNum interest;
TextFieldHandlerNum revenue;
TextFieldHandlerNum wage;
final _entriesRepo = Get.find<EntriesRepository>();
<...>
init(String uid) {
this.entry = uid == null ? Entry() : _entriesRepo.getEntry(uid);
interest = Get.put(
TextFieldHandlerNum(
label: 'INTEREST',
defaultValue:
uid == null ? '' : entry.interest?.toString()?.formatAsCurrency(decimals: 2)?.noDotZero,
onChanged: calcTotalAndNotify,
),
tag: Tags.interest.toString(),
);
revenue = Get.put(
TextFieldHandlerNum(
label: 'REVENUE',
defaultValue:
uid == null ? '' : entry.revenue?.toString()?.formatAsCurrency(decimals: 2)?.noDotZero,
onChanged: calcTotalAndNotify,
nextFocus: interest.focus,
),
tag: Tags.revenue.toString(),
);
wage = Get.put(
TextFieldHandlerNum(
label: 'WAGE',
defaultValue:
uid == null ? '' : entry.wage?.toString()?.formatAsCurrency(decimals: 2)?.noDotZero,
onChanged: calcTotalAndNotify,
nextFocus: revenue.focus,
),
tag: Tags.wage.toString(),
);
}
//-----------------------------------------
void calcTotalAndNotify([String _]) {
calcTotal();
update(['calc']);
}
//-----------------------------------------
void calcTotal() {
var result = CalcTotal(
revenue: revenue.value,
interest: interest.value,
wage: wage.value,
penalties: penalties,
);
_bonusAux = result.bonus;
entry.total = result.total;
_penaltiesTotal = result.penaltiesTotal;
}
//-----------------------------------------
void save() {
var results = <String>[wage.validate(), interest.validate(), revenue.validate()];
if (results.every((result) => result == null)) {
entry.timestamp = DateTime.now().millisecondsSinceEpoch;
entry.penalties = penalties.toList();
entry.wage = wage.value;
entry.revenue = revenue.value;
entry.interest = interest.value;
calcTotal();
_entriesRepo.addOrUpdate([entry]);
}
}
@override
FutureOr onClose() {
Get.delete<TextFieldHandlerNum>(tag: Tags.interest.toString());
Get.delete<TextFieldHandlerNum>(tag: Tags.revenue.toString());
Get.delete<TextFieldHandlerNum>(tag: Tags.wage.toString());
return super.onClose();
}
}
enum Tags { wage, revenue, interest }
```class ViewEditEntry extends StatelessWidget {
ViewEditEntry([this.entryUid]);
final String entryUid;
@override
Widget build(BuildContext context) {
return GetBuilder
init: VModelEditEntry(entryUid),
builder: (vmodel) {
return SafeArea(
child: Scaffold(
bottomNavigationBar: BottomNavigation(RoutesPaths.editEntry),
appBar: AppBar(title: Text('Entry')),
body: Container(
decoration: BoxDecoration(
gradient: FlutterGradients.cloudyApple(tileMode: TileMode.clamp),
),
child: Column(
children:
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children:
Expanded(
child: Form(
key: _formKey,
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 10.0),
children:
<...>
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: TextFieldNum(Tags.wage.toString()),
),
),
Expanded(flex: 1, child: SizedBox.shrink()),
],
),
SizedBox(height: 5.0),
Row(
children: <Widget>[
Flexible(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: TextFieldNum(Tags.revenue.toString()),
),
),
Flexible(
flex: 1,
child: TextFieldNum(Tags.interest.toString()),
),
],
),
SizedBox(height: 5.0),
<...>
Center(
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: OutlineButton(
child: Text('Cancel',
style: AppTextStyles.buttonLabelOutline),
onPressed: () => vmodel.goBack(context)),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('ОК'),
onPressed: () {
vmodel.save();
Navigator.of(context).pop();
}),
),
),
],
),
),
],
),
),
),
],
),
),
],
),
),
),
);
},
);
}
}
And I'd like to mention that @roipeker 's suggestion
> Maybe, if u don't feel comfortable using Get.put(), Get.lazyPut(), etc, don't overthink so much in terms of the controllers... sometimes is better to pass down references in constructors
isn't working for me. If I got it right:
```class ViewEditEntry extends StatelessWidget {
ViewEditEntry([this.controller]);
final VModelEditEntry controller;
@override
Widget build(BuildContext context) {
return GetBuilder<VModelEditEntry>(
init: controller,
builder: (vmodel) {
return ...
},
);
}
In this case GetBuilder won't update screen.
Most helpful comment
Hi @manojeeva, I think I understand your point, but might not be a good idea to do it the way you did it in the provided example.
Having the controller as a final field of a StatelessWidget will "recreate" it every time that
StatelessWidgetrebuilds.Although not a big issue, if you have
finalobservables variables inside the controller, you will recreate them as well, even though the multiple calls to the sameGet.put(instance)will not override the instance already in memory... it's like a waste of resources, and potential memory leaks. This is wherelazyPut()shines, as the creation of the instance is delayed until you actually use it withGet.find().Second, as you probably know,
GetBuilder()andGetX()are actually there to help you in these cases, as they have they own "lifecycle", so usinginit: Controller(), global: falsewill assure you the Controller lifetime (onInit, onReady, onClose) as long as the Widget is "mounted" in tree (like if you wanna use internal GetxControllers for each item in your "AttachWidget" sample).Third,
Get.create(()=>Controller())is a way to create a new controller every time you runGet.find(), seems like a weird usecase, but if you use it withclass MyWidget extends GetWidget<Controller>, the controller will be cached and will be unique inside that Widget.Fourth, regarding your example, did you try to use
Get.put(Controller(), tag: "controller1")? it's a way to have multiple instances stored with the sameType... like an "id".So, let me know if that answer helps you.
Answering your sample...
in PageOne:
in AttachWidget: