用仓库模式和带类型的密封结果,将 Flutter 应用组织成互不渗透的分层。
在 Kodokon 中打开本课一个为长久而构建的 Flutter 应用会把三种职责分开:表现层(widget、BuildContext)、逻辑层(状态、用例)和数据层(API、本地数据库)。首要原则是依赖的方向:表现层依赖逻辑层,逻辑层依赖数据抽象,绝不能反过来。具体来说,表现层之下不应出现任何 BuildContext,也不应出现任何 material.dart 的导入:一个自己弹出 SnackBar 的控制器是无法测试的控制器。另一个常被忽视的点:错误也是各层之间契约的一部分。与其让未类型化的异常泄漏出去,不如用一个 sealed 类来建模结果,编译器会强制你处理每一种情况。
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
const Success(this.value);
final T value;
}
class Failure<T> extends Result<T> {
const Failure(this.error);
final Object error;
}
class Course {
const Course({required this.id, required this.title});
final String id;
final String title;
}仓库(repository)是领域与外部世界之间的边界。它确切的职责是:暴露领域实体(绝不暴露原始的 API DTO)、隐藏访问策略(网络、缓存、本地数据库),并集中管理数据的新鲜度策略。在 Dart 3 中,用 abstract interface class 来声明契约:这个修饰符只允许从库外部进行 implements,你无法意外地继承某一部分实现,因此契约保持纯净。JSON 到实体的映射存在于实现之中:如果明天 API 重命名了某个字段,只有数据层需要改动。
abstract interface class CourseApi {
Future<List<Map<String, Object?>>> fetchRaw();
}
abstract interface class CourseRepository {
Future<Result<List<Course>>> getAll();
void invalidate();
}
class ApiCourseRepository implements CourseRepository {
ApiCourseRepository(this._api);
final CourseApi _api;
List<Course>? _cache;
@override
void invalidate() => _cache = null;
@override
Future<Result<List<Course>>> getAll() async {
final cached = _cache;
if (cached != null) {
return Success(cached);
}
try {
final rows = await _api.fetchRaw();
final courses = [
for (final row in rows)
Course(
id: row['id'] as String,
title: row['title'] as String,
),
];
_cache = courses;
return Success(courses);
} on Exception catch (error) {
return Failure(error);
}
}
}在表现层这一侧,一个通过构造函数注入的 ChangeNotifier 往往就足够了,无需搬出什么框架。避免使用全局单例:它们会冻结依赖图,并让测试中的替换变得痛苦。ListenableBuilder 只会重建它的 builder,而对 Result 进行的穷尽式 switch 则把 UI 锁得死死的:给密封类型再加上第三种状态,编译就会在所有没有处理它的地方失败。这正是这套模式的全部价值所在,一个疏忽会变成编译错误,而不是生产环境的 bug。
import 'package:flutter/material.dart';
class CourseListController extends ChangeNotifier {
CourseListController(this._repository);
final CourseRepository _repository;
Result<List<Course>>? state;
Future<void> load() async {
state = null;
notifyListeners();
state = await _repository.getAll();
notifyListeners();
}
}
class CourseListScreen extends StatelessWidget {
const CourseListScreen({
super.key,
required this.controller,
});
final CourseListController controller;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
return switch (controller.state) {
null => const Center(
child: CircularProgressIndicator(),
),
Failure() => const Center(
child: Text('Loading error'),
),
Success(value: final courses) =>
ListView.builder(
itemCount: courses.length,
itemBuilder: (context, i) => ListTile(
title: Text(courses[i].title),
),
),
};
},
);
}
}