iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💜

[Flutter] The GetX Ecosystem: State Management (Translated)

に公開

This article is a Japanese translation of this article. It is translated with the permission of the author, Aachman Garg.

I decided to translate this because I felt there was too little information about GetX in Japanese compared to its popularity overseas. I hope those looking for state management options beyond Provider or Riverpod find this helpful.

Click here for the second part
https://zenn.dev/inari_sushio/articles/3ce3494f37166c


Cover

Flutter is truly amazing. Among the many frameworks available, I believe it is the best choice for building cross-platform apps speedily without compromising on features or performance. It also passes the test when it comes to Developer Experience (DX).

Of course, I wouldn't call it perfect. I feel there are still many areas with room for improvement.

For example, the boilerplate code you have to write with the BLoC pattern, the time it takes for code generation when introducing MobX, the long syntax you write when utilizing Navigator or MediaQuery, and so on. These little wastes of time can be a cause for degrading the developer experience.

What is GetX?

GetX is a micro-framework that minimizes boilerplate code with its simple approach and easy-to-understand syntax. Development with GetX can be summed up in two words: simple and powerful.

It provides solutions not only for state management but also for dependency injection and route management, and it offers many other utilities to make development easier (such as internationalization, theme switching, and validation functions).

You might feel anxious about performance due to the many features. However, please be assured that GetX is an integrated package that is broken down into individual packages for each feature, allowing developers to freely choose which ones to include or exclude.

That said, once you start using GetX, I think you'll want to try its other features besides state management. Using them together feels comfortable, as if the features are resonating with each other. I call this the "GetX Ecosystem."

Why GetX?

  • Simple Syntax
    For instance, navigating to another page. No context or builder is needed, and you don't need to be aware of routes. You just write Get.to(SomePage()).

  • Performance
    Usually, developers have to choose which controllers to dispose() of and write code to handle that, but with GetX, the thinking is reversed. Developers choose which controllers to keep in memory (everything else is automatically cleared). This reduces both the amount of code and memory usage.

  • Decoupling View and Logic
    GetX does not depend on context, making it easy to decouple. Even dependency objects can be decoupled from Widgets by using a class called Bindings.

  • Centralized Management of Features
    Managing many packages to use a wide variety of features is exhausting. If even one package has a breaking change, it can be a nightmare. By centralizing management to some extent through the adoption of GetX, you can reduce worries about compatibility and maintenance.

Which State Management Should I Use? 🤯

State management is still a hot topic in the Flutter community. With so many methods and packages to choose from, it's enough to intimidate beginners. Each has its pros and cons, which makes it confusing.

In any case, it's best to choose what you feel comfortable using, but I hope this article encourages you to add GetX to your options.

Now, let's take a look at what GetX is actually like.

GetMaterialApp

Step 0: Replace MaterialApp with GetMaterialApp.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(); // Instead of MaterialApp
  }
}

GetxController

Controller classes aggregate variables and methods related to business logic, and functionality is achieved by accessing them from the View.

GetX provides a dedicated class for this controller called GetxController. GetxController inherits from the DisposableInterface class.

This means that as soon as a Widget is removed from the navigation stack, the controller is instantly removed from memory. There is nothing in GetX that you need to dispose() of explicitly.

GetxController also has methods like onInit() and onClose(). These serve as replacements for StatefulWidget's initState() / dispose(), making it possible to build an entire app using only StatelessWidget.

class Controller extends GetxController {
  @override
  void onInit() { // Executed as soon as memory is allocated to the widget
    fetchApi();
    super.onInit();
  }

  @override
  void onReady() { // Executed as soon as the widget is rendered
    showIntroDialog();
    super.onReady();
  }

  @override
  void onClose() { // Executed just before the controller is removed from memory
    closeStream();
    super.onClose();
  }

}

3 Approaches

Approach with GetBuilder

By wrapping other Widgets with GetBuilder, you can access the controller's variables and methods, call functions, and listen for state changes.

First, let's create a controller.

class Controller extends GetxController {
  int counter = 0;

  void increment() {
    counter++;
    update(); // Pay attention!
  }
}

When using GetBuilder on the UI side, the update() method is mandatory to notify the widget of changes. This is the same principle as ChangeNotifier's notifyListeners().

Then, configure GetBuilder on the UI side.

view-side
GetBuilder<Controller>( // Specify the controller type
  init: Controller(), // Initialize the controller
  builder: (value) => Text(
    '${value.counter}', // value is an instance of Controller
  ),
),
GetBuilder<Controller>( // No need to re-init once initialized
  builder: (value) => Text(
    '${value.counter}', // Updates when increment() is called
  ),
),

Initialization of the controller only needs to be done when using it for the first time in GetBuilder; there is no need to re-initialize it when using the same controller in other GetBuilders. If the type is specified, other GetBuilders will automatically share the state of the first GetBuilder. This is true regardless of where the GetBuilder is placed in the app.

Basically, think of GetBuilder as a replacement for StatefulWidget. By making good use of GetBuilder, you can even make all your views Stateless. GetBuilder is optimal for managing non-persistent state while keeping your code clean.

