Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

The ultimate guide to GetX state management in Flutter

13 min read 3758

The Ultimate Guide To The GetX State Management In Flutter

What happens when you hit the button on the switchboard in your house?

You are basically changing the state of the button from off to on or vice versa. This triggers the electricity, which either turns on the bulb or turns it off.

Lightbulb

Source: animated images.org

Think from a software developer’s point of view. When the bulb turns on, the UI of the bulb changes from a non-illuminated state to an illuminated state. Though physically we do not see the bulb being recreated or rebuilt, the UI would build from scratch if that were the situation on mobile software with reactive state management.

If you are coming from an Android or iOS framework, you need to start thinking about mobile application development from a whole new perspective. Android and iOS are imperative frameworks. On the other hand, Flutter is a declarative framework. This means that it is okay to build the UI from scratch instead of modifying a small part of it because Flutter is efficient at rebuilding the UI when a widget changes its state.

The declarative style of UI programming has its own benefits; you write the code of any UI once and describe how it should look in any state, and that’s it! However, as you dig deep into Flutter programming, there will be times when you will need to share your data and state between screens across your whole application. That is when you will need an excellent state management library to help you build applications fast and efficiently.

State management is a complex topic of discussion in Flutter. However, many state management libraries, such as Provider, are available, which most developers recommend.

But…

Today, we will discuss a simplified state management solution for Flutter application development that does not require context for most of its features, known as GetX.

What is GetX?

GetX is not only a state management library, but instead, it is a microframework combined with route management and dependency injection. It aims to deliver top-of-the-line development experience in an extra lightweight but powerful solution for Flutter. GetX has three basic principles on which it is built:

  1. Performance: focused on minimum consumption of memory and resources
  2. Productivity: intuitive and efficient tool combined with simplicity and straightforward syntax that ultimately saves development time
  3. Organization: decoupling business logic from view and presentation logic cannot get better than this. You do not need context to navigate between routes, nor do you need stateful widgets

The three pillars of GetX

  1. State management: GetX has two state managers. One is a simple state manager used with the GetBuilder function, and the other is a reactive state manager used with Getx or Obx. We will be talking about it in detail below
  2. Route management: whether navigating between screens, showing SnackBars, popping dialog boxes, or adding bottom sheets without the use of context, GetX has you covered. I will not write details on route management because it is beyond the scope of this article, but indeed a few examples to get an idea of how GetX syntax simplicity works
  3. Dependency management: GetX has a simple yet powerful solution for dependency management using controllers. With just a single line of code, it can be accessed from the view without using an inherited widget or context. Typically, you would instantiate a class within a class, but with GetX, you are instantiating with the Get instance, which will be available throughout your application

Value-added features of GetX

GetX has some great features out of the box, making it even easier to develop mobile applications in Flutter without any boilerplate code:

  1. Internationalization: translations with key-value maps, various language support, using translations with singulars, plurals, and parameters. Changing the application’s locale using only the Get word throughout the app
  2. Validation: email and password validations are also covered by GetX. Now you do not need to install a separate validation package
  3. Storage: GetX also provides fast and extra light synchronous key-value memory backup of data entirely written in Dart that easily integrates with the GetX core package
  4. Themes: switching between light and dark themes is made simple with GetX
  5. Responsive view: if you are building an application for different screen sizes, you just need to extend with GetView, and you can quickly develop your UI, which will be responsive for desktop, tablet, phone, and watch

Let’s get going with GetX state management

I will do this step by step, which I always like to do, and I will try to be descriptive and explain the process in as much detail as possible.

Step 1: Create a new application

Create a brand new application in your preferred IDE. First, remove all the starter comments by selecting the find and replace option in the Edit menu and type this: \/\/.*. This will select Flutter’s comments in the starter code, and you can just hit the delete button.

Step 2: Add required dependencies

Add these dependencies in your pubspec.yaml file:

get: ^4.6.1           //YAML
get_storage: ^2.0.3  //YAML

Run this command:

flutter pub get  //YAML

Before going on to Step 3, let me explain what we are doing here. I have created a small application that demonstrates the core functionalities of GetX. The application is about a store where the user can:

  1. change the name of the store
  2. add follower names
  3. add follower count
  4. change the status of the store from open to closed and vice versa
  5. add reviews to the store
  6. change the theme of the store from light to dark

All of the above will explain state management, dependency management, route management, storage, and themes.

We are more focused on state and dependency management here. The route, storage, and themes are just for the application’s aesthetics.

You can read along and test the application through this link.

Step 3: Update the MaterialApp Widget

After adding the dependencies, the first thing you need to do is change the MaterialApp widget to GetMaterialApp in your main.dart file. This gives access to all GetX properties across the application.



