State management in Flutter is hot topic everyone likes to talk about. There are many contenders in this category, the likes of Provider, Riverpod , Redux, BLoC, MobX and so on. For this article however, we would be taking a look at the GetX package.

What is GetX?

GetX is a package that tackles State management, Route management and Dependency management in a simple, powerful and efficient manner. It also comes with helpers utilities for simplifying Internationalization, Theme management, making HTTP requests, Validation and so much more.

The highlights of GetX include the following;

Write less code:

Many other state management solutions for flutter have the bottle-neck of boiler-plate code; i.e. redundant code developers are forced to write over and over again. For example, StatefulWidgets require both the Widget and State class to perform even the most basic tasks. To reduce this hurdle, other state management solutions utilize code generation, a technique that generates the boiler-plate code for the developer.

With GetX however, you do not need to worry about boiler-plate or generated code. Simply extend the GetxController and manage your state like a boss 😎.

Performance & Efficiency:

GetX observables (reactive values) are built using the low latency GetValue and GetStream classes from GetX. Hence, there is no buffering and very low memory consumption as compared to StreamControllers. The GetX controllers also auto dispose themselves when they are longer in use. This makes for an efficient and performant application even on low-end devices.

Simplicity:

Another benefit to using GetX is the ease at which Reactive programming is accomplished; It’s as easy as setState. Simply create a variable and append .obs to make it observable;

var name = 'John Eblabor'.obs;

Then in your UI,when you want to show that value and update the screen whenever the values changes, simply do this:

Obx(() => Text("${controller.name}"));

Simple and short.

Managing State

GetX offers several methods of handling state from simple, single value ephemeral state to complex, app-wide state. Whichever you’d pick depends on your use case.

Ephemeral State Management:

GetX provides options to help you handle single value state in a simple and elegant manner. These can be used in cases where you simply need to toggle the obscureText property in a TextField to reveal/hide the value, changing the current index for a BottomNavigationBar, toggle the visibility of a Widget or change the child of a Container widget from Image.network to a CupertinoActivityIndicator based on a single (boolean) value.

ValueBuilder

This is a simplified version of a StatefulWidget. It aims to simplify verbosity of the writing StatefulWidget's that handle one single value.

To use the ValueBuilder Widget, simply create the ValueBuilder widget, give it a Type as shown below and give it an initialValue.

NB: If you ever use a ChangeNotifier or a StreamController as the value of the builder, the value is automatically disposed 😎


ValueBuilder<int>(
  initialValue: 0,
  builder: (value, onUpdate) {
    return BottomNavigationBar(
      items: const [
        /*..BottomNavigationBarItems go here*/
      ],
      onTap: (v) {
        onUpdate(v); // this calls setstate to update the selected index
      },
    );
  },
  // if you need to call something outside the builder method.
  onUpdate: (value) => print("Value updated: $value"),
  onDispose: () => print("Widget unmounted"),
)

ObxValue

This is similar to the ValueBuilder only that instead of a regular value like an int or String, you pass a Rx instance ( as discussed here ) and the widget updates automatically when the value changes.

ObxValue((data) {
  return Switch(
    value: data.value,
    onChanged: data, // Rx has a _callable_ function! 
    //You could also use (flag) => data.value = flag,
  );
},
  false.obs,
)

App State Management:

There are several ways to handle app-wide state using GetX, This kind of state refers to global data shared across several widgets or screens scattered all over your app.

Before we talk about this section; Let me introduce you to … 🥁 … GetxController

GetxController

GetX provides a neat way to separate your business logic from your View or UI elements. You can get around with not using StatefulWidgets by using GetxController since it has onInit and onClose methods which serve same functions as the initState and dispose in a StatefulWidget.

When your controller is created in memory, the onInit method is called immediately, and the onClose method is called when it is removed from memory.

There is another method, the onReady method, which is called soon after the widget has been rendered on the screen.

Let’s consider the following GetX controller;

class CounterController extends GetxController {
    @override
  void onInit() {
    // Write code you would otherwise write in an `initState` override of a stateful widget
    // for example, making an API call or instantiating animation controllers
    super.onInit(); 
  }
  
  @override
  void onClose() {
    // Write the code you would for the `dispose` override of a stateful widget
    // for example, cancel timers or disposing objects like extEditingControllers, 
    // AnimationControllers to avoid memory leaks 
    super.onInit();
  }
  
  @override
  void onReady() {
    // Write the code to run when you UI is built and ready. Technically, this is 
    // called 1 frame after onInit(); It's a place to write run events, 
    // like snackbars, dialogs or even also make async request.
    super.onReady();
  }
}

NB: All the overrides above are optional

Reactive Variables

To make the GetxController useful, we need to create observable/reactive variables. These variables help to notify other widgets, listening to them, of value changes so they ( the listening widgets ) can re-render.

There are several ways to create observable variables in GetX;

1. GetX Helpers

GetX provides helpers for dart primitive types to easily create observables. These include the non-nullable RxInt , RxBool, RxString, RxList RxMap and their nullable counterparts RxnInt, RxnBool, RxnString etc.

Example;

final counter = RxInt(0);
final isName = RxnString(null);

2. Using Rx and Dart Generics

Asides the helpers GetX provides, you can create an observable using the generic type of the variable. This is the method you would likely use for your custom types or classes.

Example;

final counter = Rx<int>(0);
final isName = Rx<String?>(null);
final item = Rx<MyCustomClass>(MyCustomClass(id:2));

3. Shorthand

Finally, GetX provides a short form for creating observables, simply append .obs to the variable. Voila!

Example;

final counter = 0.obs;
final isName = ''.obs;
final item = MyCustomClass(id:2).obs;

