flutter_bloc - 每个 Bloc 有多个存储库,还是只有一个?
Posted
技术标签:
【中文标题】flutter_bloc - 每个 Bloc 有多个存储库,还是只有一个?【英文标题】:flutter_bloc - Multiple repositories per Bloc, or only one? 【发布时间】:2020-03-12 21:40:31 【问题描述】:假设我有一个带有嵌套集合的 firestore 数据库。一所学校可以有不同的课程,每门课程有不同的部分,每个部分有不同的页面。真的很简单。
从 Firestore 架构的角度来看,这也很简单。嵌套集合优雅地解决了这个问题。
- schools
- schoolDoc1
- courses
- courseDoc1
- sections
- sectionDoc1
- pages
- pageDoc1
但现在 BLOC 使用存储库来处理数据。这就是我不清楚的地方。
所以我有一个 SchoolBloc,它具有获取学校并将其存储到 SharedPreferences 的功能。为什么使用 SharedPreferences?因为我需要使用学校文档id,所以在构造查询的时候获取CourseBloc中的所有课程。这些课程是学校文档中的嵌套集合。
Firestore.instance.collection('schools')
.document('<the school document id>')
.collection('courses').snapshot();
到目前为止一切顺利。 SchoolBloc 有两个功能。一个从firestore获取学校并将其保存到SharedPreferences中。另一个从 SharedPreferences 加载学校。这一切都可以通过一个存储库完成。
但现在变得棘手了。当我想加载 CourseBloc 中的所有课程时,我需要先检索学校文档 ID,然后才能创建查询,以获取所有课程。我将需要所有查询的学校文档 ID。因此,将 id 传递给执行查询的每个单独函数是没有意义的。
所以这就是我的大脑爆炸并开始挣扎的地方。我该如何从逻辑上解决这个问题?
每个 SchoolBloc 和 CourseBloc 都只有一个存储库,它被注入到 main.dart 文件中。
class TeachApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>(
builder: (context)
return AuthenticationBloc(
userRepository: FirebaseUserRepository(),
)..dispatch(AppStarted());
,
),
BlocProvider<SchoolBloc>(
builder: (context)
return SchoolBloc(
schoolRepository: FirebaseSchoolRepository(),
);
,
),
BlocProvider<CourseBloc>(
builder: (context)
return CourseBloc(
courseRepository: FirebaseCourseRepository(),
);
,
),
BlocProvider<SectionBloc>(
builder: (context)
return SectionBloc(
sectionRepository: FirebaseSectionRepository(),
);
,
),
],
child: MaterialApp(
...
问题
如果我需要获取学校文档 ID,除了 CourseRepository 之外,将 SchoolRepository 注入 CourseBloc 是否有意义?然后首先使用学校存储库检索学校文档ID,然后使用CourseRepository 获取所有课程?或者一个 BLOC 是否应该只有一个被注入的存储库?
或者让 CourseRepository 从 SharedPreferences 检索学校文档 ID 是否更有意义?
理解 BLOC 模式并不难,但学习设计复杂应用程序的最佳实践却非常困难,因为那里的所有示例都非常简单,或者不使用 BLOC 模式。试图解决这个问题真的很令人沮丧。
我不知道什么是好的做法,什么不是。文档很好,但也留下了很大的解释空间。
代码下方。
main.dart
这里我使用 MultiBlocProvider 来初始化块。这也是我处理导航的地方。
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:school_repository/school_repository.dart';
import 'package:teach_mob/core/blocs/authentication/authentication.dart';
import 'package:teach_mob/ui/shared/palette.dart';
import 'package:teach_mob/ui/views/course_add_view.dart';
import 'package:teach_mob/ui/views/course_view.dart';
import 'package:teach_mob/ui/views/home_view.dart';
import 'package:teach_mob/ui/views/login_failed_view.dart';
import 'package:teach_mob/ui/views/section_add_view.dart';
import 'package:teach_mob/ui/views/section_view.dart';
import 'package:user_repository/user_repository.dart';
import 'package:teach_mob/core/blocs/blocs.dart';
import 'core/constants/app_constants.dart';
import 'core/repositories/course_repository/lib/course_repository.dart';
import 'core/repositories/section_repository/lib/section_repository.dart';
void main() async
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(TeachApp());
class TeachApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>(
builder: (context)
return AuthenticationBloc(
userRepository: FirebaseUserRepository(),
)..dispatch(AppStarted());
,
),
BlocProvider<SchoolBloc>(
builder: (context)
return SchoolBloc(
schoolRepository: FirebaseSchoolRepository(),
);
,
),
BlocProvider<CourseBloc>(
builder: (context)
return CourseBloc(
courseRepository: FirebaseCourseRepository(),
);
,
),
BlocProvider<SectionBloc>(
builder: (context)
return SectionBloc(
sectionRepository: FirebaseSectionRepository(),
);
,
),
],
child: MaterialApp(
title: "TeachApp",
routes:
RoutePaths.Home: (context) => checkAuthAndRouteTo(HomeView(), context),
RoutePaths.Course: (context) => checkAuthAndRouteTo(CourseView(), context),
RoutePaths.CourseAdd: (context) => checkAuthAndRouteTo(CourseAddView(
onSave: (id, name, description, teaserImage)
BlocProvider.of<CourseBloc>(context)
.dispatch(AddCourseEvent(Course(
name: name,
description: description,
teaserImage: teaserImage
))
);
,
isEditing: false,
), context),
RoutePaths.CourseEdit: (context) => checkAuthAndRouteTo(CourseAddView(
onSave: (id, name, description, teaserImage)
BlocProvider.of<CourseBloc>(context)
.dispatch(UpdateCourseEvent(Course(
id: id,
name: name,
description: description,
teaserImage: teaserImage
))
);
,
isEditing: true
), context),
RoutePaths.Section: (context) => checkAuthAndRouteTo(SectionView(), context),
RoutePaths.SectionAdd: (context) => checkAuthAndRouteTo(SectionAddView(
onSave: (id, name)
BlocProvider.of<SectionBloc>(context)
.dispatch(AddSectionEvent(Section(
name: name
))
);
,
isEditing: false,
), context),
RoutePaths.SectionEdit: (context) => checkAuthAndRouteTo(SectionAddView(
onSave: (id, name)
BlocProvider.of<SectionBloc>(context)
.dispatch(UpdateSectionEvent(Section(
id: id,
name: name
))
);
,
isEditing: true
), context)
,
theme: ThemeData(
scaffoldBackgroundColor: Palette.backgroundColor
)
)
);
BlocBuilder<AuthenticationBloc, AuthenticationState> checkAuthAndRouteTo(Widget view, BuildContext context)
return BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state)
if (state is Authenticated)
return view;
if (state is Unauthenticated)
return LoginFailedView();
return Center(child: CircularProgressIndicator());
,
);
SchoolBloc.dart
这里有两种方法:
-
从 Firestore 加载学校并将其保存到 SharedPreferences
从共享首选项加载学校。
代码如下:
import 'dart:async';
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:school_repository/school_repository.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'package:teach_mob/core/blocs/school/school_event.dart';
import 'package:teach_mob/core/blocs/school/school_state.dart';
class SchoolBloc extends Bloc<SchoolEvent, SchoolState>
final SchoolRepository _schoolRepository;
StreamSubscription _schoolSubscription;
// Repository is injected through constructor, so that it can
// be easily tested.
SchoolBloc(@required SchoolRepository schoolRepository)
: assert(schoolRepository != null),
_schoolRepository = schoolRepository;
// Each Bloc needs an initial state. The state must be
// defined in the state file and can't be null
@override
SchoolState get initialState => SchoolInitState();
// Here we map events to states. Events can also trigger other
// events. States can only be yielded within the main function.
// yielding in listen methods will not work. Hence from a listen
// method, another event has to be triggered, so that the state
// can be yielded.
@override
Stream<SchoolState> mapEventToState(SchoolEvent event) async*
if(event is LoadSchoolAndCacheEvent)
yield* _mapLoadSchoolAndCacheEventToState(event);
else if(event is LoadSchoolFromCacheEvent)
yield* _mapLoadSchoolFromCacheEvent(event);
else if(event is SchoolLoadedFromCacheEvent)
yield* _mapSchoolLoadedFromCacheEvent(event);
else if(event is SchoolCachedEvent)
yield* _mapSchoolCachedEventToState(event);
// Get a single school and store it in shared preferences
Stream<SchoolState> _mapLoadSchoolAndCacheEventToState(LoadSchoolAndCacheEvent event) async*
final prefs = await StreamingSharedPreferences.instance;
yield SchoolDataLoadingState();
_schoolSubscription?.cancel();
_schoolSubscription = _schoolRepository.school(event.id).listen(
(school)
final schoolString = json.encode(school.toEntity().toJson());
prefs.setString("school", schoolString);
dispatch(SchoolCachedEvent(school));
);
// Load the school from shared preferences
Stream<SchoolState> _mapLoadSchoolFromCacheEvent(LoadSchoolFromCacheEvent event) async*
final prefs = await StreamingSharedPreferences.instance;
yield SchoolDataLoadingState();
final schoolString = prefs.getString("school", defaultValue: "");
schoolString.listen((value)
final Map schoolMap = json.decode(value);
final school = School(id: schoolMap["id"],
name: schoolMap["name"]);
dispatch(SchoolLoadedFromCacheEvent(school));
);
// Yield school loaded state
Stream<SchoolState> _mapSchoolLoadedFromCacheEvent(SchoolLoadedFromCacheEvent event) async*
yield SchoolDataLoadedState(event.school);
Stream<SchoolState> _mapSchoolCachedEventToState(SchoolCachedEvent event) async*
yield SchoolDataLoadedState(event.school);
CourseBloc.dart
如果您查看 _mapLoadCoursesToState 函数,您会看到,我在存储库类中定义了一个设置器来传递学校文档 ID,因为我在所有查询中都需要它。不确定是否有更优雅的方式。
这里我很困惑,不知道如何从SharedPreferences中检索学校文档id。是否认为我注入 SchoolRepository 并以这种方式检索文档?或者推荐的最佳做法是什么?
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:teach_mob/core/blocs/course/course_event.dart';
import 'package:teach_mob/core/blocs/course/course_state.dart';
import 'package:teach_mob/core/repositories/course_repository/lib/course_repository.dart';
class CourseBloc extends Bloc<CourseEvent, CourseState>
final CourseRepository _courseRepository;
StreamSubscription _courseSubscription;
// Repository is injected through constructor, so that it can
// be easily tested.
CourseBloc(@required CourseRepository courseRepository)
: assert(courseRepository != null),
_courseRepository = courseRepository;
@override
get initialState => CourseInitState();
@override
Stream<CourseState> mapEventToState(CourseEvent event) async*
if(event is LoadCoursesEvent)
yield* _mapLoadCoursesToState(event);
else if(event is CoursesLoadedEvent)
yield* _mapCoursesLoadedToState(event);
else if(event is AddCourseEvent)
yield* _mapAddCourseToState(event);
else if(event is UpdateCourseEvent)
yield* _mapUpdateCourseToState(event);
else if(event is DeleteCourseEvent)
yield* _mapDeleteCourseToState(event);
// Load all courses
Stream<CourseState> _mapLoadCoursesToState(LoadCoursesEvent event) async*
yield CoursesLoadingState();
_courseSubscription?.cancel();
_courseRepository.setSchool = "3kRHuyk20UggHwm4wrUI";
_courseSubscription = _courseRepository.courses().listen(
(courses)
dispatch(
CoursesLoadedEvent(courses),
);
,
);
Stream<CourseState> _mapCoursesLoadedToState(CoursesLoadedEvent event) async*
yield CoursesLoadedState(event.courses);
Stream<CourseState> _mapAddCourseToState(AddCourseEvent event) async*
_courseRepository.addCourse(event.course);
Stream<CourseState> _mapUpdateCourseToState(UpdateCourseEvent event) async*
_courseRepository.updateCourse(event.updatedCourse);
Stream<CourseState> _mapDeleteCourseToState(DeleteCourseEvent event) async*
_courseRepository.deleteCourse(event.course);
@override
void dispose()
_courseSubscription?.cancel();
super.dispose();
【问题讨论】:
猜我只是将两个存储库都注入到 Bloc 中。 我决定为 shared_preferences 创建一个存储库,并将该存储库注入到我需要它们的 BLOCS 中。所以我可能有多个存储库,我将其注入到 BLOC 中。我将拥有一个带有 getString 和 setString 方法的 SharedStoreRepository。如果在某个时候,我不想使用 shared_preferences 而是 sembast,我可以只交换存储库。我认为这样代码保持解耦和可测试。我不知道这是否是最佳实践,但由于文档未解决这一点,我想这就是我要走的路 好的,它实际上已记录在案,我需要新眼镜...... bloc 层可以依赖一个或多个存储库来检索构建应用程序状态所需的数据。 【参考方案1】:BLoC 是应用的业务逻辑发生的地方,因此可以在 BLoC 内部调用来自存储库的多个 Firestore 请求。对于这个用例,可以调用 Firestore 查询来获取“课程”所需的“学校”。
【讨论】:
以上是关于flutter_bloc - 每个 Bloc 有多个存储库,还是只有一个?的主要内容,如果未能解决你的问题,请参考以下文章
谁能说出 Flutter 中“flutter_bloc”和“bloc”包的区别