Step 4: Add GetX Controller

We have already established that GetX separates the UI from the business logic. This is where GetX Controller comes into play.

You can always create more than one controller in your application. The GetX Controller class controls the state of the UI when you wrap an individual widget with its Observer so that it only rebuilds when there is a change in the state of that particular widget.

We are adding a new Dart file to create our controller class, StoreController, which extends GetxController:

class StoreController extends GetxController {}

Next, we add a few variables and initialize them with default values.

Normally we would add these variables like this as given below:

final storeName = 'Thick Shake';

But, when using GetX, we have to make the variables observable by adding obs at the end of value. Then when the variable changes, other parts of the application that depend on it will be notified about it. So now, our initialized value will look like this:

final storeName = 'Thick Shake'.obs;

The rest of the variables are given below:

// String for changing the Store Name
final storeName = 'Thick Shake'.obs;
// int for increasing the Follower count
final followerCount = 0.obs;
// bool for showing the status of the Store open or close
final storeStatus = true.obs;
// List for names of Store Followers
final followerList = [].obs;
// Map for Names and their Reviews for the Store
final reviews = <StoreReviews>[].obs;
// text editing controllers
final storeNameEditingController  = TextEditingController();
final reviewEditingController = TextEditingController();
final followerController = TextEditingController();
final reviewNameController = TextEditingController();

Next, we create three methods for changing the name, increasing the follower count, and changing the store status:

updateStoreName(String name) {
 storeName(name);
}

updateFollowerCount() {
 followerCount(followerCount.value + 1);
}

void storeStatusOpen(bool isOpen) {
 storeStatus(isOpen);
}

Step 5: Dependency injection

In layman’s terms, we add the controller class we just created into our view class. There are three ways to instantiate.

  1. Extending the whole view class with GetView and injecting our StoreController with it:
    class Home extends GetView<StoreController>{}
  2. Instantiating the storeController like this:
    final storeController = Get.put(StoreContoller())
  3. For option three, start by creating a new StoreBinding class and implementing Bindings. Inside its default dependencies, you need to lazyPut the StoreController by using Get.lazyPut(). Secondly, you need to add the binding class inside the initialBinding property in GetMaterialWidget.

Lastly, instead of Get.Put as mentioned above, now you can use Get.find and GetX will find your controller for you when you instantiate in any of your classes:

class StoreBinding implements Bindings {
// default dependency
 @override
 void dependencies() {
   Get.lazyPut(() => StoreController();
 }
}
@override
Widget build(BuildContext context) {
 return GetMaterialApp(
   debugShowCheckedModeBanner: false,
   title: 'GetX Store',
   initialBinding: StoreBinding(),
}
class UpdateStoreName extends StatelessWidget {
 UpdateStoreName({Key? key}) : super(key: key);
//Getx will find your controller.
 final storeController = Get.find<StoreController>();

There are a lot of code and Dart files in the project. I am only writing about the three methods that I have mentioned above. The rest of the code will be available on Git. The link will be provided at the end of this article. Secondly, you can also try the application through a web link.

Step 6: Instantiate Controller

Since we have extended our Home view with GetView and created a binding class to lazyPut our controller inside it, we will now use Get.find to instantiate our controller inside our classes.

First, we add a new stateless widget, UpdateStoreName. Instantiate our controller class like this:

final storeController = Get.find<StoreController>();
RoundedInput(
 hintText: "Store Name",
 controller: storeController.storeNameEditingController,
),
const SizedBox(height: 20),
ElevatedButton(
 onPressed: () {
   storeController.updateStoreName(
       storeController.storeNameEditingController.text);
   Get.snackbar(
       'Updated',
       'Store name has been updated ton '
           '${storeController.storeNameEditingController.text}',
       snackPosition: SnackPosition.BOTTOM);
 },
 child: const Padding(
   padding: EdgeInsets.all(10.0),
   child: Text(
     'Update',
     style: TextStyle(fontSize: 20.0),
   ),
 ),
),

Let me explain the above code: RoundedInput is just a custom TextField, and we are adding a TextEditingController for the TextField using our storeController. We are also calling the updateStoreName() method in the same way inside the onPressed of ElevatedButton. And then we are showing a SnackBar as a confirmation that the store name has been updated.

Below is the code for AddFollowerCount and StoreStatus. Again both are stateless widgets, and the method of implementing the storeController and calling our controller is similar:

class AddFollowerCount extends StatelessWidget {
 AddFollowerCount({Key? key}) : super(key: key);
 final storeController = Get.find<StoreController>();

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: const Text("Add Follower Count")),
     floatingActionButton: FloatingActionButton(
       onPressed: () {storeController.updateFollowerCount();
       },
       child: const Icon(Icons.add),
     ),
     body: Container(
       padding: const EdgeInsets.all(24),
       child: Center(
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             const Text(
               'You have add these many followers to your store',
               textAlign: TextAlign.center,
               style: TextStyle(fontSize: 28),
             ),
             const SizedBox(
               height: 40.0,
             ),
             Obx(
               () => Text(
                 storeController.followerCount.value.toString(),
                 style: const TextStyle(fontSize: 48),
               ),
             )
           ],
         ),
       ),
     ),
   );
 }
}
class StoreStatus extends StatelessWidget {
 StoreStatus({Key? key}) : super(key: key);
 //final storeController = Get.put(StoreController());
 final storeController = Get.find<StoreController>();

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: const Text("Test Status Toggle")),
     body: Container(
       padding: const EdgeInsets.all(24),
       child: Center(
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             const Text(
               "Is the Store open?",
               style: TextStyle(fontSize: 22),
             ),
             const SizedBox(height: 16),
             Obx(
               () => Switch(
                 onChanged: (value) => storeController.storeStatus(value),
                 activeColor: Colors.green,
                 value: storeController.storeStatus.value,
               ),
             )
           ],
         ),
       ),
     ),
   );
 }
}

