flutter
/

GetX Refresh Indicator: Pull-to-Refresh with Reactive Data

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Refresh Indicator: Pull-to-Refresh with Reactive Data

Introduction

Pull-to-refresh is a common UX pattern where users swipe down to reload content. In Flutter, the RefreshIndicator widget provides this functionality. When combined with GetX, you can manage the refresh state reactively, showing loading indicators and updating the data seamlessly. This guide covers how to implement pull-to-refresh with GetX controllers, including error handling and best practices.

Basic Setup

First, create a controller that holds the data and a method to fetch it. Use reactive variables to automatically update the UI when the data changes.

DARTRead-only
1
class ItemsController extends GetxController {
  var items = <String>[].obs;
  var isLoading = false.obs;

  Future<void> fetchItems() async {
    isLoading.value = true;
    try {
      // Simulate network call
      await Future.delayed(Duration(seconds: 1));
      items.assignAll(['Item 1', 'Item 2', 'Item 3']);
    } finally {
      isLoading.value = false;
    }
  }
}

Implementing RefreshIndicator

In your view, wrap a ListView with RefreshIndicator. The onRefresh callback should call the controller's fetch method and return a Future. GetX automatically updates the UI when items changes.

DARTRead-only
1
class ItemsPage extends GetView<ItemsController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Items')),
      body: RefreshIndicator(
        onRefresh: controller.fetchItems,
        child: Obx(() {
          if (controller.isLoading.value && controller.items.isEmpty) {
            return Center(child: CircularProgressIndicator());
          }
          return ListView.builder(
            itemCount: controller.items.length,
            itemBuilder: (_, index) => ListTile(
              title: Text(controller.items[index]),
            ),
          );
        }),
      ),
    );
  }
}

Handling Refresh State

The onRefresh callback should return a Future. The RefreshIndicator shows its built‑in loading spinner while the future is pending. You don't need a separate loading indicator for the refresh action, but you may want to show a loading state for the initial load.

DARTRead-only
1
@override
void onInit() {
  super.onInit();
  fetchItems(); // initial load
}

Future<void> fetchItems() async {
  // For initial load, we use isLoading; for refresh, RefreshIndicator shows its own spinner
  if (items.isEmpty) isLoading.value = true;
  try {
    // ... fetch data
  } finally {
    isLoading.value = false;
  }
}

Error Handling During Refresh

If an error occurs during refresh, you should show a message to the user. You can use Get.snackbar or a reactive error variable. The RefreshIndicator will still finish (the spinner stops), so you need to handle the error separately.

DARTRead-only
1
class ItemsController extends GetxController {
  var items = <String>[].obs;
  var error = ''.obs;

  Future<void> fetchItems() async {
    error.value = '';
    try {
      // Simulate a potential error
      await Future.delayed(Duration(seconds: 1));
      if (Random().nextBool()) throw Exception('Network error');
      items.assignAll(['Item 1', 'Item 2']);
    } catch (e) {
      error.value = e.toString();
      Get.snackbar('Error', error.value, snackPosition: SnackPosition.BOTTOM);
      rethrow; // rethrow so RefreshIndicator knows it failed (optional)
    }
  }
}

Using StateMixin for Cleaner Code

You can also use StateMixin to handle loading, error, and data states in one place. Then your view can use .obx() and the onRefresh callback can simply call the fetch method that updates the mixin state.

DARTRead-only
1
class ItemsController extends GetxController with StateMixin<List<String>> {
  @override
  void onInit() {
    super.onInit();
    fetchItems();
  }

  Future<void> fetchItems() async {
    change(null, status: RxStatus.loading());
    try {
      await Future.delayed(Duration(seconds: 1));
      final data = ['Item A', 'Item B'];
      change(data, status: RxStatus.success());
    } catch (e) {
      change(null, status: RxStatus.error(e.toString()));
    }
  }
}

// UI
RefreshIndicator(
  onRefresh: controller.fetchItems,
  child: controller.obx(
    (data) => ListView.builder(
      itemCount: data!.length,
      itemBuilder: (_, i) => ListTile(title: Text(data[i])),
    ),
    onLoading: Center(child: CircularProgressIndicator()),
    onError: (error) => Center(child: Text('Error: $error')),
  ),
);

Best Practices

  • Show initial loading state – When the screen first opens, show a loading indicator if data is empty.
  • Use assignAll to update lists – Replacing the whole list with .value = newList can be less efficient; use .assignAll to trigger reactivity properly.
  • Handle errors gracefully – Show a snackbar or a widget with a retry button.
  • Avoid duplicate refreshes – Use a flag to prevent multiple simultaneous refresh calls, though RefreshIndicator already handles that.
  • Keep the controller focused – One controller per data source or screen.

Common Mistakes

  • ❌ Forgetting to call refresh on the controller – The onRefresh callback must return a Future that completes when data is ready.
  • ❌ Not handling the case where refresh fails – The user sees the spinner disappear but no feedback. ✅ Show an error message.
  • ❌ Using Obx incorrectly inside RefreshIndicator – The child of RefreshIndicator must be a scrollable widget; using Obx that returns a ListView is fine.
  • ❌ Setting isLoading to true inside refresh – This will show an extra loading indicator over the refresh spinner, causing visual duplication. ✅ Only use isLoading for initial load or for operations not triggered by refresh.

FAQ

  • Q: Can I use RefreshIndicator with a CustomScrollView?
    A: Yes, as long as the child is a scrollable widget. Wrap the CustomScrollView with RefreshIndicator.
  • Q: How do I add a pull-to-refresh to a GridView?
    A: Same approach – wrap the GridView with RefreshIndicator.
  • Q: Does RefreshIndicator work on the web?
    A: Yes, with mouse drag it works similarly.
  • Q: How to combine refresh with pagination?
    A: In your controller, have separate methods for initial load and load more. For refresh, reset the page and fetch from scratch.
  • Q: Can I use GetBuilder instead of Obx?
    A: Yes, but Obx is more concise. If you need fine‑grained control, you can use GetBuilder with update().

Conclusion

Implementing pull-to-refresh with GetX is straightforward: combine RefreshIndicator with a controller that holds your data and a fetch method. By using reactive state, you ensure the UI updates automatically when data changes. With the added patterns like StateMixin, you can also handle loading and error states elegantly.

Try it yourself

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: RefreshDemo(),
    );
  }
}

class ItemsController extends GetxController {
  var items = <String>[].obs;

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

  Future<void> fetchItems() async {
    await Future.delayed(Duration(seconds: 1));
    items.assignAll(['Item 1', 'Item 2', 'Item 3']);
  }
}

class RefreshDemo extends StatelessWidget {
  final controller = Get.put(ItemsController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Pull to Refresh')),
      body: RefreshIndicator(
        onRefresh: controller.fetchItems,
        child: Obx(() => ListView.builder(
          itemCount: controller.items.length,
          itemBuilder: (_, index) => ListTile(
            title: Text(controller.items[index]),
          ),
        )),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which widget in Flutter provides pull‑to‑refresh functionality?

A
RefreshIndicator
B
PullToRefresh
C
SwipeRefresh
D
RefreshView
Q2
of 3

What should the `onRefresh` callback return?

A
void
B
Future
C
Stream
D
bool
Q3
of 3

How do you update a reactive list in GetX after a refresh?

A
list = newList
B
list.assignAll(newList)
C
list.addAll(newList)
D
list.update(newList)

Previous

getx loading indicator

Next

getx computed state

Related Content

Need help?

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