First off, I'm not a fan of Getx. Although it's a good and popular library. If I were to rate it, I could give this library 100/100 for its magic, and 0/100 for its documentation! And That's why this article exists.
Getx includes a lot of features, I mean really a lot, but its documentation is very scattered and hard to read. This document aims to introduce its core ideas and provide some best practices.
But if you don't know anything about Getx, I still suggest you have a general understanding of it from the official documentation and some getting started videos. Then you can come back here.
Controllers are how Getx separates the UI from the business logic.
There are three ways to use GetxController:
GetBuilder
mode, the UI will update when an update()
function is triggered in the controller.Obx
mode, the UI will update automatically when the data source is updated.GetX
mode, the UI will update automatically when the data source is updated, and it provides more configurations than Obx
.GetBuilder
is the most efficient way to update UI in Getx. But it needs manually update. There are two key points here:
update()
to manually trigger UI update.GetBuilder
to render UI.// Create controller class and extends GetxController.
class CounterController extends GetxController {
int counter = 0;
void increment() {
counter++;
// use update() to update counter variable on UI when increment be called.
update();
}
}
// On your Stateless/Stateful class, use GetBuilder to update Text when increment be called
class CounterWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GetBuilder<CounterController>(
// You can initialize your controller here the first time.
// Don't use init in your other GetBuilders of same controller.
init: CounterController(),
builder: (_) => Text(
'clicks: ${_.counter}',
)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Get.find<CounterController>().increment();
}),
);
}
}
Obx
is very commonly used for it’s the easiest way to perform automatic UI updates. There are two key points here:
.obs
.Obx
to render UI.class CounterController extends GetxController{
// counter is type RxInt
// these options will also work:
// final counter = RxInt(0);
// final count = Rx<Int>(0);
// but using .obs just makes life easier
final counter = 0.obs;
void increase() => ++counter;
}
class CounterWidget extends StatelessWidget {
Widget build(BuildContext context) {
final controller = Get.put(CounterController);
return Scaffold(
body: Center(
// Use Obx to update Text() whenever count is changed.
child: Obx(() => Text(
'clicks: ${controller.counter}',
))
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: counter.increment,
);
}
}
Getx
is useful when you want to do injections, but it’s just an option and I haven’t seen very good use cases here, so be careful if you decide to use it.
class CounterController extends GetxController{
final counter = 0.obs;
void increase() => ++counter;
}
class CounterWidget extends StatelessWidget {
Widget build(BuildContext context) {
final controller = Get.put(CounterController);
return Scaffold(
body: Center(
// Use Obx to update Text() whenever count is changed.
child: GetX<Controller>(
builder: (_) => Text(
'clicks: ${controller.count}',
)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: counter.increment,
);
}
}
In simple terms, we must instantiate a controller before it can be used. But how should we do that? Actually, there are multiple ways.
Get.put
will instantiate a controller immediately, just remember, to have GetxController disposed when the widget is removed, we should use Get.put
inside the build()
method! Don’t worry that if the Get.put
will be called multiple times, build in a StatelessWidget will only be called once so it will be fine, which also means you shouldn’t use StatefulWidget with Getx.
Tips:
Get.put
with permanent: true
.Get.put
with tag
.Get.find
to get that controller instance.Unlike Get.put
, Get.lazyPut
will just put a builder, and the controller will be actually initialized by the first time when the widget Get.find
it.
Tips:
Get.lazyPut
is very useful with Bindings, you can see that in Bindings part of this document.Get.lazyPut
with fenix: true
.It’s similar to Get.lazyPut
but watch out:
Get.find()
, it runs the builder method and returns a new instance every time! It does not follow the singleton pattern.So be careful with this one!
Tips:
Get.create
works well with GetWidget because it will do cache instead of creating a new instance every time. So for a component used in multiple places, which has its own controller, Get.create + Getwidget can be very useful.They are just the replacement for writing “StatelessWidget + Get.find“, nothing more than that.
In the real-world case:
Get.put
or Get.lazyPut
.Get.create
. Because gives the same instance of the controller every time. Good for creating components that have their own controllers.class AwesomeController extends GetxController {
final String title = 'My Awesome View';
}
class AwesomeView extends GetView<AwesomeController> {
/// if you need you can pass the tag for
/// Get.find<AwesomeController>(tag:"myTag");
final String tag = "myTag";
AwesomeView({Key key}):super(key:key);
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
child: Text( controller.title ),
);
}
}
⚠️ If you try to use multiple controllers in a widget, GetView or GetWidget won’t help for they only accept one controller.
Get.put
will instantiate a controller immediately, so it should be used in the build method.Get.lazyPut
will put a builder, and the controller will initialize when Get.find is called the first time.Get.find
can't work without Get.put
or Get.lazyPut
.When using
GetMaterialApp
, allGetPages
and navigation methods (likeGet.to()
) have abinding
property that takes an instance of Bindings to manage the dependencies() (viaGet.put()
) for the Route you are opening.
So what are Bindings for? They are used to inject dependencies for the route you are opening. Let’s see how to use it.
Bindings are used with routes, there are two types of routes in Getx, Get.to()
and Get.toNamed()
.
Let’s create a custom binding:
class CounterBinding extends Bindings {
void dependencies() {
Get.lazyPut(() => CounterController());
}
}
Get.to(CounterPage(), binding: CounterBinding());
When we open CounterPage this way, dependencies
function in CounterBinding will be called, and then we can use Get.find
to get the controller in CounterPage.
// define the named route first, bindings should be set here.
GetPage(name: "/counter", page: () => CounterPage(), binding: CounterBinding());
// then you can call Get.toNamed somewhere to do the routing.
Get.toNamed("/counter");
It does the same work as above.
Actually, GetPage can accept a list of Bindings
, like this:
// ViewPager named route
GetPage(
name: "/viewpager",
page: () => ViewPagerPage(),
binding: ViewPagerBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]); // they will be excuted as well when route happens.
// use it
Get.toNamed("/viewpager");
So you must ask, what are PageA, PageB, and PageC?
In a real-world case, PageA, PageB, and PageC might be the Nested pages of ViewPager, these pages have their own bindings, but considering they are not loaded by the route directly, bindings allow us to do the work when we route to page ViewPager.
So why do we need bindings? In my opinion, they just provide us the possibility to do some work outside the “PageView” (that’s why we call it injection), considering the routes are usually defined in the same place, these bindings can be organized together as well, so they could be helpful to perform a clean code!