Step 7: Obx Widget (Observer)

Now, let us get to the part where the entered value of our store name, increased count of followers, and store status will be shown using our storeController.

Our Home view is extended with GetView<StoreController>, so we do not need to instantiate our storeController here. Instead, we can just use GetX’s default controller. Please look at the code given below to get a clear picture and understand the difference between Step 6 and Step 7.


More great articles from LogRocket:


You must have noticed that the Text widget inside the Flexible widget is wrapped with an Obx widget where we have also called our controller. Remember how we added (.obs) to our variables? Now, when we want to see the change in that observable variable, we have to wrap the widget with Obx, also known as Observer, similar to what you must have noticed in the above code.

Wrapping the widget with Obx will only rebuild that particular widget and not the whole class when the state changes. This is how simple it is:

class Home extends GetView<StoreController> {
 Home({Key? key}) : super(key: key);
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: AppColors.spaceCadet,
     appBar: AppBar(
       title: const Text("GetX Store"),),
     drawer: const SideDrawer(),
     body: Container(
       padding: const EdgeInsets.all(10),
       child: SingleChildScrollView(
         child: Column(
           children: [
             MainCard(
               title: "Store Info",
               body: Column(
                 crossAxisAlignment: CrossAxisAlignment.stretch,
                 children: [
                   Row(
                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     children: [
                       const Flexible(
                         child: Text('Store Name:',
                           style: TextStyle(fontSize: 20),),
                         fit: FlexFit.tight,),
                       const SizedBox(width: 20.0),
                   // Wrapped with Obx to observe changes to the storeName
                   // variable when called using the StoreController.
                       Obx(
                         () => Flexible(
                           child: Text(
                             controller.storeName.value.toString(),
                             style: const TextStyle(
                             fontSize: 22, fontWeight: FontWeight.bold) ),
                           fit: FlexFit.tight,
                         ),),
                     ],),
                   const SizedBox(height: 20.0),
                   Row(
                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     children: [
                       const Flexible(
                         child: Text('Store Followers:',
                           style: TextStyle(fontSize: 20),),
                         fit: FlexFit.tight, ),
                       const SizedBox(width: 20.0),
               // Wrapped with Obx to observe changes to the followerCount
               // variable when called using the StoreController.
                       Obx(
                         () => Flexible(
                           child: Text(
                             controller.followerCount.value.toString(),
                             textAlign: TextAlign.start,
                             style: const TextStyle(
                             fontSize: 22, fontWeight: FontWeight.bold),
                           ),
                           fit: FlexFit.tight,),), ],
                   ),
                   const SizedBox(height: 20.0),
                   Row(
                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     children: [
                       const Flexible(
                         child: Text('Status:',
                           style: TextStyle(fontSize: 20),),
                         fit: FlexFit.tight,),
                       const SizedBox(width: 20.0),
                 // Wrapped with Obx to observe changes to the storeStatus
                 // variable when called using the StoreController.
                       Obx(
                         () => Flexible(
                           child: Text(
                        controller.storeStatus.value ? 'Open' : 'Closed',
                             textAlign: TextAlign.start,
                             style: TextStyle(
                                 color: controller.storeStatus.value
                                     ? Colors.green.shade700
                                     : Colors.red,
                                 fontSize: 22,
                                 fontWeight: FontWeight.bold),),
                           fit: FlexFit.tight,
                         ),  ),  ], ), ], ), ),

I have purposely highlighted the controllers and Obx to understand the difference between a default stateful widget provided by Flutter and using GetX for managing the state of a view or an entire application.

If we were using a stateful widget, we would have to use the setState() method every time we wanted to see changes. We would also have to dispose of controllers manually. So instead, we avoid all the boilerplate code and just wrap our widget with Obx, and the rest is taken care of.

If we had to summarize all the above, it could be done in only two steps:

  1. Add obs to your variable
  2. Wrap your widget with Obx

An alternative method

Well, that is not the only way to do it. For example, if you make your variables observable, you can also wrap the widget with GetX<StoreController> directly instead of Obx. However, the functionality remains the same. This way, you do not need to instantiate the storeController before it can be called. Please look at the code below:

// Wrapped with GetX<StoreController> to observe changes to the
//storeStatus variable when called using the StoreController.
GetX<StoreController>(
 builder: (sController) => Flexible(
   child: Text(
     sController.storeStatus.value ? 'Open' : 'Closed',
     textAlign: TextAlign.start,
     style: TextStyle(
         color: sController.storeStatus.value
             ? Colors.green.shade700
             : Colors.red,
         fontSize: 22,
         fontWeight: FontWeight.bold), ),
   fit: FlexFit.tight, ),),

N.B., I have changed the storeStatus from Obx to GetX<StoreController> and it is using sController from the building function.

Wrapping the widgets with Obx or GetX is known as reactive state management.

Simple state management

Let us see an example for simple state management. First, the advantage of using simple state management is that you do not need to change your MaterialWidget to GetMaterialWidget. Secondly, you can combine other state management libraries with simple state management.

N.B., if you do not change your MaterialWidget to GetMaterialWidget, you will not be able to use other GetX features such as route management.

For simple state management:

  1. you need to use the GetBuilder function
  2. you do not need observable variables
  3. you have to call the update() function in your method

I have created a new variable in our StoreController. But this time, I have not added (obs) at the end of the variable. It means now it is not observable.

But I still need my view to get updated when the store count increases, so I have to call the update() function inside my newly created method. Check the code below:

// variable is not observable
int storeFollowerCount = 0;

void incrementStoreFollowers() {
 storeFollowerCount++;
//update function needs to be called
 update();
}

Now, in our Home view I have changed Obx to GetBuilder to the Text widget, which displays the follower count:

GetBuilder<StoreController>(
 builder: (newController) => Flexible(
   child: Text(
     newController.storeFollowerCount.toString(),
     textAlign: TextAlign.start,
     style: const TextStyle(
         fontSize: 22, fontWeight: FontWeight.bold),
   ),
   fit: FlexFit.tight, ),),

Since we are wrapping our follower count with GetBuilder in our Home view, we also have to make changes to the AddFollowerCount Dart file.

  1. Add this inside the onPressed function in the Fab button:
    storeController.incrementStoreFollowers();
    1. Wrap the Text widget with GetBuilder as well so that it displays the follower count:
      GetBuilder<StoreController>(
       builder: (newController) => Text(
         'With GetBuilder: ${newController.storeFollowerCount.toString()}',
         textAlign: TextAlign.start,
         style: const TextStyle(
             fontSize: 22, fontWeight: FontWeight.bold), ),),

There is one more difference between using Obx or GetX and using GetBuilder. When using Obx or GetX, you need to add value after calling your method using the StoreController. But when using GetBuilder, you do not need to add a value parameter to it. Please look at the difference below:

// value parameter to be added with Obx or GetX
controller.storeName.value.toString(),

// value parameter is not needed with GetBuilder
newController.storeFollowerCount.toString(),

That is all for different state managements provided by GetX. Furthermore, as promised, I am writing a little about route management and other features of the GetX package. Hence, a whole new article is needed to write in detail about it all.

Other GetX features

Route management

Traditionally, when a user wants to go from one screen to another with a click of a button, code would look like this:

Navigator.push(context, 
    MaterialPageRoute(builder: (context)=> Home()));

But, with GetX, there are literally just two words:

Get.to(Home());

When you want to navigate back to your previous screen:

Navigator.pop(context);

There is absolutely no need for context when you are using GetX:

Get.back();

If you have a dialog or a drawer opened and you want to navigate to another screen while closing the drawer or dialog, there are two ways to do this with default Flutter navigation:

  1. Close the drawer or dialog and then navigate like this:
    Navigator.pop(context);
    Navigator.push(context, 
        MaterialPageRoute(builder: (context)=> SecondScreen()));
  2. If you have named routes generated:
    Navigator.popAndPushNamed(context, '/second');

With GetX, it gets a lot simpler to generate named routes and navigate between screens while closing any dialogs or drawers that are open:

// for named routes
Get.toNamed('/second'),
// to close, then navigate to named route
Get.offAndToNamed('/second'),

Value-added features

  1. Snackbars
    Get.snackbar(
       'title',
       'message',
       snackPosition: SnackPosition.BOTTOM,
    colorText: Colors.white,
    backgroundColor: Colors.black,
    borderColor: Colors.white);
  2. Dialogs
    Get.defaultDialog(
       radius: 10.0,
       contentPadding: const EdgeInsets.all(20.0),
       title: 'title',
       middleText: 'content',
       textConfirm: 'Okay',
       confirm: OutlinedButton.icon(
         onPressed: () => Get.back(),
         icon: const Icon(
           Icons.check,
           color: Colors.blue,     ),
         label: const Text('Okay',
           style: TextStyle(color: Colors.blue),
         ),   ),
     cancel: OutlinedButton.icon(
         onPressed: (){},
         icon: Icon(),
         label: Text(),),);
  3. Bottom sheets
    Get.bottomSheet(
       Container(
     height: 150,
     color: AppColors.spaceBlue,
     child: Center(
         child: Text(
       'Count has reached ${obxCount.value.toString()}',
       style: const TextStyle(fontSize: 28.0, color: Colors.white),
     )),
    ));

Looking at the above code, you can easily understand how simple it is to show and customize snackbars, dialogs, and bottom sheets.

Well, this is the tip of the iceberg. There is a lot more that can be done with the GetX library. Before I end my article, one last example is switching between light and dark themes.

Switching from light to dark themes and vice versa

First, I created a ThemeController similar to our StoreController. Inside my controller, I am using the GetStorage function to save the switched theme:

class ThemeController extends GetxController {
  final _box = GetStorage();
  final _key = 'isDarkMode';

  ThemeMode get theme => _loadTheme() ? ThemeMode.dark : ThemeMode.light;
  bool _loadTheme() => _box.read(_key) ?? false;

  void saveTheme(bool isDarkMode) => _box.write(_key, isDarkMode);
  void changeTheme(ThemeData theme) => Get.changeTheme(theme);
  void changeThemeMode(ThemeMode themeMode) => Get.changeThemeMode(themeMode);
}

Inside the GetMaterialApp widget, I have added properties for theme and darkTheme as well as initialized themeController and added the same to the themeMode property:

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);
  final themeController = Get.put(ThemeController());

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'GetX Store',
      initialBinding: StoreBinding(),
      theme: Themes.lightTheme,
      darkTheme: Themes.darkTheme,
      themeMode: themeController.theme,
}
}

Next, in our Home screen in the appBar, I have added an icon that switches the theme between light and dark. Just have a look at the code below:

class Home extends GetView<StoreController> {
 Home({Key? key}) : super(key: key);
 final themeController = Get.find<ThemeController>();

 @override
 Widget build(BuildContext context) {
   return Scaffold(backgroundColor: AppColors.spaceCadet,
     appBar: AppBar(title: const Text("GetX Store"),
       actions: [IconButton(
           onPressed: () {
             if (Get.isDarkMode) {
               themeController.changeTheme(Themes.lightTheme);
               themeController.saveTheme(false);
             } else {
               themeController.changeTheme(Themes.darkTheme);
               themeController.saveTheme(true); }},
           icon: Get.isDarkMode
               ? const Icon(Icons.light_mode_outlined)
               : const Icon(Icons.dark_mode_outlined),),], ),

And that’s it. Now you can easily switch between light and dark themes.

Conclusion

After reading the whole article, do you wonder why the creator of this library gave it the name GetX? In my humble opinion, people often give an order saying, “Get it done!” or “Let’s get it done!”

However, the x is an unknown variable, but in this case, it really is anything and everything.

You can get everything done with GetX.

That’s all for now, and thank you for reading. If you have any suggestions, corrections, or feedback, leave a comment below.

I’m leaving links below to the source code of the application that I have explained in this article and an additional counter application for the basics. Feel free to clone the Git repository and experiment with the code yourself. There are links to PWAs as well for trying out the application without any installations.

GetX store link: https://github.com/timelessfusionapps/getx_store

GetX counter link: https://github.com/timelessfusionapps/getx_counter

GetX store link: https://getx-store.web.app/#/

GetX counter app: https://getx-counter.web.app/#/

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

One Reply to “The ultimate guide to GetX state management in Flutter”

  1. Its not working on web version on web you are using getx so its not working properly

Leave a Reply