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 模式。试图解决这个问题真的很令人沮丧。

我不知道什么是好的做法,什么不是。文档很好,但也留下了很大的解释空间。

代码下方。

ma​​in.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_bloc?

使用 flutter_bloc 库有啥缺点

谁能说出 Flutter 中“flutter_bloc”和“bloc”包的区别

flutter_bloc :使 initialState 方法异步

由于某种原因不能使用flutter_bloc

需要帮助了解 flutter_bloc 如何注入 bloc