Flutter State Management With GetX

Flutter State Management With GetX

State Management

State management is crucial when developing Flutter applications. In a typical Flutter application, a single screen can contain many different states, which are constantly changing as a user interacts with the application. It could be something as simple as toggling a Switch in the settings page or as complex as displaying an HTTP response.

Flutter provides the StatefulWidget, which allows an application's state to be updated using the setState method. This is especially useful in simple applications but as the complexity of an application grows, its low-level nature could make managing the application state quite challenging.

Flutter has many popular packages that help simplify state management. Some of these include Provider, BLoC, Riverpod, and GetX. Choosing which one to use depends on your needs.

In this article, we'll be looking at GetX, which is coincidentally the most liked package on pub.dev at the time this article was published. GetX requires minimal setup and does much of the heavy lifting, allowing you to focus on writing your core application's business logic.

GetX

GetX is a simplified Flutter state management solution, which relies on reactive programming. Some of the benefits of using GetX include the following:

  • It rebuilds only selected widgets.

  • BuildContext use is not required.

  • Stateful widget use is not required.

  • Code generation is not needed.

  • Decoupling of presentation and business logic layers.

  • Auto cleanup of resources.

When using GetX for state management, you're generally concerned with two components:

  • Controllers: This is where the state of the application is stored and managed. This is where your business logic goes. A controller is generally created for each feature.

  • GetX Views/widgets: These are specialized widgets that listen to and react to changes in the controller.

GetX provides the following two state managers:

  • Simple State Manager (GetBuilder)

  • Reactive State Manager (GetX/Obx)

GetBuilder vs Obx

GetBuilder is a GetX widget that wraps around a widget holding a state via its builder function. It rebuilds to reflect a new state when the update function is called in the controller class, quite similar to setState of StatefulWidget and notifyListeners of ChangeNotifier.

Obx is a reactive GetX widget that also wraps around a widget holding a state. It takes a lambda function in its constructor which has a return value of the widget. Unlike GetBuilder, Obx does not require a call to the update function to rebuild. Obx listens for changes to observable types like RxInt, RxString, RxBool, etc. Appending .obs to a field's value makes it observable.

Now that you know how GetX works, let's walk through a code sample of its usage. We'll build an application that performs basic arithmetic operations, using GetX to manage the application state.

Setting up the project

#1. Create a new flutter project, name it getx_calculator , and run it to ensure everything is working fine.

#2. Run flutter pub add get in the terminal to add the GetX package to your pubspec.yaml.

Creating the Controllers

As mentioned above, GetX has two state managers. We'll first dive into using the Simple State Manager (GetBuilder).

#1. Create a new directory within your lib directory and call it controllers

#2. Create a new dart file for the controller under the controllers directory, naming it calc_controller.dart

#3. Create the CalculationController class inside calc_controller.dart

import 'package:get/get.dart';

class CalculationController extends GetxController { }

We have to extend the GetxController class to create a controller.

#4. Add the following fields in CalculationController :

  //current operator
  String _currentSign = "+";
  String get currentSign => _currentSign;

  //First operand
  int _numOne = 0;
  int get numOne => _numOne;

  //Second operand
  int _numTwo = 0;
  int get numTwo => _numTwo;

  //Result of the operation
  int _result = 0;
  int get result => _result;

These fields are the states of our application.

#5. Add the following methods to CalculationController :

  void increaseNumOne() {
    _numOne++;
    update();
  }

  void increaseNumTwo() {
    _numTwo++;
    update();
  }

  void decreaseNumOne() {
    _numOne--;
    update();
  }

  void decreaseNumTwo() {
    _numTwo--;
    update();
  }

  void add() {
    _currentSign = "+";
    _result = _numOne + _numTwo;
    update();
  }

  void subtract() {
    _currentSign = "-";
    _result = _numOne - _numTwo;
    update();
  }

  void multiply() {
    _currentSign = "x";
    _result = _numOne * _numTwo;
    update();
  }

  void divide() {
    _currentSign = "/";
    _result = _numOne ~/ _numTwo;
    update();
  }

  void reset() {
    _numOne = 0;
    _numTwo = 0;
    _result = 0;
    update();
  }

