flutter
/

GetX State Persistence: Save & Restore App State with GetStorage

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX State Persistence: Save & Restore App State with GetStorage

Introduction

State persistence is the ability to save your app's state (like user preferences, settings, or even data) to a local storage and restore it when the app is reopened. In GetX, the simplest way to achieve this is with GetStorage – a lightweight key‑value storage that integrates seamlessly with reactive variables and workers. This guide covers how to persist and restore state using GetX patterns.

Setting Up GetStorage

Add GetX to your pubspec.yaml (GetStorage is included). Initialize GetStorage in main() before running the app.

DARTRead-only
1
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await GetStorage.init(); // initializes default storage
  runApp(MyApp());
}

Basic Persistence with Workers

The most common pattern is to save a reactive variable's value to storage whenever it changes, using a worker. Then load the saved value in onInit.

DARTRead-only
1
class SettingsController extends GetxController {
  final storage = GetStorage();
  var themeMode = ThemeMode.system.obs;

  @override
  void onInit() {
    super.onInit();
    // Load saved value
    String? saved = storage.read('themeMode');
    if (saved != null) {
      themeMode.value = ThemeMode.values.firstWhere(
        (e) => e.name == saved,
        orElse: () => ThemeMode.system,
      );
    }
    // Save when changed
    ever(themeMode, (mode) {
      storage.write('themeMode', mode.name);
    });
  }
}

Persisting Multiple Values

For multiple settings, you can create a method to load all at once, or use separate workers. A dedicated settings service can help organize.

DARTRead-only
1
class SettingsController extends GetxController {
  final storage = GetStorage();
  var username = ''.obs;
  var notificationsEnabled = true.obs;

  @override
  void onInit() {
    super.onInit();
    // Load all at once
    username.value = storage.read('username') ?? '';
    notificationsEnabled.value = storage.read('notifications') ?? true;

    // Save when changed
    ever(username, (value) => storage.write('username', value));
    ever(notificationsEnabled, (value) => storage.write('notifications', value));
  }
}

Persisting Complex Data

GetStorage only supports primitive types, lists, and maps. To store custom objects, convert them to a map (JSON) first. You can also use a helper service.

DARTRead-only
1
class User {
  final String name;
  final int age;
  User(this.name, this.age);

  Map<String, dynamic> toJson() => {'name': name, 'age': age};
  factory User.fromJson(Map<String, dynamic> json) => User(json['name'], json['age']);
}

class UserController extends GetxController {
  final storage = GetStorage();
  var user = Rx<User?>(null);

  @override
  void onInit() {
    super.onInit();
    // Load
    Map<String, dynamic>? data = storage.read('user');
    if (data != null) user.value = User.fromJson(data);

    // Save on change
    ever(user, (user) {
      if (user != null) {
        storage.write('user', user.toJson());
      } else {
        storage.remove('user');
      }
    });
  }
}

Persisting Lists

You can store reactive lists as RxList and persist them using assignAll when loading, and save whenever the list changes.

DARTRead-only
1
class TodoController extends GetxController {
  final storage = GetStorage();
  var todos = <String>[].obs;

  @override
  void onInit() {
    super.onInit();
    // Load list
    List<dynamic>? saved = storage.read('todos');
    if (saved != null) todos.assignAll(saved.cast<String>());

    // Save on every change
    ever(todos, (list) {
      storage.write('todos', list.toList());
    });
  }
}

Restoring State on App Start

You can also create a service that loads all persisted data before any controller is initialized. Use an initialBinding to load and then inject the data into controllers.

DARTRead-only
1
class AppInitializer extends Bindings {
  @override
  void dependencies() {
    final storage = GetStorage();
    // Load global settings
    final theme = storage.read('themeMode') ?? 'system';
    Get.put<ThemeService>(ThemeService(theme));
    // ... other global data
  }
}

void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialBinding: AppInitializer(),
      // ...
    );
  }
}

Using with StateMixin

You can combine persistence with StateMixin to manage loading/error states while restoring data.