Also, by assigning a unique ID to GetBuilder, you can limit which Builders are notified.

GetBuilder<Controller>(
  id: 'aVeryUniqueID', // Assign ID here
  init: Controller(),
  builder: (value) => Text(
    '${value.counter}', // This will change
  ),
),
GetBuilder<Controller>(
  id: 'someOtherID', // Assign ID here
  init: Controller(),
  builder: (value) => Text(
    '${value.counter}', // This will not change
  ),
),

class Controller extends GetxController {
  int counter = 0;

  void increment() {
    counter++;
    update(['aVeryUniqueID']); // Specify ID here
  }
}

Approach with GetX (Class)

GetBuilder is fast and has a low memory footprint, but it is not reactive. Therefore, GetX also provides a GetX class separate from GetBuilder.

The GetX class has a similar syntax to GetBuilder, but the approach is Stream-based.

First, let's create a GetxController.

class Controller extends GetxController {
  var counter = 0.obs; // Pay attention!

  void increment() => counter.value++;
}

Instead of using update(), this time we append .obs after the variable. By appending .obs, any variable becomes a stream and monitorable. "obs" stands for observable.

In this counter example, think of it as an int becoming a Stream<int> (actually, it becomes an RxInt). Now you can monitor changes from the view side using the GetX class.

GetX<Controller>(
  init: Controller(),
  builder: (val) => Text(
    '${val.counter.value}',
  ),
),

GetX<Controller> is basically a StreamBuilder with boilerplate code removed.

Since the type of counter is RxInt, you use .value to actually extract the value. This is necessary for all variables in the view side that were marked with .obs in the controller side.

So, what if you want to monitor the class object itself? Actually, it's the same: just append .obs. Let's try that in the next section.

Approach with Obx

This is my personal favorite among the three approaches. I think the syntax is the simplest and easiest to adopt. Usage is just wrapping the widget with Obx(() => ).

class User {
  String name;
  User({this.name});
}

class Controller extends GetxController {	
  var user = User(name: "Aachman").obs; // Same as other variables
	
  void changeName() => user.value.name = "Garg"; // Access class variables via .value
}
view-side
Obx(() => Text(
  '${controller.user.value.name}'
  ),
),

Syntax similar to setState. Initialize the controller like this:

class PageOne extends StatelessWidget {
  Controller controller = Get.put(Controller());
  // Instead of controller = Controller()
}

By calling put(), the controller is placed in the route. This allows multiple widgets to access the same controller.

Now, let's use the same controller instance in another class.

class PageSeven extends StatelessWidget {
  Controller controller = Get.find();
}

find() will automatically search for an instance of the same type that was used before, from anywhere in the tree.

With this, the User changed in PageSeven is now reflected in PageOne as well.

So, Which Should You Use?

Case for using GetBuilder

  • When managing non-persistent state. This is almost the same situation as using setState.
  • When prioritizing performance. State is shared between Builders and does not consume much RAM.
  • When you don't want to deal with streams.

Case for using GetX

  • When you want to manage state reactively.
  • When you want to redraw the widget only when the value actually changes. For example, if a variable changes from "Garg" to "Garg", it will not be redrawn.
  • When you don't want to use Get.put().

Case for using Obx

  • When you prefer simple syntax.
  • When you need to handle multiple controllers within the same widget. Obx doesn't require specifying a type, so such usage is possible.
Obx(() => Text(
  '${firstController.counter.value + thirtySeventhController.counter.value}'
  ),
),
  • Always use Obx when managing dependent objects bundled using Bindings (more on this in the next article).

MixinBuilder

Wait, there's more?!
Yes, there is. As the name suggests, MixinBuilder mixes GetBuilder and Obx, which are two of the three approaches. By using MixinBuilder, you can use "Aachman" and "Aachman".obs in the same widget.

Isn't that amazing? Why didn't I bring it up above? There are two reasons:

  • Since it's a resource-heavy approach, it can affect performance in apps of a certain scale.
  • Because it is assumed to be used only in special cases that you might never encounter.

I'll introduce it just in case.

class Controller extends GetxController {
  int one = 1; // Just an int variable

  void incrementOne() {
    one++;
    update();
  }

  var two = 2.obs; // Reactive RxInt

  void incrementTwo() => two.value++;
}
view-side
  MixinBuilder(
    builder: (controller) => Text(
      '${controller.one + controller.two.value}'
    ),
  ),

My Usage

As for how I use GetX, I almost always use Obx combined with Bindings. Because with Obx, you can combine multiple controllers.

For other simple widgets or cases where there is no need to persist state, I use GetBuilder.

For example, changing the color when a button is pressed. In this case, using streams would be overkill, and if you were to choose normally, it would be setState. However, if you want to avoid using StatefulWidget and want to separate business logic from the view, GetBuilder is perfect for this purpose.

That's it

I've touched on state management methods using GetX. Among state management packages, it is personally my favorite. I hope GetX and this article lead to improvements in your developer experience (DX).

Part 2
https://zenn.dev/inari_sushio/articles/3ce3494f37166c

Discussion