Reputation: 3557
Let's say I have a firestore database with nested collections. A school can have different courses, each course has different sections and each section has different pages. Really simple one would think.
It's also simple from a firestore schema perspective. Nested collections solve this elegantly.
- schools
- schoolDoc1
- courses
- courseDoc1
- sections
- sectionDoc1
- pages
- pageDoc1
But now BLOCs come into the picture with repositories, to handle the data. And that's where things get unclear to me.
So I have a SchoolBloc that has a function to get the school and store it into SharedPreferences. Why SharedPreferences? Because I need to use the school document id, when I construct the query to get all the courses in the CourseBloc. The courses are a nested collection inside the school document.
Firestore.instance.collection('schools')
.document('<the school document id>')
.collection('courses').snapshot();
All good till now. The SchoolBloc has two functions. One to get the school from firestore and save it into SharedPreferences. Another one to load the school from SharedPreferences. This can all be done with one repository.
But now it gets tricky. When I want to load all the courses inside the CourseBloc, I will need to retrieve the school document id first, before I can create the query, to get all the courses. And I will need the school document id for all the queries. So it doesn't make sense to pass the id to each individual function that does a query.
So this is where my brain explodes and I start to struggle. How do I solve this logically?
Each the SchoolBloc and the CourseBloc have exactly one repository, that is injected in the main.dart file.
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(
...
Questions
If I need to get the school document id, does it make sense to inject the SchoolRepository, in addition to the CourseRepository into the CourseBloc? And then first retrieve the school document id, with the school repository, and after that get all the courses with the CourseRepository? Or should a BLOC only have one repository, that is injected?
Or would it make more sense to have the CourseRepository retrieve the school document id from SharedPreferences?
It's not that hard to understand the BLOC pattern, but it's incredibly hard, to learn the best practices to design a complex app, because all the examples that are out there are very simple, or don't use the BLOC pattern. It's really frustrating trying get my head around this.
I don't know what's good practice and what isn't. The documentation is good, but also leaves a lot of room for interpretation.
Below the code.
main.dart
Here I use MultiBlocProvider to initialize the blocs. It's also where I handle navigation.
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
Here there are two methods:
Here the code:
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
If you look at the _mapLoadCoursesToState function, you will see, that I have defined a setter in the repository class to pass the school document id, as I will need it in all the queries. Not sure if there is a more elegant way.
Here I am puzzled and don't know, how to retrieve the school document id from SharedPreferences. Is it thought, that I inject the SchoolRepository and retrieve the document this way? Or what is the recommended best practice to do this?
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();
}
}
Upvotes: 4
Views: 7202
Reputation: 10519
The BLoC is where the business logic for the app happens so it's fine to have multiple Firestore requests from the Repository called inside the BLoC. For this use case, it's fine to call a Firestore query to fetch the 'school' needed for the 'courses'.
Upvotes: 1