I'm trying to get a list of movies from api but when implementing infinite scroll pagination using ListView ScrollController infinite scroll is not working. I've checked the output from state also it is getting correctly when scroll reaches end of list but not updating in UI. only after i hot reload it shows in the UI.
Here is the Blocfile.
#injectable
class SearchMovieBloc extends Bloc<SearchMovieEvent, SearchMovieState> {
final SearchMovieRepo searchmovierepo;
SearchMovieBloc(this.searchmovierepo) : super(SearchMovieState.initial()) {
on<SearchMovieEvent>((event, emit) async {
final Either<MainFailures, List<MovieModel>> result =
await searchmovierepo.searchmovie(
moviequery: event.moviequery, page: event.page);
log(result.toString());
emit(result.fold(
(failure) =>
state.copyWith(isLoading: false, options: Some(Left(failure))),
(success) => state.copyWith(
isLoading: false,
movies: success,
options: Some(Right(success)))));
});
}
}
SearchMovieState
#freezed
class SearchMovieState with _$SearchMovieState {
const factory SearchMovieState(
{required bool isLoading,
required List<MovieModel> movies,
required Option<Either<MainFailures, List<MovieModel>>> options}) =
_SearchMovieState;
factory SearchMovieState.initial() =>
const SearchMovieState(isLoading: false, options: None(), movies: []);
}
SearchMovieEvent
#freezed
class SearchMovieEvent with _$SearchMovieEvent {
const factory SearchMovieEvent.searchmovie(
{required String moviequery, required int page}) = _SearchMovie;
}
and the UI
class SearchMovieList extends StatefulWidget {
TextEditingController text;
SearchMovieList({
Key? key,
required this.text,
}) : super(key: key);
#override
State<SearchMovieList> createState() => _SearchMovieListState();
It's been two days i've working on this issue hope someone helps me.
}
class _SearchMovieListState extends State<SearchMovieList> {
int page = 1;
ScrollController controller = ScrollController();
#override
void initState() {
controller.addListener(() {
if (controller.position.maxScrollExtent == controller.offset) {
page++;
BlocProvider.of<SearchMovieBloc>(context).add(
SearchMovieEvent.searchmovie(
moviequery: widget.text.text, page: page));
}
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<SearchMovieBloc, SearchMovieState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(
color: orange,
),
);
} else if (state.movies.isEmpty && widget.text.text.isNotEmpty) {
return const Center(
child: Text(
"No Results to Show",
style: TextStyle(color: grey, fontSize: 25),
),
);
} else if (state.movies.isNotEmpty && widget.text.text.isNotEmpty) {
/* log(state.movies.toString()); */
return ListView.separated(
controller: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: ((context, itemIndex) {
if (itemIndex < state.movies.length) {
return SearchTile(
ismovie: true,
overview: state.movies[itemIndex].overview!,
id: state.movies[itemIndex].movieid!,
heading: state.movies[itemIndex].title == null
? state.movies[itemIndex].name!
: state.movies[itemIndex].title!,
rating:
state.movies[itemIndex].rating!.toStringAsFixed(1),
image: state.movies[itemIndex].posterPath != null
? "$posterhead${state.movies[itemIndex].posterPath}"
: null,
year: state.movies[itemIndex].releasedate == null ||
state.movies[itemIndex].releasedate!.length < 5
? "_"
: state.movies[itemIndex].releasedate!
.substring(0, 4));
} else {
return const Center(
child: CircularProgressIndicator(color: orange));
}
}),
separatorBuilder: (context, index) => const Divider(
height: 4.0,
),
itemCount: state.movies.length);
} else {
return const SizedBox();
}
},
);
}
}
use the equatable package or write the == operator manually. However, I have not tried using the freezed package together with equatable
class SearchMovieState extends Equatable {
const SearchMovieState(
{required this.isLoading, required this.movies, required this.options});
final bool isLoading;
final List<MovieModel> movies;
final Option<Either<MainFailures, List<MovieModel>>> options;
#override
List<Object?> get props => [isLoading, movies, options];
}
I am using equatable. For my case, props wasn't enough to emit the same state. I had to include identityHashCode on props.
abstract class TestState extends Equatable {
const TestState();
#override
List<Object?> get props => [];
}
class ErrorState extends TestState{
final Map? message;
const ErrorState({this.message});
#override
List<Object?> get props => [message, identityHashCode(this)];
}
I recommend using Equatable https://pub.dev/packages/equatable to override the == operator. So instead of checking for the same address, Equatable checks if the object and its values are equal.
And you have to be sure that you are requesting more data and not the same data
I recommend you debug this line of code
final Either<MainFailures, List<MovieModel>> result = await searchmovierepo.searchmovie(moviequery: event.moviequery, page: event.page);
Just to be sure that the data is different
Use a fancy bool operator to trick the block to emit the state. use the below code.
In your state add a new field change state and initialize with false.
#freezed
class SearchMovieState with _$SearchMovieState {
const factory SearchMovieState(
{required bool isLoading,
required bool changeState, // add this field here
required List<MovieModel> movies,
required Option<Either<MainFailures, List<MovieModel>>> options}) =
_SearchMovieState;
factory SearchMovieState.initial() =>
const SearchMovieState(isLoading: false, changeState: false, options: None(), movies: []); // initialize the changefield with false
}
on the BlockFile invert the changeState on success
emit(result.fold(
(failure) =>
state.copyWith(isLoading: false, options: Some(Left(failure))),
(success) => state.copyWith(
isLoading: false,
changeState: !state.changeState, // toggle the changeStatefield
movies: success,
options: Some(Right(success)))));
This Worked for me.
Related
I have some data that I want to fetch when the page loads up.
Below is the code for fetching the data in the screen
class _HighSchoolScreenState extends State<HighSchoolScreen> {
late PagingController<int, HighSchool> _pagingController;
#override
void initState() {
_pagingController = context.read<HighSchoolsBloc>().pageController;
_pagingController.addPageRequestListener(
(pageKey) {
context.read<HighSchoolsBloc>().add(
FetchHighSchools(page: pageKey, category: widget.category!),
);
},
);
super.initState();
}
I am using the infinite_scroll_pagination package to lazy load the data in the UI
Widget build(BuildContext context) => PagedListView<int, HighSchool>(
addAutomaticKeepAlives: false,
shrinkWrap: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<HighSchool>(
animateTransitions: true,
newPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
firstPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
itemBuilder: (context, item, index) => SchoolsContent(
item: item,
theme: theme,
isIos: isIos,
),
),
),
Below is also my bloc for the data
class HighSchoolsBloc extends Bloc<HighSchoolsEvent, HighSchoolsState> {
final String token = Hive.box('user').get(kToken);
bool hasNextPage = true;
late HighSchoolRepo _highSchoolRepo;
final PagingController<int, HighSchool> pageController =
PagingController(firstPageKey: 0);
Future<void> _fetchPage(int pageKey, FetchHighSchools event) async {
try {
final results = await _highSchoolRepo.get(
page: event.page,
category: event.category,
token: token,
);
hasNextPage = results['hasNextPage'];
final List<HighSchool> newItems = results['schools'];
if (!hasNextPage) {
pageController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
pageController.appendPage(newItems, nextPageKey);
}
} catch (error) {
pageController.error = 'error';
}
}
HighSchoolsBloc(this._highSchoolRepo) : super(InitailState()) {
on<FetchHighSchools>((event, emit) {
print('new ${event.category}');
_fetchPage(event.page, event);
});
}
}
So the real issue is whenever visit the screen, the data fetches correctly and shows on the screen(UI) but when I leave the screen and press on another category, it should fetch data based on the different category now but it is not event fetching anything again. it just shows the same data that was fetched previously
Below is the states for my bloc
#immutable
abstract class HighSchoolsState extends Equatable {
#override
List<Object?> get props => [];
}
// ignore_for_file: public_member_api_docs, sort_constructors_first
class InitailState extends HighSchoolsState {}
class HighSchoolFetchError extends HighSchoolsState {
late final String error;
HighSchoolFetchError(this.error);
#override
List<Object?> get props => [error];
}
PLEASE NOT THAT LOADING AND ERROR ARE HANDLED BY THE PACKAGE SO THERE'S NO NEED TO MAKE IT'S RELATIVE STATES
ALSO, ONE MORE ERROR I AM FACING IS SOMETIMES, WHEN I SCROLL THROUGH THE DATA GIVEN IT GIVES ME AN ERROR OF
This widget has been unmounted, so the State no longer has a context (and should be considered defunct). // It appears on line 32 which is where the initstate it.
Simplified example using a Todo App approach where data is submitted from different pages and bloc reacts to it subscribing to a stream.
View, I have four pages.
Page A: Shows a ListView of Todos.
Page B-1: Shows a Form to update the overview data related to a Todo.
Page B-2: Shows a ListView of Actions that has a Todo.
Page C: Shows a Form to update the data related to an Action.
Logic, I have four blocs. CollectionBloc which subscribes to a stream of data using Hive and it is supposed to emit states every time there is an update in the repository. Also, EditTodoBloc and ActionBloc which submit data to the same repository. TodoBloc is for managing a Todo in general.
EditTodoBloc
----------> Page B-1
Page A ----------> | ActionBloc
CollectionBloc ----------> Page B-2 ----------> Page C
TodoBloc
Models
#HiveType(typeId: 0)
class Action extends Equatable {
Action({this.id, this.name});
#HiveField(0)
String id;
#HiveField(1)
String name;
#override
List<Object?> get props => [id, name];
}
#HiveType(typeId: 1)
class Todo extends Equatable {
Todo({this.id, this.actions});
#HiveField(0)
String id;
#HiveField(1)
String name;
#HiveField(2)
List<Action> actions;
...
#override
List<Object?> get props => [id, name, actions];
}
Database / Repository
class HiveDatabase {
late Box<List<Todo>> todos;
...
Stream<List<Todo>> watchTodos() {
return todos
.watch()
.map((event) => todos.values.toList())
.startWith(todos.values.toList());
}
Future<void> saveTodo(Todo todo) async {
await todos.put(todo.id, todo);
}
Future<void> saveAction(Todo todo, Action action) async {
todo.actions.add(action);
await todos.put(todo.id, todo);
}
}
Blocs
EditTodoBloc:
class EditTodoBloc extends Bloc<EditTodoEvent, EditTodoState> {
EditTodoBloc({
required TodosRepository todosRepository,
required Todo? todo,
}) : _todosRepository = todosRepository, super(EditTodoState(todo)) {
on<TodoSubmitted>(_onTodoSubmitted);
}
...
Future<void> _onTodoSubmitted(
TodoSubmitted event,
Emitter<EditTodoState> emit,
) async {
emit(state.copyWith(status: EditTodoStaus.loading));
try {
await _todosRepository.saveTodo(state.todo!);
emit(state.copyWith(status: EditTodoStaus.success));
} catch (e) {}
}
}
class EditTodoState extends Equatable {
final EditTodoStatus status;
final Todo? todo;
...
}
TodoBloc:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc({
required TodosRepository todosRepository,
required Todo todo,
}) : _todosRepository = todosRepository, super(TodoState(todo)) {
...
}
}
class TodoState extends Equatable {
final TodoStatus status;
final Todo todo,
final List<Action> actions;
...
}
Action Bloc:
class ActionBloc extends Bloc<ActionEvent, ActionState> {
ActionBloc({
required TodosRepository todosRepository,
required Todo todo,
required Action? action,
}) : _todosRepository = todosRepository, super(ActionState(todo, action)) {
on<ActionSubmitted>(_onActionSubmitted);
}
...
Future<void> _onActionSubmitted(
ActionSubmitted event,
Emitter<ActionState> emit,
) async {
emit(state.copyWith(status: ActionStatus.loading));
try {
await _todosRepository.saveAction(todo, state.action!);
emit(state.copyWith(status: ActionStatus.success));
} catch(e) {}
}
}
class ActionState extends Equatable {
final ActionStatus status;
final Todo todo,
final Action? action;
...
}
And the problem is here.
CollectionBloc can't persist the state of the bloc when I submit data by adding an event from Page C (ActionBloc). Unlike when sending data from Page B-1 (EditTodoBloc) which works successfully.
CollectionBloc:
class CollectionBloc extends Bloc<CollectionEvent, CollectionState> {
CollectionBloc({
required TodosRepository todosRepository,
}) : super(CollectionState()) {
on<CollectionRequested>(_onCollectionRequested);
}
...
Future<void> _onCollectionRequested(
CollectionRequested event,
Emitter<CollectionState> emit,
) async {
emit(state.copyWith(status: TodoStatus.loading));
await emit.forEach<List<Todo>>(
_todosRepository.watchTodos(),
onData: (todos) {
print('newTodos: ${todos}');
print('oldTodos: ${state.todos}');
// Why oldTodos shows the same modified todo list (with its actions)
// as the one returned from onData
// Page-B2 does not update coming back from Page-C unless I pop up to
// Page-A and then push to Page-B2.
return state.copyWith(status: CollectionStatus.success, todos: todos);
},
onError: (_, __) => state.copyWith(status: CollectionStatus.failure),
);
}
}
class CollectionState extends Equatable {
final CollectionStatus status;
final List<Todo> todos;
...
}
Page A:
class PageA extends StatelessWidget {
const PageA({Key? key}) : super(key: key);
...
#override
Widget build(BuildContext context) {
final todos = context.watch<CollectionBloc>().state.todos;
return ListView(
children: [
for (final todo in todos) ...[
ListTile(
title: Text(todo.name),
onTap: () {
Navigator.of(context).push(
PageB2.route(todo),
);
},
),
],
],
);
}
}
All in all, I would like to be able to show the list of actions updated when popping back from submitting the form in Page C to Page B-2.
class PageB2 extends StatelessWidget {
const PageB2({Key? key}) : super(key: key);
static Route<void> route(Todo todo) {
return MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => TodoBloc(
todoRepository: context.read<TodosRepository>(),
todo: todo,
),
child: const PageB2(),
),
);
}
#override
Widget build(BuildContext context) {
final todos = context.watch<CollectionBloc>().state.todos;
final todo = todos.firstWhere((element) => element.id == state.todo.id);
return BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
return ListView.separated(
itemCount: todo.actions.length,
separatorBuilder: (context, index) => const Divider(height: 8),
itemBuilder: (context, index) {
return ListTile(
title: Text(todo.actions[index].name),
onTap: () {
Navigator.of(context).push(
PageC.route(todo, todo.actions[index]),
);
},
);
},
);
},
);
}
}
I think I don't know how to apply emit.forEach() in this case. XD
I'm currently learning and converting my code to BLoc pattern. Before I'm using flutter_pagewise ^1.2.3 for my infinite scroll using Future<> but I don't know how to use it using bloc or is it compatible with it.
So now I'm trying infinite_scroll_pagination: ^2.3.0 since it says in its docs that it supports Bloc. But I don't understand the example code in the docs for bloc. Can you give me a simple example of how to use it with bloc? I'm currently using flutter_bloc: ^6.1.3.
Here are my bloc script:
class TimeslotViewBloc extends Bloc<TimeslotViewEvent, TimeslotViewState> {
final GetTimeslotView gettimeslotView;
TimeslotViewBloc({this.gettimeslotView}) : super(TimeslotViewInitialState());
#override
Stream<TimeslotViewState> mapEventToState(
TimeslotViewEvent event,
) async* {
if (event is GetTimeslotViewEvent) {
yield TimeslotViewLoadingState();
final failureOrSuccess = await gettimeslotView(Params(
id: event.id,
date: event.date,
));
yield* _eitherLoadedOrErrorState(failureOrSuccess);
}
}
Stream<TimeslotViewState> _eitherLoadedOrErrorState(
Either<Failure, List<TimeslotViewEntity>> failureOrTrivia,
) async* {
yield failureOrTrivia.fold(
(failure) => TimeslotViewErrorState(
message: _mapFailureToMessage(failure), failure: failure),
(result) => TimeslotViewLoadedState(result),
);
}
//Bloc Events----------------------------------------
abstract class TimeslotViewEvent extends Equatable {
const TimeslotViewEvent();
#override
List<Object> get props => [];
}
class GetTimeslotViewEvent extends TimeslotViewEvent {
final String id;
final String date;
final int offset;
final int limit;
GetTimeslotViewEvent(
{this.id,
this.date,
this.offset,
this.limit});
}
//Bloc States----------------------------------------
abstract class TimeslotViewState extends Equatable {
const TimeslotViewState();
#override
List<Object> get props => [];
}
class TimeslotViewLoadingState extends TimeslotViewState {}
class TimeslotViewLoadedState extends TimeslotViewState {
final List<TimeslotViewEntity> records;
TimeslotViewLoadedState(this.records);
#override
List<Object> get props => [records];
}
UPDATE: Here is the revised code from Davii that works for me
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _timeLotBloc,
child: BlocListener<TimeslotViewBloc, TimeslotViewState>(
listener: (context, state) {
if (state is TimeslotViewLoadedState) {
//Save record count instead of records list
totalRecordCount += state.records.length;
final _next = 1 + totalRecordCount;
final isLastPage = state.records.length < PAGE_SIZE;
if (isLastPage) {
_pagingController.appendLastPage(state.records);
} else {
_pagingController.appendPage(state.records, _next);
}
}
if (state is TimeslotViewErrorState) {
_pagingController.error = state.error;
}
},
//Removed pagedListview from bloc builder
child: PagedListView<int, TimeslotViewEntity>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<TimeslotViewEntity>(
itemBuilder: (context, time, index) => TimeslotViewEntityListItem(
character: time,
),
),
),),
);
}
class PaginatedList extends StatefulWidget {
const PaginatedList({Key? key}) : super(key: key);
#override
_PaginatedListState createState() => _PaginatedListState();
}
class _PaginatedListState extends State<PaginatedList> {
//*bloc assuming you use getIt and injectable
late final _timeLotBloc = getIt<TimeslotViewBloc>();
List<TimeslotViewEntity> records = [];
//*initialize page controller
final PagingController<int, TimeslotViewEntity> _pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
//*so at event add list of records
_pagingController.addPageRequestListener(
(pageKey) => _timeLotBloc
.add(GetTimeslotViewEvent(records: records, offset: pageKey,limit: 10)),
);
}
#override
void dispose() {
super.dispose();
_timeLotBloc.close();
_pagingController.dispose();
}
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _timeLotBloc,
child: BlocListener<TimeslotViewBloc, TimeslotViewState>(
listener: (context, state) {
if (state is TimeslotViewLoadedState) {
records =state.records;
//forget about existing record
//about the last page, fetch last page number from
//backend
int lastPage = state.lastPage
final _next = 1 + records.length;
if(_next>lastPage){
_pagingController.appendLastPage(records);
}
else{
_pagingController.appendPage(records, _next);
}
}
if (state is TimeslotViewErrorState) {
_pagingController.error = state.error;
}
},child: BlocBuilder<TimeslotViewBloc,TimeslotViewState>(
builder: (context,state)=> PagedListView<int, TimeslotViewEntity>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<TimeslotViewEntity>(
itemBuilder: (context, time, index) => TimeslotViewEntityListItem(
character: time,
),
),
),),
),
);
}
}
now on the bloc event class
class GetTimeslotViewEvent extends TimeslotViewEvent {
final String id;
final String date;
final int offset;
final int limit;
//add this on event
final List<TimeslotViewEntity> records;
GetTimeslotViewEvent({
this.id,
this.date,
this.offset,
this.limit,
required this.records,
});
}
on state class
class TimeslotViewLoadedState extends TimeslotViewState {
final List<TimeslotViewEntity> records;
final List<TimeslotViewEntity> existingRecords;
TimeslotViewLoadedState(this.records, this.existingRecords);
#override
List<Object> get props => [records, existingRecords];
}
and on bloc now
yield* _eitherLoadedOrErrorState(failureOrSuccess,event);
Stream<TimeslotViewState> _eitherLoadedOrErrorState(
Either<Failure, List<TimeslotViewEntity>> failureOrTrivia,
GetTimeslotViewEvent event,
) async* {
yield failureOrTrivia.fold(
(failure) => TimeslotViewErrorState(
message: _mapFailureToMessage(failure), failure: failure),
//existing records from the event,
(result) => TimeslotViewLoadedState(result,event.records),
);
}
yap this method worked on me
I update state bloc builder context like this context.read<SurveyBloc>().add(SurveyModeChanged(mode: 'draft')); in the bloc file state changing is triggered but value always null. the last 2days I struck with this someone please help to resolve this issue.
if (event is SurveyModeChanged) {
print('mode==>');
print(state.mode);
yield state.copyWith(mode: state.mode);
}
This is Survey screen file
class SurveyView extends StatefulWidget {
#override
State<StatefulWidget> createState() => _SurveyViewState();
}
class _SurveyViewState extends State<SurveyView> {
#override
Widget build(BuildContext context) {
final sessionCubit = context.read<SessionCubit>();
return BlocProvider(
create: (context) => SurveyBloc(
user: sessionCubit.selectedUser ?? sessionCubit.currentUser,
surveyId: '4aa842ff-2b7d-4364-9669-29c200a3fe9b',
dataRepository: context.read<DataRepository>(),
),
child: BlocListener<SurveyBloc, SurveyState>(
listener: (context, state) {},
child: Scaffold(
backgroundColor: Color(0xFFF2F2F7),
appBar: _appbar(),
body: stepFormContainer(context),
resizeToAvoidBottomInset: false,
),
),
);
}
Widget saveButton() {
return BlocBuilder<SurveyBloc, SurveyState>(builder: (context, state) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: ElevatedButton.icon(
onPressed: () {
context
.read<SurveyBloc>()
.add(SurveyModeChanged(mode: 'draft'));
},
label: Text('Save')));
});
}
}
This is my Survey event code
abstract class SurveyEvent {}
class SurveyResultChanged extends SurveyEvent {
final String surveyResult;
SurveyResultChanged({this.surveyResult});
}
class SurveyModeChanged extends SurveyEvent {
final String mode;
SurveyModeChanged({this.mode});
}
class SurveyIdChanged extends SurveyEvent {
final String surveyId;
SurveyIdChanged({this.surveyId});
}
class SaveSurveyChanges extends SurveyEvent {}
Survey State dart
class SurveyState {
final User user;
final FormSubmissionStatus formSubmissionStatus;
final String surveyId;
final String mode;
final String surveyResult;
SurveyState(
{#required User user,
#required String surveyId,
String mode,
String surveyResult,
this.formSubmissionStatus = const InitialFormStatus()})
: this.user = user,
this.surveyId = surveyId,
this.mode = mode,
this.surveyResult = surveyResult;
SurveyState copyWith({
User user,
FormSubmissionStatus formSubmissionStatus,
String surveyId,
String mode,
String surveyResult,
}) {
return SurveyState(
user: user ?? this.user,
surveyId: surveyId ?? this.surveyId,
mode: mode ?? this.mode,
surveyResult: surveyResult ?? this.surveyResult,
formSubmissionStatus:
formSubmissionStatus ?? this.formSubmissionStatus);
}
}
SurveyBloc.dart
class SurveyBloc extends Bloc<SurveyEvent, SurveyState> {
final DataRepository dataRepository;
SurveyBloc({
#required User user,
#required String surveyId,
this.dataRepository,
}) : super(SurveyState(user: user, surveyId: surveyId));
#override
Stream<SurveyState> mapEventToState(SurveyEvent event) async* {
if (event is SurveyModeChanged) {
print('mode==>');
print(state.mode);
yield state.copyWith(mode: state.mode);
}
}
}
class SurveyBloc extends Bloc<SurveyEvent, SurveyState> {
final DataRepository dataRepository;
SurveyBloc({
#required User user,
#required String surveyId,
this.dataRepository,
}) : super(SurveyState(user: user, surveyId: surveyId));
#override
Stream<SurveyState> mapEventToState(SurveyEvent event) async* {
if (event is SurveyModeChanged) {
print('mode==>');
print(state.mode);
// This is where the problem occurs. You are emitting the state
// value again and again which is null. Change this:
yield state.copyWith(mode: state.mode);
// into this:
yield state.copyWith(mode: event.mode);
}
}
}
I'm starting to learn Flutter/Dart by building a simple Todo app using Provider, and I've run into a state management issue. To be clear, the code I've written works, but it seems... wrong. I can't find any examples that resemble my case enough for me to understand what the correct way to approach the issue is.
This is what the app looks like
It's a grocery list divided by sections ("Frozen", "Fruits and Veggies"). Every section has multiple items, and displays a "x of y completed" progress indicator. Every item "completes" when it is pressed.
TheGroceryItemModel looks like this:
class GroceryItemModel extends ChangeNotifier {
final String name;
bool _completed = false;
GroceryItemModel(this.name);
bool get completed => _completed;
void complete() {
_completed = true;
notifyListeners();
}
}
And I use it in the GroceryItem widget like so:
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
GroceryItem(this.model);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryItemModel>(builder: (context, groceryItem, child) {
return ListTile(
title: Text(groceryItem.name),
leading: groceryItem.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked)
onTap: () => groceryItem.complete();
})
);
}
}
The next step I want is to include multiple items in a section, which tracks completeness based on how many items are completed.
The GroceryListSectionModel looks like this:
class GroceryListSectionModel extends ChangeNotifier {
final String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
// THIS RIGHT HERE IS WHERE IT GETS WEIRD
items.forEach((item) {
item.addListener(notifyListeners);
});
// END WEIRD
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
}
And I use it in the GroceryListSection widget like so:
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
final ValueChanged<bool> onChanged;
GroceryListSection(this.model, this.onChanged);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.map((groceryItemModel) =>
GroceryItem(groceryItemModel)).toList()
)
);
}
)
);
}
}
The Problems:
It seems weird to have a ChangeNotifierProvider and a Consumer in both Widgets. None of the examples I've seen do that.
It's definitely wrong to have the GroceryListSectionModel listening to changes on all the GroceryItemModels for changes to propagate back up the tree. I don't see how that can scale right.
Any suggestions? Thanks!
this ist not a nested Provider, but i think in your example it is the better way..
only one ChangeNotifierProvider per section ("Frozen", "Fruits and Veggies") is defined
the complete() function from a ItemModel is in the GroceryListSectionModel() and with the parameter from the current List Index
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
// final ValueChanged<bool> onChanged;
GroceryListSection(this.model);
#override
Widget build(BuildContext context) {
return new ChangeNotifierProvider<GroceryListSectionModel>(
create: (context) => GroceryListSectionModel(model.name, model.items),
child: new Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.asMap().map((i, groceryItemModel) => MapEntry(i, GroceryItem(groceryItemModel, i))).values.toList()
)
);
}
)
);
}
}
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
final int index;
GroceryItem(this.model, this.index);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(model.name),
leading: model.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked),
onTap: () => Provider.of<GroceryListSectionModel>(context, listen: false).complete(index),
);
}
}
class GroceryListSectionModel extends ChangeNotifier {
String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
// complete Void with index from List items
void complete(int index) {
this.items[index].completed = true;
notifyListeners();
}
}
// normal Model without ChangeNotifier
class GroceryItemModel {
final String name;
bool completed = false;
GroceryItemModel({this.name, completed}) {
this.completed = completed == null ? false : completed;
}
}