I have a simplified situation that I can't make to work in Flutter: a MobX observable object (Book) has a list of chapters that I also want to observe so my UI is redrawn to show a new chapter just created.
In reality I have more complex models, this is just the simpliest case I managed to show my problem.
When I create a new chapter, the new chapter isn't presented in the list but if I go back to the main page and back it appears in the chapter list.
I can't change the chapters property inside Book to an ObservableList for other reasons in my app: final ObservableList<Chapter> chapters;
is a no-no.
I looked several similar questions in stackoverflow but couldn't find a solution to my problem on any of them.
How can I observe the chapters in my current book so my UI properly updates after creating a new chapter?
My model:
class Book {
final String title;
final List<Chapter> chapters;
Book(this.title, this.chapters);
}
class Chapter {
final String title;
Chapter(this.title);
}
My MobX store:
import 'book.dart';
import 'package:mobx/mobx.dart';
part 'book_editor_store.g.dart';
class BookEditorStore = BookEditorStoreBase with _$BookEditorStore;
abstract class BookEditorStoreBase with Store {
@readonly
Book _currentBook = Book('', []);
@readonly
Chapter? _currentChapter;
@computed
int get chapterCount => _currentBook?.chapters.length ?? 0;
@computed
List<Chapter> get chapters => _currentBook?.chapters ?? [];
@action
setBook(Book book) {
_currentBook = book;
}
@action
addChapter(Chapter chapter) {
_currentBook?.chapters.add(chapter);
}
}
My main.dart:
import 'book_editor_page.dart';
import 'book_editor_store.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => BookEditorStore(),
child: MaterialApp(
title: 'MobX reactions',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MainPage(),
),
);
}
}
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Main Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const BookEditorPage()),
);
},
child: const Text('Editor'),
),
),
);
}
}
My BookEditorPage:
import 'book_editor_store.dart';
import 'book.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class BookEditorPage extends StatelessWidget {
const BookEditorPage({super.key});
@override
Widget build(BuildContext context) {
final bookEditorStore = Provider.of<BookEditorStore>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Book Editor'),
),
body: Column(
children: [
Observer(
builder: (_) => ListView.builder(
shrinkWrap: true,
itemCount: bookEditorStore.chapterCount,
itemBuilder: (_, index) {
final chapter = bookEditorStore.chapters[index];
return ListTile(
title: Text(chapter.title),
);
},
),
),
ElevatedButton(
onPressed: () => addChapter(context, bookEditorStore),
child: const Text('Add Chapter'),
),
],
),
);
}
void addChapter(BuildContext context, BookEditorStore bookEditorStore) {
final TextEditingController titleController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('New Chapter'),
content: TextFormField(
controller: titleController,
decoration: const InputDecoration(hintText: 'Enter chapter title'),
autofocus: true,
),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Create'),
onPressed: () {
final String title = titleController.text;
if (title.isNotEmpty) {
bookEditorStore.addChapter(Chapter(title));
Navigator.of(context).pop();
}
},
),
],
);
},
);
}
}
It's not elegant at all but it works: creating a separate observable chapter list inside the store (and taking care to mirror any changes in the real Book.chapters list in the observable copy).
Just change the BookEditorStore to:
import 'book.dart';
import 'package:mobx/mobx.dart';
part 'book_editor_store.g.dart';
class BookEditorStore = BookEditorStoreBase with _$BookEditorStore;
abstract class BookEditorStoreBase with Store {
@readonly
Book _currentBook = Book('', []);
@readonly
Chapter? _currentChapter;
@computed
int get chapterCount => _chapters.length;
@readonly
ObservableList<Chapter> _chapters = ObservableList<Chapter>();
@action
setBook(Book book) {
_currentBook = book;
_chapters = ObservableList<Chapter>.of(book.chapters);
}
@action
addChapter(Chapter chapter) {
_currentBook.chapters.add(chapter);
_chapters.add(chapter);
}
}
The rest remains the same and it works!
I won't accept my answer for now in hope someone presents a less klugdy solution.