You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中MobX如何观测普通列表实现UI实时更新?

问题描述

我在Flutter中遇到一个简化场景的问题:MobX可观察对象Book包含一个章节列表,我希望观测该列表,以便在创建新章节时UI能重绘显示新章节。
实际项目中我的模型更复杂,这是能复现问题的最简案例。
当创建新章节时,列表不会立即显示新章节,但返回主页面再进入后,新章节会出现在列表中。
由于项目其他原因,我不能将Book中的chapters属性改为ObservableList,即final ObservableList<Chapter> chapters;不可行。
我在Stack Overflow上查找了多个类似问题,但未找到解决方案。
请问如何观测当前书籍的章节列表,使创建新章节后UI能正确更新?


我的模型

class Book {
  final String title;
  final List<Chapter> chapters;

  Book(this.title, this.chapters);
}

class Chapter {
  final String title;

  Chapter(this.title);
}

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);
  }
}

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'),
        ),
      ),
    );
  }
}

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();
                }
              },
            ),
          ],
        );
      },
    );
  }
}

解决方案

由于无法将Book中的chapters改为ObservableList,可以通过以下几种方式触发MobX的响应式更新:

方法一:创建新列表实例替换原列表

核心思路是利用MobX对_currentBook引用变化的监听,当_currentBook的引用更新时,依赖它的计算属性和Observer会自动触发更新。

修改Store中的addChapter方法:

@action
addChapter(Chapter chapter) {
  // 生成包含新章节的新列表
  final newChapters = [..._currentBook.chapters, chapter];
  // 创建新的Book实例替换原有对象
  _currentBook = Book(_currentBook.title, newChapters);
}

方法二:给Book添加Observable注解并手动触发更新

如果允许对Book类做小改动,可以给章节列表添加@observable注解,再通过runInAction确保MobX感知到变化:

  1. 修改Book类:
import 'package:mobx/mobx.dart';

class Book {
  final String title;
  @observable
  final List<Chapter> chapters;

  Book(this.title, this.chapters);
}
  1. 更新Store的addChapter方法:
@action
addChapter(Chapter chapter) {
  runInAction(() {
    _currentBook.chapters.add(chapter);
  });
}

方法三:添加自定义触发变量

如果完全不能修改Book类,可以在Store中新增一个可观察计数器,让计算属性依赖这个计数器,每次添加章节时更新计数器来强制触发UI更新:

修改Store:

abstract class BookEditorStoreBase with Store {
  @readonly
  Book _currentBook = Book('', []);

  @readonly
  Chapter? _currentChapter;

  // 用于触发更新的计数器
  @observable
  int _updateTrigger = 0;

  @computed
  int get chapterCount {
    // 依赖计数器,每次计数器变化时重新计算
    _updateTrigger;
    return _currentBook.chapters.length;
  }

  @computed
  List<Chapter> get chapters {
    // 依赖计数器
    _updateTrigger;
    return _currentBook.chapters;
  }

  @action
  addChapter(Chapter chapter) {
    _currentBook.chapters.add(chapter);
    // 更新计数器触发UI更新
    _updateTrigger++;
  }
}

内容的提问来源于stack exchange,提问作者Rsevero

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 13:39:53