flutter
/

GetX StateMixin: Reduce Boilerplate for Loading/Error/Success States

Last Sync: Today

On this page

11
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX StateMixin: Reduce Boilerplate for Loading/Error/Success States

What is StateMixin?

StateMixin is a powerful GetX mixin that helps you manage the common states of your data: loading, success, error, and empty. It reduces boilerplate and gives you a clean, reactive way to handle asynchronous operations like API calls, database queries, or any task that can be in one of these states. Combined with the .obx() helper widget, you can build UI that automatically responds to state changes without writing repetitive if/else conditions.

Basic Usage

To use StateMixin, your controller should extend GetxController and mix in StateMixin<T>, where T is the type of the data you want to hold (e.g., List<Post>, User, etc.). You then use the change method to update the state.

DARTRead-only
1
class PostsController extends GetxController with StateMixin<List<Post>> {
  final ApiService apiService;
  PostsController(this.apiService);

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

  Future<void> fetchPosts() async {
    // Set loading state
    change(null, status: RxStatus.loading());
    try {
      final posts = await apiService.getPosts();
      // Success state with data
      change(posts, status: RxStatus.success());
    } catch (e) {
      // Error state
      change(null, status: RxStatus.error(e.toString()));
    }
  }
}

The .obx() Helper Widget

In your view, you can use the .obx() method on the controller. It automatically handles the different states and returns the appropriate widget.

DARTRead-only
1
class PostsPage extends GetView<PostsController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts')),
      body: controller.obx(
        (data) => ListView.builder(
          itemCount: data!.length,
          itemBuilder: (_, index) => ListTile(
            title: Text(data[index].title),
          ),
        ),
        onLoading: Center(child: CircularProgressIndicator()),
        onError: (error) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: $error'),
              ElevatedButton(
                onPressed: controller.fetchPosts,
                child: Text('Retry'),
              ),
            ],
          ),
        ),
        onEmpty: Center(child: Text('No posts found')),
      ),
    );
  }
}

RxStatus – Predefined Status Values

StateMixin uses RxStatus to represent the state. The available factory constructors are:

  • RxStatus.loading() – Indicates the data is being loaded.
  • RxStatus.success() – The data has been loaded successfully.
  • RxStatus.error(String message) – An error occurred, with an optional message.
  • RxStatus.empty() – The data is empty (e.g., an empty list).

Custom States

You can also create your own status by extending RxStatus, but for most cases the built‑in ones are sufficient.

Manual State Checks

If you prefer not to use .obx(), you can manually check the status using reactive properties: controller.status.value, controller.loading, controller.hasError, controller.isSuccess, etc.

DARTRead-only
1
Obx(() {
  if (controller.loading) return CircularProgressIndicator();
  if (controller.hasError) return Text('Error: ${controller.errorMessage}');
  if (controller.isEmpty) return Text('No data');
  return ListView(...);
});

StateMixin vs Manual State

Best Practices

  • Use StateMixin for asynchronous operations – Especially for API calls, database queries, and file operations.
  • Always set a status – Every call to change should include a status to keep the UI consistent.
  • Provide custom onLoading, onError, onEmpty widgets – Make your UI more descriptive and user‑friendly.
  • Keep the data type clear – Use a strong type for T (e.g., List<Post>) to avoid runtime errors.
  • Handle error messages – Display them in the UI and consider logging them for debugging.

Common Mistakes

  • ❌ Not calling change with a status – The state won't update and UI will stay in previous state. ✅ Always pass a status (e.g., RxStatus.loading(), RxStatus.success()).
  • ❌ Calling change with null data when status is success – This will set data to null and might cause null issues. ✅ Keep data and status in sync.
  • ❌ Forgetting to reset state before retrying – If you retry, the previous error may still be shown. ✅ In the retry method, call change(null, status: RxStatus.loading()) first.
  • ❌ Using .obx without handling onEmpty – An empty list may show as success with no data, but you might want to show a message. ✅ Provide onEmpty for that case.

FAQ

  • Q: Can I use StateMixin with GetBuilder?
    A: Yes, but Obx or .obx is simpler. You can still access controller.status and use GetBuilder manually.
  • Q: How do I reset the state?
    A: Call change(null, status: RxStatus.loading()) to start fresh.
  • Q: What if I want to show a custom loading widget that is not a simple CircularProgressIndicator?
    A: You can pass any widget to onLoading.
  • Q: Can I combine StateMixin with StatefulWidget?
    A: Yes, but you lose the stateless advantage. It's better to use GetView.
  • Q: Is StateMixin only for async operations?
    A: No, you can use it for any data that can be in one of those states, e.g., form submission, local data loading.

Conclusion

StateMixin is a simple yet powerful tool in GetX that eliminates the repetitive code for handling loading, error, success, and empty states. By using it consistently, you'll write cleaner, more maintainable Flutter apps. The .obx() helper makes your UI code even more expressive and readable.

Try it yourself

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() => runApp(MyApp());

class Post {
  final int id;
  final String title;
  Post(this.id, this.title);
}

class ApiService {
  Future<List<Post>> fetchPosts() async {
    await Future.delayed(Duration(seconds: 2));
    // Simulate success
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
    if (response.statusCode == 200) {
      List jsonList = json.decode(response.body);
      return jsonList.map((json) => Post(json['id'], json['title'])).take(5).toList();
    } else {
      throw Exception('Failed to load');
    }
  }
}

class PostsController extends GetxController with StateMixin<List<Post>> {
  final ApiService apiService;
  PostsController(this.apiService);

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

  Future<void> fetchPosts() async {
    change(null, status: RxStatus.loading());
    try {
      final posts = await apiService.fetchPosts();
      if (posts.isEmpty) {
        change(posts, status: RxStatus.empty());
      } else {
        change(posts, status: RxStatus.success());
      }
    } catch (e) {
      change(null, status: RxStatus.error(e.toString()));
    }
  }
}

class PostsBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => ApiService());
    Get.lazyPut(() => PostsController(Get.find()));
  }
}

class PostsPage extends GetView<PostsController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('StateMixin Demo')),
      body: controller.obx(
        (data) => ListView.builder(
          itemCount: data!.length,
          itemBuilder: (_, index) => ListTile(
            title: Text('${data[index].title}'),
            subtitle: Text('ID: ${data[index].id}'),
          ),
        ),
        onLoading: Center(child: CircularProgressIndicator()),
        onError: (error) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: $error'),
              ElevatedButton(
                onPressed: controller.fetchPosts,
                child: Text('Retry'),
              ),
            ],
          ),
        ),
        onEmpty: Center(child: Text('No posts found')),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: '/',
      getPages: [
        GetPage(name: '/', page: () => PostsPage(), binding: PostsBinding()),
      ],
    );
  }
}

Test Your Knowledge

Q1
of 3

What is the primary purpose of StateMixin?

A
To manage navigation
B
To reduce boilerplate for loading/error/success states
C
To replace GetBuilder
D
To handle dependency injection
Q2
of 3

Which method do you call to update the state in StateMixin?

A
updateState
B
setState
C
change
D
update
Q3
of 3

What widget helper does StateMixin provide for the UI?

A
.builder()
B
.obx()
C
.view()
D
.state()

Previous

getx api integration

Next

getx testing

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.