Dart/Flutter 模式¶
本文件使用 Dart、Flutter 以及通用生态系统特定内容扩展 common/patterns.md。
仓库模式¶
abstract interface class UserRepository {
Future<User?> getById(String id);
Future<List<User>> getAll();
Stream<List<User>> watchAll();
Future<void> save(User user);
Future<void> delete(String id);
}
class UserRepositoryImpl implements UserRepository {
const UserRepositoryImpl(this._remote, this._local);
final UserRemoteDataSource _remote;
final UserLocalDataSource _local;
@override
Future<User?> getById(String id) async {
final local = await _local.getById(id);
if (local != null) return local;
final remote = await _remote.getById(id);
if (remote != null) await _local.save(remote);
return remote;
}
@override
Future<List<User>> getAll() async {
final remote = await _remote.getAll();
for (final user in remote) {
await _local.save(user);
}
return remote;
}
@override
Stream<List<User>> watchAll() => _local.watchAll();
@override
Future<void> save(User user) => _local.save(user);
@override
Future<void> delete(String id) async {
await _remote.delete(id);
await _local.delete(id);
}
}
状态管理:BLoC/Cubit¶
// Cubit — シンプルなステート遷移
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
// BLoC — イベント駆動
@immutable
sealed class CartEvent {}
class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; }
class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; }
class CartCleared extends CartEvent {}
@immutable
class CartState {
const CartState({this.items = const []});
final List<Item> items;
CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);
}
class CartBloc extends Bloc<CartEvent, CartState> {
CartBloc() : super(const CartState()) {
on<CartItemAdded>((event, emit) =>
emit(state.copyWith(items: [...state.items, event.item])));
on<CartItemRemoved>((event, emit) =>
emit(state.copyWith(items: state.items.where((i) => i.id != event.id).toList())));
on<CartCleared>((_, emit) => emit(const CartState()));
}
}
状态管理:Riverpod¶
// シンプルなプロバイダー
@riverpod
Future<List<User>> users(Ref ref) async {
final repo = ref.watch(userRepositoryProvider);
return repo.getAll();
}
// ミュータブルなステート用の Notifier
@riverpod
class CartNotifier extends _$CartNotifier {
@override
List<Item> build() => [];
void add(Item item) => state = [...state, item];
void remove(String id) => state = state.where((i) => i.id != id).toList();
void clear() => state = [];
}
// ConsumerWidget
class CartPage extends ConsumerWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartNotifierProvider);
return ListView(
children: items.map((item) => CartItemTile(item: item)).toList(),
);
}
}
依赖注入¶
推荐构造函数注入。在组合根中使用 get_it 或 Riverpod 提供者:
// get_it の登録 (セットアップファイル内)
void setupDependencies() {
final di = GetIt.instance;
di.registerSingleton<ApiClient>(ApiClient(baseUrl: Env.apiUrl));
di.registerSingleton<UserRepository>(
UserRepositoryImpl(di<ApiClient>(), di<LocalDatabase>()),
);
di.registerFactory(() => UserListViewModel(di<UserRepository>()));
}
ViewModel 模式(无 BLoC/Riverpod)¶
class UserListViewModel extends ChangeNotifier {
UserListViewModel(this._repository);
final UserRepository _repository;
AsyncState<List<User>> _state = const Loading();
AsyncState<List<User>> get state => _state;
Future<void> load() async {
_state = const Loading();
notifyListeners();
try {
final users = await _repository.getAll();
_state = Success(users);
} on Exception catch (e) {
_state = Failure(e);
}
notifyListeners();
}
}
UseCase 模式¶
class GetUserUseCase {
const GetUserUseCase(this._repository);
final UserRepository _repository;
Future<User?> call(String id) => _repository.getById(id);
}
class CreateUserUseCase {
const CreateUserUseCase(this._repository, this._idGenerator);
final UserRepository _repository;
final IdGenerator _idGenerator; // 注入 — ドメイン層は uuid パッケージに直接依存してはならない
Future<void> call(CreateUserInput input) async {
// バリデーション、ビジネスルールの適用、その後永続化
final user = User(id: _idGenerator.generate(), name: input.name, email: input.email);
await _repository.save(user);
}
}
使用 freezed 的不可变状态¶
@freezed
class UserState with _$UserState {
const factory UserState({
@Default([]) List<User> users,
@Default(false) bool isLoading,
String? errorMessage,
}) = _UserState;
}
整洁架构的层边界¶
lib/
├── domain/ # 純粋な Dart — Flutter なし、外部パッケージなし
│ ├── entities/
│ ├── repositories/ # 抽象インターフェース
│ └── usecases/
├── data/ # ドメインインターフェースの実装
│ ├── datasources/
│ ├── models/ # fromJson/toJson を持つ DTO
│ └── repositories/
└── presentation/ # Flutter ウィジェット + ステート管理
├── pages/
├── widgets/
└── providers/ (or blocs/ or viewmodels/)
- 领域层不得导入
package:flutter或数据层包 - 数据层在仓库边界处将 DTO 映射为领域实体
- 表现层不直接使用仓库,而是调用用例
导航(GoRouter)¶
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/users/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserDetailPage(userId: id);
},
),
],
// refreshListenable は認証ステートが変わるたびに redirect を再評価する
refreshListenable: GoRouterRefreshStream(authCubit.stream),
redirect: (context, state) {
final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
if (!isLoggedIn && !state.matchedLocation.startsWith('/login')) {
return '/login';
}
return null;
},
);
参考资料¶
通过技能 flutter-dart-code-review 查看全面的代码审查检查清单。
通过技能 compose-multiplatform-patterns 查看 Kotlin Multiplatform/Flutter 互操作模式。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC