Flutter Tutorial

Flutter Intermediate Lesson 52 of 135 4 min read

GetX in Flutter: Obx, GetBuilder, Dependency Injection and Routing

Learn GetX state management step by step: reactive Obx with .obs, simple GetBuilder with update(), Get.put and Get.find, and routing. Easy examples.

On this page

GetX (package name get) is an all-in-one toolkit: state management, dependency injection and routing in one package, with very little code. It is popular for building apps quickly.

GetX has two ways to manage state:

  • Obx: reactive. Mark a value with .obs and any Obx that reads it rebuilds automatically.
  • GetBuilder: simple. Change a normal field, call update(), and GetBuilder rebuilds.

The get package is not available in the online editor, so the examples below have no Run button. Copy them into lib/main.dart of a new project to try them.

Install

flutter pub add get

Replace MaterialApp with GetMaterialApp so routing, snackbars and dialogs work without a context.

Obx: reactive state

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

class CounterController extends GetxController {
  final count = 0.obs; // an observable int (RxInt)

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

void main() => runApp(const GetMaterialApp(home: CounterPage()));

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    final c = Get.put(CounterController()); // create and register once

    return Scaffold(
      appBar: AppBar(title: const Text('GetX Obx')),
      body: Center(
        child: Obx(() => Text('${c.count.value}', style: const TextStyle(fontSize: 48))),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: c.increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • .obs turns any value into an observable: 0.obs, ''.obs, false.obs, <String>[].obs, User().obs.
  • Obx(() => ...) rebuilds when an observable it read changes.
  • Get.put creates the controller and stores it so other screens can find it.

GetBuilder: simple state

No .obs; call update() yourself. It uses less memory and is very predictable.

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

class CartController extends GetxController {
  final List<String> items = [];

  void add(String item) {
    items.add(item);
    update(); // rebuild every GetBuilder<CartController>
  }
}

void main() => runApp(const GetMaterialApp(home: ShopPage()));

class ShopPage extends StatelessWidget {
  const ShopPage({super.key});

  @override
  Widget build(BuildContext context) {
    return GetBuilder<CartController>(
      init: CartController(),
      builder: (cart) => Scaffold(
        appBar: AppBar(title: Text('Cart: ${cart.items.length}')),
        body: ListView(
          children: [
            for (final fruit in ['Apple', 'Mango', 'Banana'])
              ListTile(
                title: Text(fruit),
                trailing: IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: () => cart.add(fruit),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Obx vs GetBuilder

ObxGetBuilder
Values.obs observablesNormal fields
RebuildAutomaticWhen you call update()
Best forLots of small reactive valuesSimple, predictable screens

There is also a GetX<Controller>(builder: ...) widget, a reactive builder that can create the controller for you.

Dependency injection

Get.put(ApiService());                 // create now
Get.lazyPut(() => ProfileController()); // create the first time it is used
final api = Get.find<ApiService>();    // get it anywhere, no context
Get.delete<ProfileController>();       // remove it

Bindings group the controllers a screen needs:

class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}

Routing, snackbars and dialogs without context

Get.to(() => const DetailsPage());       // push
Get.back();                               // pop
Get.off(() => const LoginPage());        // replace
Get.toNamed('/details', arguments: 42);   // named route
Get.snackbar('Saved', 'Your note was saved');
Get.defaultDialog(title: 'Delete?', middleText: 'This cannot be undone');

A login screen example

class LoginController extends GetxController {
  final email = ''.obs;
  final loading = false.obs;

  bool get isValid => email.value.contains('@');

  Future<void> login() async {
    loading.value = true;
    await Future.delayed(const Duration(seconds: 1)); // call your API here
    loading.value = false;
    Get.snackbar('Welcome', email.value);
  }
}

// In the widget:
final c = Get.put(LoginController());

TextField(onChanged: (v) => c.email.value = v);

Obx(() => FilledButton(
      onPressed: c.isValid && !c.loading.value ? c.login : null,
      child: c.loading.value
          ? const CircularProgressIndicator()
          : const Text('Log in'),
    ));

Pros and cons

  • Pros: very little code; state, DI and routing in one place; no context needed; fast to prototype.
  • Cons: a lot of “magic”, which can make large apps harder to test and reason about; it replaces many Flutter conventions, so skills transfer less to other packages; check the package’s update activity before starting a long project.

Good for: quick development, small to medium apps, developers who want one toolkit. For large team projects, Riverpod or Bloc are more common.

Try it yourself

Build a two-screen app: a list of products on the first screen and a cart on the second, sharing one CartController found with Get.find. Show the cart count in the first screen’s app bar with Obx.

Practise in the playground Updated