What is best practise with flutter provider to lazy load data - flutter

So when using provider with flutter, the main problem is that if want to specify the provider where its descendants need its value and its good until you don't use navigation if we use navigation then we have to use builder to provide new context so for most people provide all provider before material app in widget hierarchy but the problem with that, suppose i want to load a data which will load on the second screen so what i m suppose to put in initial model class because i don't think providing null values is a good way.
So my question is what is the best to lazy load data without using null or default empty values.
see below code for some context to my problem.
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
const title = 'GPA Calculator';
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => StudentManager()),
ChangeNotifierProvider(create: (context) => StudentCourseManager()),
],
child: MaterialApp(
title: title,
home: const HomeScreen(title: title),
),
);
}
}
class StudentManager extends ChangeNotifier {
List<Student> _list = [];
List<Student> get list => _list;
void add(Student student) {
_list.add(student);
notifyListeners();
}
void remove(int index) {
_list.removeAt(index);
notifyListeners();
}
void update(
int index,
Student student,
) {
_list[index] = student;
notifyListeners();
}
}
class StudentCourseManager extends ChangeNotifier {
late int studentIndex;
late StudentManager studentManager;
StudentCourseManager({
required this.studentIndex,
required this.studentManager,
});
Student? get student => studentManager.list[studentIndex];
List<StudentCourse> get list => student != null ? student!.courseList : [];
void add(StudentCourse studentCourse) {
student!.addCourse(studentCourse);
studentManager.update(studentIndex, student!);
notifyListeners();
}
void delete(int index) {
student!.deleteCourse(index);
studentManager.update(studentIndex, student!);
notifyListeners();
}
void update(
int index,
StudentCourse studentCourse,
) {
student!.updateCourse(index, studentCourse);
studentManager.update(studentIndex, student!);
notifyListeners();
}
}

According to #Nico Spencer comment, i tried ChangeNotifierProxyProvider and i found it is improvement to my old way because its simply my code and also auto-update provider here is the code that i changed to.
//...
providers: [
ChangeNotifierProvider<StudentManager>(
create: (context) => StudentManager(),
),
ChangeNotifierProxyProvider<StudentManager, StudentCourseManager>(
create: (context) => StudentCourseManager(),
update: (context, result, previous) {
return previous!..student = result.currentStudent;
},
),
],
class StudentCourseManager extends ChangeNotifier {
late Student student;
List<StudentCourse> get list => student.courseList;
void add(StudentCourse studentCourse) {
student.addCourse(studentCourse);
notifyListeners();
}
void delete(int index) {
student.deleteCourse(index);
notifyListeners();
}
void update(
int index,
StudentCourse studentCourse,
) {
student.updateCourse(index, studentCourse);
notifyListeners();
}
}
// ...
return StudentTile(
student: student,
onDelete: () => studentManager.remove(index),
onTap: () {
studentManager.currentStudent = student;
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) => const CoursesScreen()),
);
},

Related

Why emit.forEach(..) doesn't persist the state of the bloc in this case?

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

cubit returns a null value

I am facing very weird problem. i am using bloc with freezed, injectable and dartz. i just need to get data from SQl database and display it when a today page opened.
The code of UI is:
class TodayPage extends HookWidget {
const TodayPage();
#override
Widget build(BuildContext context) {
return BlocProvider<ScheduledNotesCubit>(
lazy:false,
create: (context) => getIt<ScheduledNotesCubit>()
..countDoneNoteOutOfAllNotes()
..retrieveData(),
child: BlocBuilder<ScheduledNotesCubit, ScheduledNotesState>(
builder: (context, state) {
return ListView.builder(
itemCount: state.maybeMap(
orElse: () {}, getNotesCount: (g) => g.noteCount),
itemBuilder: (BuildContext context, int index) {
return Text(
"${state.maybeMap(orElse: () {}, getNotes: (notes) {
return notes.getNotes[index]['content'];
})}",
);
},
);
},
),
);
}
}
The code of state is:
#freezed
class ScheduledNotesState with _$ScheduledNotesState {
const factory ScheduledNotesState.initial() = _Initial;
const factory ScheduledNotesState.getNotes({required List<Map<String, dynamic>> getNotes}) = _GetNotes;
const factory ScheduledNotesState.getNotesCount({required int noteCount}) = _GetNotesCount;
const factory ScheduledNotesState.getCountDoneNoteOutOfAllNotes({required String getCountDoneNoteOutOfAllNotes}) = _GetCountDoneNoteOutOfAllNotes;
const factory ScheduledNotesState.updateIsDoneNote({required int updateIsDoneNote}) = _UpdateIsDoneNote;
}
The code of cubit is:
#injectable
class ScheduledNotesCubit extends Cubit<ScheduledNotesState> {
ScheduledNotesCubit(this._noteRepository)
: super(const ScheduledNotesState.initial());
final NoteRepository _noteRepository;
// retrieve data
void retrieveData() async {
return emit(ScheduledNotesState.getNotes(
getNotes: await _noteRepository.retrieveData()));
}
}
This Cubit Does not return a value in listView, instead it returns null values, But When i try to do so it works!!!!!!
the updated cubit code is:
#injectable
class ScheduledNotesCubit extends Cubit<ScheduledNotesState> {
ScheduledNotesCubit(this._noteRepository)
: super(const ScheduledNotesState.initial());
final NoteRepository _noteRepository;
// retrieve data
void retrieveData() async {
var d= await _noteRepository.retrieveData(); //-->updated
var x= d[1]['content']; //-->updated
print("\n $x \n") ; // -->updated
return emit(ScheduledNotesState.getNotes(
getNotes: await _noteRepository.retrieveData()));
}
}
Can you try add lazy to false for BlocProvider, and update this code:
void retrieveData() {
_noteRepository.retrieveData().then((value) {
emit(ScheduledNotesState.getNotes(getNotes: value));
});
}
The solution is create a data class for the cubit, instead of creating a sealed classes.

