Skip to main content

JSON Serialization of Stores

The pub package is a popular way to encode/decode between json representations of your models. It works by attaching the @JsonSerializable() annotation to the Store classes. Since this is a custom annotation, we have to invoke the build_runner command, just like we do for mobx_codegen.

Let's add support for json_serializable to the todos example.

See the complete code here.

Adding dependency in pubspec.yaml

The first step is to include the dependency on the pub and pub packages. We add this to the pubspec.yaml and run flutter pub get to download it.

dependencies:
json_serializable: ^6.3.2
json_annotation: ^4.6.0

Adding annotations

To make our store classes travel to JSON and back, we need to annotate them with @JsonSerializable():

import 'package:json_annotation/json_annotation.dart';
import 'package:mobx/mobx.dart';

part 'todo.g.dart';

@JsonSerializable()
class Todo extends _Todo with _$Todo {
Todo(String description) : super(description);
}

enum VisibilityFilter { all, pending, completed }

@JsonSerializable()
class TodoList extends _TodoList with _$TodoList {}

abstract class _TodoList with Store {
@observable
@ObservableTodoListConverter()
ObservableList<Todo> todos = ObservableList<Todo>();

@observable
VisibilityFilter filter = VisibilityFilter.all;

@observable
String currentDescription = '';

@computed
ObservableList<Todo> get pendingTodos =>
ObservableList.of(todos.where((todo) => todo.done != true));

@computed
ObservableList<Todo> get completedTodos =>
ObservableList.of(todos.where((todo) => todo.done == true));

@computed
bool get hasCompletedTodos => completedTodos.isNotEmpty;

@computed
bool get hasPendingTodos => pendingTodos.isNotEmpty;

@computed
String get itemsDescription {
if (todos.isEmpty) {
return "There are no Todos here. Why don't you add one?.";
}

final suffix = pendingTodos.length == 1 ? 'todo' : 'todos';
return '${pendingTodos.length} pending $suffix, ${completedTodos.length} completed';
}

@computed
@JsonKey(ignore: true)
ObservableList<Todo> get visibleTodos {
switch (filter) {
case VisibilityFilter.pending:
return pendingTodos;
case VisibilityFilter.completed:
return completedTodos;
default:
return todos;
}
}

@computed
bool get canRemoveAllCompleted =>
hasCompletedTodos && filter != VisibilityFilter.pending;

@computed
bool get canMarkAllCompleted =>
hasPendingTodos && filter != VisibilityFilter.completed;

@action
void addTodo(String description) {
final todo = Todo(description);
todos.add(todo);
currentDescription = '';
}

@action
void removeTodo(Todo todo) {
todos.removeWhere((x) => x == todo);
}

@action
void removeCompleted() {
todos.removeWhere((todo) => todo.done);
}

@action
void markAllAsCompleted() {
for (final todo in todos) {
todo.done = true;
}
}
}

Custom Converters

In case of special types like ObservableList<Todo>, which are not directly serializable into JSON, we can write custom converters by extending the JsonConverter<T, S> type, as shown below:

import 'package:json_annotation/json_annotation.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_examples/todos/todo.dart';

class ObservableTodoListConverter extends JsonConverter<ObservableList<Todo>,
Iterable<Map<String, dynamic>>> {
const ObservableTodoListConverter();

@override
ObservableList<Todo> fromJson(Iterable<Map<String, dynamic>> json) =>
ObservableList.of(json.map(Todo.fromJson));

@override
Iterable<Map<String, dynamic>> toJson(ObservableList<Todo> object) =>
object.map((element) => element.toJson());
}

This converter is then used for the todos property as shown below:

abstract class _TodoList with Store {
@observable
@ObservableTodoListConverter()
ObservableList<Todo> todos = ObservableList<Todo>();

// ...
}

On with the code-generation

With these changes, let's run the build_runner command in the project folder:

dart pub run build_runner watch --delete-conflicting-outputs

This will generate todo.g.dart and todo_list.g.dart files.

JSON Serialization / Deserialization

final list = TodoList();
expect(list.todos.length, equals(0));

list..addTodo('first one')..addTodo('second one');
const targetJson = '''
{
"todos": [
{
"description": "first one",
"done": false
},
{
"description": "second one",
"done": false
}
],
"filter": "VisibilityFilter.all",
}''';

final listJson = list.toJson();
expect(listJson, targetJson);

final listInstance = TodoList.fromJson(listJson);
expect(list.todos.length, listInstance.todos.length);
expect(list.canMarkAllCompleted, listInstance.canMarkAllCompleted);
expect(list.itemsDescription, listInstance.itemsDescription);

Summary

With these changes, you should now be able to serialize the Todos to/from JSON ✌️. BTW, its worth noting that mobx_codegen can co-exist with other generators.

See the complete code here.