DARTRead-only
1
class DataController extends GetxController with StateMixin<List<Item>> {
  final storage = GetStorage();

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

  void loadData() {
    change(null, status: RxStatus.loading());
    try {
      List<dynamic>? saved = storage.read('items');
      if (saved != null) {
        final items = saved.map((e) => Item.fromJson(e)).toList();
        change(items, status: RxStatus.success());
      } else {
        change([], status: RxStatus.empty());
      }
    } catch (e) {
      change(null, status: RxStatus.error(e.toString()));
    }
  }
}

Best Practices

  • Initialize GetStorage early – Call await GetStorage.init() before runApp.
  • Use workers to auto‑save – ever saves on every change; debounce can be used for less frequent saves.
  • Keep data small – GetStorage is not meant for large datasets; use Hive or SQLite for that.
  • Handle null defaults – Provide fallback values when reading.
  • Separate concerns – Create a service for persistence logic, not inside controllers if it becomes complex.
  • Encapsulate persistence in a service – A StorageService can provide methods like saveTheme, loadTheme.

Common Mistakes

  • ❌ Forgetting to call GetStorage.init() – Causes runtime errors. ✅ Call it in main before runApp.
  • ❌ Storing large objects – May cause performance issues or disk limits. ✅ Use appropriate storage for large data.
  • ❌ Not handling null when reading – Can cause exceptions. ✅ Use ?? default values.
  • ❌ Saving on every tiny change without debounce – May cause unnecessary writes. ✅ Use debounce for fields like text inputs.

FAQ

  • Q: Can I use SharedPreferences instead of GetStorage?
    A: Yes, but GetStorage is faster and has a simpler API. It also works on web (localStorage).
  • Q: How do I clear all persisted data?
    A: Use GetStorage().erase() to clear the entire storage box.
  • Q: Is it safe to persist user tokens?
    A: Yes, but consider security. GetStorage stores in plain text; for sensitive data, consider encrypting or using FlutterSecureStorage.
  • Q: How to handle multiple storage boxes?
    A: Use named boxes: GetStorage('settings') and GetStorage('user'). Initialize each with GetStorage.init('boxName').
  • Q: Can I persist state across app updates?
    A: Yes, GetStorage persists across updates as long as the storage file format remains compatible.

Conclusion

Persisting state with GetX and GetStorage is straightforward. By using workers to automatically save reactive variables and loading them in onInit, you can create apps that remember user preferences and data across sessions. For more complex data, you can serialize objects and combine with StateMixin for a full data layer.

Try it yourself

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

void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class SettingsController extends GetxController {
  final storage = GetStorage();
  var username = ''.obs;

  @override
  void onInit() {
    super.onInit();
    // Load saved username
    username.value = storage.read('username') ?? '';
    // Save when changed
    ever(username, (value) {
      storage.write('username', value);
    });
  }

  void setUsername(String name) {
    username.value = name;
  }
}

class MyApp extends StatelessWidget {
  final controller = Get.put(SettingsController());

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('State Persistence')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Obx(() => Text('Saved username: ${controller.username.value}', style: TextStyle(fontSize: 20))),
              SizedBox(height: 20),
              TextField(
                onChanged: controller.setUsername,
                decoration: InputDecoration(labelText: 'Enter username'),
              ),
              SizedBox(height: 10),
              ElevatedButton(
                onPressed: () => controller.setUsername(''),
                child: Text('Clear'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which method is used to initialize GetStorage?

A
GetStorage.prepare()
B
GetStorage.init()
C
GetStorage.start()
D
GetStorage.load()
Q2
of 3

What is the recommended way to auto‑save a reactive variable when it changes?

A
Use a timer
B
Use a worker like ever
C
Call storage.write manually every time
D
Use a StreamSubscription
Q3
of 3

How do you store a custom object in GetStorage?

A
Store it directly
B
Convert to Map/JSON first
C
Use a special wrapper
D
It's not possible

Previous

getx computed state

Next

getx smart management

Related Content

Need help?

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