how to use infinite_scroll_pagination for bloc pattern

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

Should I use final in models with equatable and flutter_bloc to distinguish 2 states?

I'm creating an app where you login and go to a page where you have a list of your restaurants, you have also a form where you can add a new restaurant.
This part works.
The problem is that when i click add the restaurant is added in firestore correctly, but the list doesn't refresh. I usually yield 2 states, a LoadingState and a LoadedRestaurantsListState, but with the last version of flutter_bloc this trick doesn't work, seems like just the last state yielded is received, but the previous was LoadedRestaurantsListState, so they are equals and the blocbuilder ignores the second one. So I've to use the equatable's props to distinguish the 2 states, but in the equatable documentation is written: "Note: Equatable is designed to only work with immutable objects so all member variables must be final".
So I've to make all the model's fields final, but if I do it how can i modify just one o two fields when I need it to?
What is the best practice?
If someone has examples, or videos, etc it would be very appreciated.
Thanks in advance
Without props
FirebaseBloc.dart
Stream<FirebaseState> mapEventToState(
FirebaseEvent event,
) async* {
print("event firebase ${event.runtimeType.toString()}");
if (event is CreateRestaurantFirebaseEvent) {
yield LoadingState();
await _databaseService.createRestaurant(event.restaurant, event.user);
List<Restaurant> restaurantsList = await _databaseService
.loadRestaurantsList(event.user.restaurantsIDsList);
yield LoadedRestaurantsListState(restaurantsList);
}
if (event is LoadRestaurantsListEvent) {
List<Restaurant> restaurantsList =
await _databaseService.loadRestaurantsList(event.restaurantsIDs);
yield LoadedRestaurantsListState(restaurantsList);
}
FirebaseState.dart
class LoadingState extends FirebaseState {
#override
List<Object> get props => [];
}
class LoadedRestaurantsListState extends FirebaseState {
List<Restaurant> restaurantsList;
LoadedRestaurantsListState(this.restaurantsList);
#override
List<Object> get props => [];
}
view.dart
class RestaurantSelectionScreen extends StatefulWidget {
final User user;
RestaurantSelectionScreen({
#required this.user,
});
#override
_RestaurantSelectionScreenState createState() =>
_RestaurantSelectionScreenState();
}
class _RestaurantSelectionScreenState extends State<RestaurantSelectionScreen> {
FirebaseBloc _firebaseBloc;
#override
void initState() {
super.initState();
_firebaseBloc = FirebaseBloc();
_firebaseBloc.add(LoadRestaurantsListEvent(widget.user.restaurantsIDsList));
}
#override
Widget build(BuildContext context) {
return BlocProvider<FirebaseBloc>(
create: (context) => _firebaseBloc,
child: Scaffold(
body: SingleChildScrollView(
child: Center(
child: BlocBuilder(
cubit: _firebaseBloc,
builder: (context, state) {
print("state ${state.runtimeType.toString()}");
if (state is InitialFirebaseState) {
return CircularProgressIndicator();
} else if (state is LoadedRestaurantsListState) {
return buildUI(state);
} else if (state is LoadingState) {
return CircularProgressIndicator();
} else {
return _CreateRestaurantFormWidget(widget.user);
}
},
),
),
),
),
);
}

Flutter Provider: How to notify a model that a change happened on a model it contains?

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;
}
}