Whatever method you use is fine.

NB: Quick Note on Observable or Reactive variables


final name = 'John Eblabor'.obs;

//to change the value of name, we use the following line of code
name.value = 'Hey';

//NB: name only "updates" the stream, if the new value is different from the current one.

// All Rx properties are "callable" and returns the new value.
// but this approach does not accepts `null`, the UI will not rebuild.
name('Hello');

// is like a getter, prints 'Hello'.
name() ;

final abc = [0,1,2].obs;
// Converts the value to a json Array, prints RxList
// Json is supported by all Rx types!
print('json: ${jsonEncode(abc)}, type: ${abc.runtimeType}');

// RxMap, RxList and RxSet are special Rx types, that extends their native types.
// but you can work with a List as a regular list, although is reactive!
abc.add(12); // pushes 12 to the list, and UPDATES the stream.
abc[3]; // like Lists, reads the index 3.


/// Custom Rx Models:
// toJson(), toString() are deferred to the child, 
// so you can implement override on them, and print() the observable directly.

class User {
    String name, last;
    int age;
    User({this.name, this.last, this.age});

    @override
    String toString() => '$name $last, $age years old';
}

final user = User(name: 'John', last: 'Doe', age: 33).obs;

// `user` is "reactive", but the properties inside ARE NOT!
// So, if we change some variable inside of it...
user.value.name = 'Roi';
// The widget will not rebuild!,
// `Rx` don't have any clue when you change something inside user.
// So, for custom classes, we need to manually "notify" the change.
user.refresh();

// or we can use the `update()` method!
user.update((value){
  value.name='Roi';
});

print( user );

Using observable variables

The following code as the MyController for the default Flutter Counter App that is created when you run flutter create <project-name>.

class MyController extends GetxController {
  
  // Create a variable that can notify other widgets when its value changes
  // by simply appending `.obs` to the value 
  final count = 0.obs;
  
  void increment(){
    // Note the `.value` on the count variable as mentioned above
    count.value += 1; // count is of type Rx<int>  
  }
  void decrement(){
    count.value -= 1;
  }  
}

To utilize our controller, we can either use the GetBuilder or the Obx Widgets which listen to changes from our observable variables and update their children.

The Obx is very useful when using GetX’s Dependency Manager alongside the State Management or when the controller is already instantiated.

GetBuilder

The following example is how the to re-render the UI using GetBuilder

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        GetBuilder<MyController>(
          init: MyController(),
          /* initialize MyController if you use 
          it first time in your views */
          builder: (controller) {
            return IconButton(
              icon: const Icon(Icons.add),
              onPressed: controller.increment,
            );
          },
        ),
        GetBuilder<MyController>(
          /* No need to initialize MyController again here, since it is 
          already initialized in the previous GetBuilder */
          builder: (controller) {
            return Text('${controller.count.value}');
          },
        ),
        GetBuilder<MyController>(
          builder: (controller) {
            return IconButton(
              icon: const Icon(Icons.remove),
              onPressed: controller.decrement,
            );
          },
        ),
      ],
    );
  }
}

Obx

Obx implementation


class Counter extends StatelessWidget {

// Controller needs to be initialized before use with Obx
final _ = MyController(); 

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: _.increment,
        ),
        Obx(()=> Text('${_.count.value}')),
        IconButton(
          icon: const Icon(Icons.remove),
          onPressed: _.decrement,
        ),
      ],
    );
  }
}

StateMixin

GetX also provides a StateMixin class that helps clean up asynchronous task representation.

Say we are building an application that needs to fetch data from the backend. We might need a way to easily track the state (loading, error, success) and the data returned from the server in our controller and maybe use if-else statements to determine what UI to render on the screen.

GetX’s also got us covered with the StateMixin. This helps us keep track of our status at any point in the app using a clean API. You simply need to mix StateMixin into your controller by using the with keyword. for example;

class DataController extends GetxController with StateMixin<ServerData> {}

StateMixin also provides the RxStatus class to help keep track of the state of the controller at any point and also provides the change method to change the RxStatus. All you need to do is pass the new data and the status as shown below;

change(newState, status: RxStatus.success());

Checkout the code below

class DataController extends GetxController with StateMixin<ServerData>{

    @override
    void onInit() {
        fetchProducts();
        super.onInit();
    }


    // You can  fetch data from remote server
    void fetchProducts() async {
        // Show the loading indicator
        change(null, status: RxStatus.loading());   
        final response = await fetchDataFromRemoteServer(); 

        If(response.hasData) {
            final data = ServerData.fromMap(response.data);
            // Successfully fetched products data
            change(data, status: RxStatus.success());     

          } else if(response.hasError) {
              // Error occurred while fetching data
          change(null, status: RxStatus.error('Something went wrong')); 

          } else {
              // No products data
          change(null, status: RxStatus.empty());
        }
    }
}

To utilize this controller, simply use the obx Widget from the GetxController class in your view;


class RemoteDataPage extends StatelessWidget {

    final controller = DataController();

    @override 
    Widget build(BuildContext context) {
        return Scaffold(
            body: controller.obx(

                (state) => ShowProductList(state),

                onLoading: AppLoader(),

                onEmpty: Text('No Data'),

                onError: (error) => Text(error),

            )
        );
    }
}

The controller.obx widget will update your UI according to the value of the status and the data. 🐱‍🏍

Conclusion

This article does not cover the entirety of GetX. It is simply aimed at providing a quick overview of how GetX makes you a happier developer with its unique approach to State Management.

Watch out for the next article on Dependency Management, Route Management & Helpers in GetX.

You can read more about GetX from official documentation.