This is the business logic of our application. The update function call ensures the presentation layer receives updates to the application state.

Creating the Presentation Layer

#1. Replace main.dart with the code below:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controllers/calc_controller.dart';

typedef OnTap = void Function();

void main() {
  runApp(
    const MaterialApp(
      title: "GetX Example",
      home: Home(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

#2. Add the Home widget to main.dart

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    //Creates and injects the controller
    final controller = Get.put(CalculationController());
    return Scaffold(
        appBar: AppBar(
          title: const Text("GetX Example"),
        ),
        body: Container(
          margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
          child: Column(
            children: [
              Card(
                child: SizedBox(
                  width: double.maxFinite,
                  height: 100,
                  child: _calcWidget(),
                ),
              ),
              Expanded(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Expanded(
                          child: _tapButton(
                            "+ NumOne",
                            Colors.grey.shade700,
                            () {
                              controller.increaseNumOne();
                            },
                          ),
                        ),
                        const SizedBox(width: 10),
                        Expanded(
                          child: _tapButton(
                            "+ NumTwo",
                            Colors.blueGrey,
                            () {
                              controller.increaseNumTwo();
                            },
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 10.0),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Expanded(
                          child: _tapButton(
                            "- NumOne",
                            Colors.grey,
                            () {
                              controller.decreaseNumOne();
                            },
                          ),
                        ),
                        const SizedBox(width: 10),
                        Expanded(
                          child: _tapButton(
                            "- NumTwo",
                            Colors.blueGrey.shade300,
                            () {
                              controller.decreaseNumTwo();
                            },
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 10.0),
                    _tapButton(
                      "Add",
                      Colors.green,
                      () {
                        controller.add();
                      },
                    ),
                    const SizedBox(height: 10.0),
                    _tapButton(
                      "Subtract",
                      Colors.blueAccent,
                      () {
                        controller.subtract();
                      },
                    ),
                    const SizedBox(height: 10.0),
                    _tapButton(
                      "Multiply",
                      Colors.amber,
                      () {
                        controller.multiply();
                      },
                    ),
                    const SizedBox(height: 10.0),
                    _tapButton(
                      "Divide",
                      Colors.red,
                      () {
                        controller.divide();
                      },
                    ),
                    const SizedBox(height: 10.0),
                    _tapButton(
                      "Reset",
                      Colors.teal,
                      () {
                        controller.reset();
                      },
                    )
                  ],
                ),
              )
            ],
          ),
        ));
  }

  Widget _tapButton(String title, Color color, OnTap onTap) {
    return SizedBox(
      width: double.maxFinite,
      height: 50,
      child: ElevatedButton(
        onPressed: onTap,
        style: ElevatedButton.styleFrom(backgroundColor: color),
        child: Text(
          title,
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}

final controller = Get.put(CalculationController()); creates the CalculationController and injects it, which makes the controller available below the widget tree. The instantiated controller is used in the tap function of the buttons to call the appropriate controller method.

#3. Add the following function to the Home widget class:

Widget _calcWidget() {
    return GetBuilder<CalculationController>(
      builder: (calcController) {
        return Center(
          child: Text(
            "${calcController.numOne} ${calcController.currentSign} ${calcController.numTwo} = ${calcController.result}",
            style: const TextStyle(
                fontSize: 30, color: Colors.black),
          ),
        );
      },
    );
  }

The code above uses the GetBuilder widget to wrap around the Center widget, which holds the Text widget where the CalculationController states are used. The builder function is called, triggering a rebuild each time one of the states changes.

#4. Run the project on your emulator or device. It should look like the screen below

Screenshot_20221105_214111.png

Reactive State Manager (Obx/GetX)

As noted earlier, both Obx and GetX are reactive widgets that listen for changes in observable types/states. The notable difference between them is that the GetX builder function has a controller parameter while Obx doesn't. GetX also has an init parameter that can be used to initialize a controller.

We'll refactor our application to use Obx and observables for the state.

#1. Change the CalculationController fields to the following:

  //Current operator
  final RxString _currentSign = "+".obs;
  RxString get currentSign => _currentSign;

  //First operand
  final RxInt _numOne = 0.obs;
  RxInt get numOne => _numOne;

  //Second operand
  final RxInt _numTwo = 0.obs;
  RxInt get numTwo => _numTwo;

  //Result of the operation
  final RxInt _result = 0.obs;
  RxInt get result => _result;

Appending the .obs getter to our values converts the field to an Rx observable. GetX provides Observable types for all the basic data types in dart. Custom classes can also be made observable by affixing .obs to its instance or field values.

#2. Update the CalculationController methods to the following:

  void increaseNumOne() {
    _numOne.value++;
  }

  void increaseNumTwo() {
    _numTwo.value++;
  }

  void decreaseNumOne() {
    _numOne.value--;
  }

  void decreaseNumTwo() {
    _numTwo.value--;
  }

  void add() {
    _currentSign.value = "+";
    _result.value = _numOne.value + _numTwo.value;
  }

  void subtract() {
    _currentSign.value = "-";
    _result.value = _numOne.value - _numTwo.value;
  }

  void multiply() {
    _currentSign.value = "x";
    _result.value = _numOne.value * _numTwo.value;
  }

  void divide() {
    _currentSign.value = "/";
    _result.value = _numOne.value ~/ _numTwo.value;
  }

  void reset() {
    _numOne.value = 0;
    _numTwo.value = 0;
    _result.value = 0;
  }

Here, we've removed the update function calls. The value setter and getter are used to assign and retrieve the actual values.

#3. Update the _calcWidget function in main.dart to use the Obx widget.

Widget _calcWidget() {
    final calcController = Get.find<CalculationController>();
    return Obx(() => Center(
        child: Text(
          "${calcController.numOne} ${calcController.currentSign} ${calcController.numTwo} = ${calcController.result}",
          style: const TextStyle(
              fontSize: 30, color: Colors.black),
        ),
      )
    );
  }

Here, the Get.find method is used to retrieve the controller that was injected in the build method since Obx does not have direct access to the controller instance.

#4. Run the project. It should still function as before.

Other Features

  • Bindings

GetX also provides a convenient way of creating and initializing controllers in one place instead of doing it in the build method.

#1. Create a new dart file named init_controllers.dart and add the following code:

import 'package:get/get.dart';


class InitControllers extends Bindings {

  @override
  void dependencies() {
  }
}

#2. Add the following line in the dependencies method of the InitControllers class:

 Get.lazyPut(() => CalculationController());

#3. Update the main method in main.dart to use GetMaterialApp in place of MaterialApp

void main() {
  runApp(
    GetMaterialApp(
      title: "GetX Example",
      home: const Home(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

GetMaterialApp provides additional configurations on top of MaterialApp, which aids in the functionality of GetX.

#4. Initialize the InitControllers class

void main() {
  runApp(
    GetMaterialApp(
      title: "GetX Example",
      initialBinding: InitControllers(),
      home: const Home(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

#5. Replace final controller = Get.put(CalculationController()); with final controller = Get.find<CalculationController>(); in the build method.

final controller = Get.find<CalculationController>();

This retrieves the controller that was injected in the InitControllers class.

#6. Run the project. It should continue running as before.

Service

GetX controllers are created on demand and discarded when they are no longer needed. This behavior is not ideal in situations where the controller is required throughout the application's lifecycle, for instance, when interacting with a database.

GetX provides GetxService which ensures that the service remains in memory until the application is killed.

GetxService can be created in different ways either by:

  • Extending the GetxService abstract class

  • Implementing the GetxService interface

  • Using the GetxService mixin

The snippet below shows how the GetxService can be integrated into our CalculationController class.

class CalculationController extends GetxController implements GetxService {}

Conclusion

In this article, we've covered how to use GetX to manage application state and the different approaches that can be used, all of which produce the same result.

Having read through this, I hope you now have a greater understanding of the benefits of GetX and how to implement it in your projects moving forward. You can find the link to the complete source code here.

Thank you for reading.