LWDW!

Learn the work from doing the work🍺

Getx practice - controllers and bindings
Posted on 2023-03-21

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.

Description

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.

Controller

What are controllers for?

Controllers are how Getx separates the UI from the business logic.

Usage

There are three ways to use GetxController:

  1. GetBuilder mode, the UI will update when an update() function is triggered in the controller.
  2. Obx mode, the UI will update automatically when the data source is updated.
  3. GetX mode, the UI will update automatically when the data source is updated, and it provides more configurations than Obx.

GetBuilder

GetBuilder is the most efficient way to update UI in Getx. But it needs manually update. There are two key points here:

  1. call update() to manually trigger UI update.
  2. use 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

Obx is very commonly used for it’s the easiest way to perform automatic UI updates. There are two key points here:

  1. variables should be defined with .obs.
  2. use 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

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

How to get the controller in a widget?

In simple terms, we must instantiate a controller before it can be used. But how should we do that? Actually, there are multiple ways.

1. use Get.put

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:

  • If you want to keep the controller in memory and persist it, use Get.put with permanent: true.
  • If you want to create multiple records of the same controller type, use Get.put with tag.
  • If the controller exists in your widget tree, you can use Get.find to get that controller instance.

2. use Get.lazyPut

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:

  • In a real-world case, Get.lazyPut is very useful with Bindings, you can see that in Bindings part of this document.
  • If you want to keep the controller in memory and persist it, use Get.lazyPut with fenix: true.

3. use Get.create

It’s similar to Get.lazyPut but watch out:

  1. It doesn’t immediately create the instance. So it’s kinda like lazyPut.
  2. Every time you use 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.

Q&A: what are GetView and GetWidget?

They are just the replacement for writing “StatelessWidget + Get.find“, nothing more than that.

In the real-world case:

  • GetView is used with Get.put or Get.lazyPut.
  • GetWidget is very useful when used in combination with 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.

Conclusion

  1. Get.put will instantiate a controller immediately, so it should be used in the build method.
  2. Get.lazyPut will put a builder, and the controller will initialize when Get.find is called the first time.
  3. Get.find can't work without Get.put or Get.lazyPut.
  4. GetWidget and GetView work the same as “Get.find + StatelessWidget“.
  5. GetView for pages, GetWidget for components.

Bindings

What are bindings?

When using GetMaterialApp, all GetPages and navigation methods (like Get.to()) have a binding property that takes an instance of Bindings to manage the dependencies() (via Get.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.

Usage

Bindings are used with routes, there are two types of routes in Getx, Get.to() and Get.toNamed().

Create a custom Binding

Let’s create a custom binding:

class CounterBinding extends Bindings {
  
  void dependencies() {
    Get.lazyPut(() => CounterController());
  }
}

Use bindings in Get.to()

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.

Use bindings in Get.toNamed()

// 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.

Conclusion

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!

More to Read