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.