State
The application state is all the data that your app needs to function, and that can change over time.
Usually you'd have a class that represents your app state.
Since State is a name already taken in Flutter, you can call it AppState.
This is an example:
class AppState {
final String name;
final int age;
AppState({required this.name, required this.age});
}
It's optional but common to also define a static method initialState that returns an instance
of the state with initial values:
class AppState {
final String name;
final int age;
AppState({required this.name, required this.age});
static AppState initialState() => AppState(name: "", age: 0);
}
The immutability requirement
In Redux, the state class must be immutable. This means that you can't change it directly.
Note: If you're not familiar with immutability, it means that once an object is created, you can't change its fields. Usually the fields are marked as
final.
Instead, you need to create a new AppState object every time you need to change the app state.
This is simple to do. For example, methods withName and withAge below
return a new AppState object with the new name or age:
class AppState {
final String name;
final int age;
AppState({required this.name, required this.age});
static AppState initialState() => AppState(name: "", age: 0);
AppState withName(String name) => AppState(name: name, age: age);
AppState withAge(int age) => AppState(name: name, age: age);
}
A common pattern is having a copy or copyWith method that allows you to change multiple fields
at once:
class AppState {
...
AppState withName(String name) => copy(name: name);
AppState withAge(int age) => copy(age: age);
AppState copy({String? name, int? age}) =>
AppState(
name: name ?? this.name,
age: age ?? this.age
);
}
Your state can be composed of any immutable objects,
including other immutable objects that you create.
For example, the following is a state that represents a Todo List.
The Todo class is also immutable:
class AppState {
final IList<Todo> todos;
AppState({required this.todos});
static AppState initialState() => AppState(todos: IList.empty());
}
class Todo {
final String description;
final bool done;
Todo({required this.description, this.done = false});
}
Note that the todos field is an immutable list of type IList, provided by the
fast_immutable_collections
package (one of my other 16 Flutter packages).
You don't need to use package fast_immutable_collections, but it's recommended because it
provides immutable lists, sets and maps that are easier to use than trying to use standard Dart collections
like List in an immutable way.
To add and remove items from the list, you can use the IList.add and IList.remove methods:
class AppState {
final IList<Todo> todos;
AppState({required this.todos});
static AppState initialState() => AppState(todos: IList.empty());
AppState copy({IList<Todo>? todos}) =>
AppState(todos: todos ?? this.todos);
AppState add(Todo todo) => copy(todos: todos.add(todo));
AppState remove(Todo todo) => copy(todos: todos.remove(todo);
}
Single state
If you think having a single state class to represent all your app state is too restrictive, it's actually not. You can have multiple state classes, as long as you put them all inside a single class in the end. For example, suppose we need to represent a state that contains both:
- A Todo List
- Some user information:
First, create separate classes for each part of the state:
// Todo List
class TodoList {
final IList<Todo> list;
TodoList({this.list = const IList<Todo>()});
TodoList copy({IList<Todo>? list}) =>
TodoList(list: list ?? this.list);
TodoList add(Todo todo) => copy(list: list.add(todo));
TodoList remove(Todo todo) => copy(list: list.remove(todo));
}
// User information
class User {
final String name;
final int age;
User({this.name = "", this.age = 0});
User copy({String? name, int? age}) =>
User(name: name ?? this.name, age: age ?? this.age);
}
Then, create a single AppState class that holds both of them:
class AppState {
final TodoList todoList;
final User user;
AppState({required this.todoList, required this.user});
static AppState initialState() =>
AppState(todoList: TodoList(), user: User());
AppState copy({TodoList? todoList, User? user}) =>
AppState(todoList: todoList ?? this.todoList, user: user ?? this.user);
AppState withTodoList(LodoList todoList) => copy(todoList: todoList);
AppState withUser(User user) => copy(user: user);
}
Testing the state
Since your state is immutable and its methods return new instances, testing is very easy. LLMs like Claude, ChatGPT, and Gemini can easily write very complete unit tests for your state classes.
This is a suggested prompt for an AI agent:
Read file
app_state.dartthat defines theAppStateclass. Start by listing every state class in the file, and include any state classes they use. Follow this recursively until the full set of state classes is found. This will be the complete list of all classes that make up the application state. Create a todo list to write unit tests for each class in that list. For every class, write complete unit tests that cover all its methods. This includes constructors,copymethods, and any method that returns a new instance. Each class must have its own separate Dart test file. If a test file already exists, review it and add any missing tests.
Now that we know how to create the state, let's see next how to create the Async Redux store that will hold it.