How to write this Flutter code more efficiently? - flutter

As you can see in first part I'm checking that a certain value contains in a document from Firestore and returns a boolean value. Now I'm calling that function in a build and based on that return value I'm changing a chip color (second part).
Now the problem is maybe because I'm calling it in a build function so its being called continuously and on that build and it costing me a ton of reads in Firestore or maybe the function is inefficient. How can I write this more efficiently?
checkAtt(String name, id , date) async{
var ref = _db.collection('subjects').document(id).collection('Att').document(date);
var docref = await ref.get();
return docref.data.containsKey(name)
?true
:false;
}
class PresentChip extends StatefulWidget {
final candidate;
PresentChip(
this.candidate, {
Key key,
}) : super(key: key);
#override
_PresentChipState createState() => _PresentChipState();
}
class _PresentChipState extends State<PresentChip> {
var isSelected = false;
var c = false;
#override
Widget build(BuildContext context) {
final SelectSub selectSub = Provider.of<SelectSub>(context);
final Date date = Provider.of<Date>(context);
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});
return Container(
child: ChoiceChip(
label: Text('Present'),
selected: isSelected,
onSelected: (selected) {
db.gibAtt(
widget.candidate, selectSub.selectsub, date.datenew.toString());
setState(() {
isSelected = selected;
});
},
backgroundColor: !c ?Colors.red :Colors.green ,
selectedColor: !c ?Colors.red :Colors.green ,
));
}
}

Assuming you only want to read once from firestore, you need a FutureBuilder.
return Container(
child: FutureBuilder(
future: db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew),
builder: (context, snapshot) {
if(snapshot.hasData)
return ChoiceChip(
...
backgroundColor: !snapshot.data ?Colors.red :Colors.green,
selectedColor: !snapshot.data ?Colors.red :Colors.green,
);
//Return another widget if the future has no data
return Text('Future has no data');
}
)
);
If you need your UI to react to changes from firestore, use a StreamBuilder.
You can remove the following bloc from your build method:
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});

Related

How to sync stream variable in BlocState with the TextFormField

I am not able to figure out the right architecture of getting this done using firestore streams and flutter_bloc.
My goal :
I have a logged in user and I want to store the user details in the collection with the document id same as logged in user id and want to listen to changes, Throughout the program.
Meanwhile i would want to even store user details (if doesn't exist) and update user details.
My approach:
I am having a usermodel in UserState and i am listening to userModel via state using BlocBuilder.
Problem:
As i am using BlocBuilder my setState isn't working and TextFormField isn't working as it says beginBatchEdit on inactive InputConnection
Code:
UserCubit.dart
class UserCubit extends Cubit<UserState> {
final _firestore = FirebaseFirestore.instance;
final User? _currentUser = FirebaseAuth.instance.currentUser;
UserCubit() : super(UserInitialState()) {
emit(UserMainLoadingState());
_firestore --> listening to stream and updating state
.collection("sample")
.doc(_currentUser?.uid)
.snapshots()
.listen((event) {
event.exists
? emit(UserExists(sample: SampleModel.fromjson(event.data()!, event.id)))
: {emit(UserNotExists())};
});
}
Future<void> addSampleUser({required SampleModel sample}) async {
emit(UserSideLoadingState());
_firestore
.collection('sample')
.doc(_currentUser?.uid)
.set(sample.toJson())
.then((value) => emit(UserSavedUpdatedState()));
}
}
UserState.dart
abstract class UserState extends Equatable {
final SampleModel? sampleModel;
const UserState({this.sampleModel});
#override
List<Object?> get props => [sampleModel];
}
class UserExists extends UserState {
const UserExists({required SampleModel sample}) : super(sampleModel: sample);
}
Widget.dart (Save/Update User Details)
class _MyWidgetState extends State<MyWidget> {
// #override
var fullNameKey;
TextEditingController? fullNameController;
bool _formChecked = false;
TextEditingController? phoneNumberController;
Avatar? selectedAvatar;
#override
void initState() {
fullNameController = TextEditingController();
fullNameKey = GlobalKey<FormState>();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<UserCubit, UserState>(listener: (context, state) {
if (state is UserSavedUpdatedState) {
print("Saved/Updated User");
context.goNamed(Routes.profileMain);
}
}, builder: (context, state) {
// Here i am trying to get details from the state
selectedAvatar = state.sampleModel?.avatar == "boy"
? Avatar.boy
: state.sampleModel?.avatar == "boy"
? Avatar.girl
: null;
fullNameController!.text = state.sampleModel?.name ?? "";
if (state is UserMainLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.girl; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.girl)),
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.boy; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.boy))
],
),
Form(
key: fullNameKey,
child: TextFormField(
// BlocBuilder even freezes TextFormField
autovalidateMode: _formChecked
? AutovalidateMode.always
: AutovalidateMode.disabled,
validator: (value) {
if (value == null ||
fullNameController?.text.trim() == "") {
return "Name cannot be empty";
}
if (value.length < 3) {
return "Username must be greater than 3 characters";
}
return null;
},
controller: fullNameController,
decoration: const InputDecoration(
labelText: "Full Name",
),
)).marginDown(),
FilledButton(
onPressed: () {
setState(() {
_formChecked = true;
});
if (fullNameKey.currentState!.validate() &&
selectedAvatar != null) {
SampleModel sample = SampleModel(
name: fullNameController!.text,
avatar: selectedAvatar == Avatar.boy ? "boy" : "girl");
BlocProvider.of<UserCubit>(context)
.addSampleUser(sample: sample);
}
},
child: const Text("Submit"),
)
],
));
}),
);
}
}
As soon as the submit button is clicked it erases the entire text and validator gets activated. Avatar selection doesn't work as well.
What is the best way to achieve the desired function using streams, flutter_bloc, Suggestions would be greatly appreciated
as far as I can see you pre-select the avatar based on the emitted state. However, I do not see that you return the selection via an event/function to the bloc/cubit. So this is needed in order to send the updated avatar with the next emit.
From what I can see, I would also possibly exchange the abstract class state with a class state implementing Equatable and the simply always copyWith the state for any updates. This way you always have the same UserState - no need for if and else if for state selection, however, the data of the state changes based on the situation. I think for a user bloc/cubit this makes the lifecycle a bit easier
UPDATE:
IconButton(
onPressed: () {
setState(() {
context.read<UserCubit>.updateUser(selectedAvatar: Avatar.boy);
selectedAvatar = Avatar.boy; -- > possibly no longer needed if returned from Cubit
});
},
icon: const Icon(Icons.boy))
As for the state management, a state can look like this:
class TaskListState extends Equatable {
const TaskListState({
this.status = DataTransStatus.initial,
this.taskList = const [],
this.filter,
this.error,
this.editThisTaskId,
});
final DataTransStatus status;
final List<TaskListViewmodel> taskList;
final TaskListFilter? filter;
final String? error;
final String? editThisTaskId;
TaskListState copyWith({
DataTransStatus Function()? status,
List<TaskListViewmodel> Function()? taskList,
TaskListFilter Function()? filter,
String Function()? error,
String? Function()? editThisTaskId,
}) {
return TaskListState(
status: status != null ? status() : this.status,
taskList: taskList != null ? taskList() : this.taskList,
filter: filter != null ? filter() : this.filter,
error: error != null ? error() : this.error,
editThisTaskId: editThisTaskId != null
? editThisTaskId() : this.editThisTaskId,
);
}
#override
List<Object?> get props => [
status,
taskList,
filter,
error,
editThisTaskId,
];
}
which you use - in this case with a Stream - like this:
await emit.forEach<dynamic>(
_propertiesRepository.streamTasks(event.propertyId),
onData: (tasks) {
return state.copyWith(
status: () => DataTransStatus.success,
taskList: () => List.from(
tasks.map((t) => TaskListViewmodel.fromDomain(t))),
);
},
onError: (_, __) {
return state.copyWith(
status: () => DataTransStatus.failure,
);
},
);

shared preferences does not save radio button checkmark in Flutter

I implemented the shared preferences package in my Flutter app, with a list widget as radio button, that only save the language preference and not the checkmark.
So when i close the Language screen and come back, the language checkmark goes the the default one even if the language, saved in shared preferences is French or Italian.
This is my Language screen:
class LanguagesScreen extends StatefulWidget {
const LanguagesScreen({Key? key}) : super(key: key);
#override
State<LanguagesScreen> createState() => _LanguagesScreenState();
}
class Item {
final String prefix;
final String? helper;
const Item({required this.prefix, this.helper});
}
var items = [
Item(prefix: 'English', helper: 'English',), //value: 'English'
Item(prefix: 'Français', helper: 'French'),
Item(prefix: 'Italiano', helper: 'Italian'),
];
class _LanguagesScreenState extends State<LanguagesScreen> {
var _selectedIndex = 0;
final _userPref = UserPreferences();
var _selecLangIndex;
int index = 0;
final List<String> entries = <String>['English', 'French', 'Italian'];*/
//init shared preferences
#override
void initState() {
super .initState();
_populateField();
}
void _populateField() async {
var prefSettings = await _userPref.getPrefSettings();
setState((){
_selecLangIndex = prefSettings.language;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(...
),
body: CupertinoPageScaffold(
child: Container(
child: SingleChildScrollView(
child: CupertinoFormSection.insetGrouped(
children: [
...List.generate(items.length, (index) => GestureDetector(
onTap: () async {
setState(() => _selectedIndex = index);
if (index == 0){
await context.setLocale(Locale('en','US'));
_selecIndex = Language.English;
}
else if (index == 1){
await context.setLocale(Locale('fr','FR'));
_selecIndex = Language.French;
}
child: buildCupertinoFormRow(
items[index].prefix,
items[index].helper,
selected: _selectedIndex == index,
)
)),
TextButton(onPressed:
_saveSettings,
child: Text('save',
)
buildCupertinoFormRow(String prefix, String? helper, {bool selected = false,}) {
return CupertinoFormRow(
prefix: Text(prefix),
helper: helper != null
? Text(helper, style: Theme.of(context).textTheme.bodySmall,)
:null, child: selected ? const Icon(CupertinoIcons.check_mark,
color: Colors.blue, size: 20,) :Container(),
);
}
void _saveSettings() {
final newSettings = PrefSettings(language:_selecIndex);
_userPref.saveSettings(newSettings);
Navigator.pop(context);
}
}
this is the UserPreference:
class UserPreferences {
Future saveSettings(PrefSettings prefSettings) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setInt('language' , prefSettings.language.index );
}
Future<PrefSettings> getPrefSettings() async {
final preferences = await SharedPreferences.getInstance();
final language = Language.values[preferences.getInt('language') ?? 0 ];
return PrefSettings(language: language);
}
}
enum Language { English, French, Italian}
class PrefSettings{
final Language language;
PrefSettings (
{required this.language});
}
I'm betting that the issue is in initState. You are calling _populateField, but it doesn't complete before building because it's an async method, and you can't await for it: so the widget gets build, loading the default position for the checkmark, and only after that _populateField completes...but then it's too late to show the saved data correctly.
In my experience, if I have not already instantiated a SharedPreferences object somewhere else in the code, I use this to load it:
class _LanguagesScreenState extends State<LanguagesScreen> {
[...]
#override
Widget build(BuildContext context) {
return FutureBuilder(
//you can put any async method here, just be
//sure that you use the type it returns later when using 'snapshot.data as T'
future: await SharedPreferences.getInstance(),
builder: (context, snapshot) {
//error handling
if (!snapshot.hasData || snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
var prefs= snapshot.data as SharedPreferences;
//now you have all the preferences available without need to await for them
return Scaffold((
[...]
);
EDIT
I started writing another comment, but there are so many options here that there wasn't enough space.
First, the code I posted should go in your _LanguagesScreenState build method. The FutureBuilder I suggested should wrap anything that depends on the Future you must wait for to complete. I put it up at the root, above Scaffold, but you can move it down the widgets' tree as you need, just remember that everything that needs to read the preferences has to be inside the FutureBuilder.
Second, regarding SharedPreferences.getInstance(), there are two ways: the first is declaring it as a global variable, and loading it even in the main method where everything starts. By doing this you'll be able to reference it from anywhere in your code, just be careful to save the changes everytime is needed. The second is to load it everytime you need, but you'll end up using a FutureBuilder a lot. I don't know if any of these two options is better than the other: the first might have problems if somehow the SharedPreferences object gets lost, while the second requires quite more code to work.

Flutter: How to change state of sibling widget widget?

I have already tried the solution here. My code already depended on a class passed on a parent class.
import 'package:flutter/cupertino.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../widgets/cupertino_modal_button_row.dart';
import '../widgets/simple_widgets.dart';
import '../models/match.dart';
class AddScoreSection extends StatefulWidget {
AddScoreSection({
this.set,
this.partnerIds,
Key key,
}) : super(key: key);
final MatchSet set;
final List<String> partnerIds;
#override
_AddScoresState createState() => _AddScoresState();
}
class _AddScoresState extends State<AddScoreSection> {
String _partnerText;
#override
Widget build(BuildContext context) {
final set = widget.set;
final _hostId = FirebaseAuth.instance.currentUser.uid;
String _partnerId = set.hostTeam != null
? set.hostTeam.firstWhere(
(element) => element != FirebaseAuth.instance.currentUser.uid)
: null;
Future<String> _partnerName(String partnerId) async {
if (partnerId == null) {
return null;
}
final userData = await FirebaseFirestore.instance
.collection('users')
.doc(partnerId)
.get();
return userData['name']['full_name'];
}
print(widget.set.visitingGames);
return CupertinoFormSection(
header: const Text('Set'),
children: [
CupertinoModalButtonRow(
builder: (context) {
return CupertinoActionSheet(
title: _partnerText == null
? const Text('Select a Partner')
: _partnerText,
actions: widget.partnerIds
.map((partner) => CupertinoActionSheetAction(
child: FutureBuilder<String>(
future: _partnerName(partner),
builder: (ctx, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return SimpleWidget.loading10;
}
return Text(snapshot.data);
},
),
onPressed: () async {
final partnerTemp = await _partnerName(partner);
_partnerId = partner;
setState(() {
_partnerText = partnerTemp;
});
set.addToHostTeam = [_partnerId, _hostId];
set.addToVisitTeam = widget.partnerIds
.where((element) =>
widget.set.hostTeam.contains(element))
.toList();
print(set.hostTeam);
Navigator.of(context).pop();
},
))
.toList());
},
buttonChild:
_partnerText == null ? 'Select your Partner' : _partnerText,
prefix: 'Your Partner'),
ScoreEntryRow(
setsData: widget.set,
prefix: 'Team Score',
),
ScoreEntryRow(
prefix: 'Opponent Score',
setsData: widget.set,
isHostMode: false,
),
],
);
}
}
class ScoreEntryRow extends StatefulWidget {
ScoreEntryRow({
Key key,
#required this.setsData,
#required this.prefix,
this.isHostMode: true,
}) : super(key: key);
final MatchSet setsData;
final String prefix;
final bool isHostMode;
#override
_ScoreEntryRowState createState() => _ScoreEntryRowState();
}
class _ScoreEntryRowState extends State<ScoreEntryRow> {
#override
Widget build(BuildContext context) {
final ValueNotifier _games = widget.isHostMode
? ValueNotifier<int>(widget.setsData.hostGames)
: ValueNotifier<int>(widget.setsData.visitingGames);
List<int> scoreList = List.generate(9, (index) => index + 1);
return CupertinoModalButtonRow(
builder: (context) {
return SizedBox(
height: 300,
child: ListView.builder(
itemCount: scoreList.length,
itemBuilder: (context, i) {
return CupertinoButton(
child: Text(scoreList[i].toString()),
onPressed: () {
setState(() {
if (widget.isHostMode) {
widget.setsData.setHostGames = scoreList[i];
widget.setsData.setVisitGames = 9 - scoreList[i];
return;
}
widget.setsData.setVisitGames = scoreList[i];
widget.setsData.setHostGames = 9 - scoreList[i];
});
});
}),
);
},
buttonChild: widget.isHostMode
? widget.setsData.hostGames == null
? '0'
: widget.setsData.hostGames.toString()
: widget.setsData.visitingGames == null
? '0'
: widget.setsData.visitingGames.toString(),
prefix: widget.prefix,
);
}
}
The code for the custom class is:
class MatchSet {
MatchSet(this.setData);
final Map<String, dynamic> setData;
List<String> hostTeam;
List<String> visitingTeam;
int hostGames;
int visitingGames;
set addToHostTeam(List<String> value) {
if (setData.values.every((element) => element != null))
hostTeam = setData['host_team'];
hostTeam = value;
}
set addToVisitTeam(List<String> value) {
if (setData.values.every((element) => element != null))
visitingTeam = setData['visiting_team'];
visitingTeam = value;
}
set setHostGames(int value) {
if (setData.values.every((element) => element != null))
hostGames = setData['host_games'];
hostGames = value;
}
set setVisitGames(int value) {
if (setData.values.every((element) => element != null))
visitingGames = setData['visiting_games'];
visitingGames = value;
}
Map<List<String>, int> get matchResults {
return {
hostTeam: hostGames,
visitingTeam: visitingGames,
};
}
List<String> get winningTeam {
if (matchResults.values.every((element) => element != null)) {
return null;
}
return matchResults[hostTeam] > matchResults[visitingTeam]
? hostTeam
: visitingTeam;
}
String result(String userId) {
if (matchResults.values.every((element) => element != null)) {
return null;
}
if (winningTeam.contains(userId)) {
return 'W';
}
return 'L';
}
bool get isUpdated {
return hostGames != null && visitingGames != null;
}
}
The state does not change for the sibling as you can see in the video. I have also trying the whole button in a ValueListenableBuilder, nothing changes.
What am I doing wrong? By all my knowledge the sibling row widget should rebuild, but it is not rebuilding.
You can use multiple approaches
the solution that you point out is a callback function and it works fine ... you use a function that when it is called it would go back to its parent widget and do some stuff there(in your case setState)
but It does not seem that you are using this it.
the other one is using StateManagement. you can use GetX , Provider, RiverPod,... .
in this one, you can only rebuild the widget that you want but in the first solution, you have to rebuild the whole parent widget.
both solutions are good the second one is more professional and more efficient but i think the first one is a must-know too

How change value item in CheckBoxListTile?

I am trying to update the check of the CheckBoxListTile but I am not getting the desired result.
This is my code:
Widget build(BuildContext context) {
return Query(
options: QueryOptions(document: getVehiclesTypeQueryDoc),
builder: (result, {fetchMore, refetch}) {
if (result.isLoading && result.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
newVehiclesType = [];
final _vehiclesTypeJson = result.data['tipoVehiculos'] as List<dynamic>;
for (final i in _vehiclesTypeJson) {
var productMap = {
'id': i['id'],
'nombre': i['nombre'],
'imagenActivo': i['imagenActivo'],
'isSelected': true
};
newVehiclesType.add(productMap);
}
final List<dynamic> test = newVehiclesType;
print('test: $test');
return ListView.builder(
itemCount: test.length,
shrinkWrap: true,
itemBuilder: (context, index) {
print(index);
print('VALO: ${test[index]['isSelected']}');
return CheckboxListTile(
title: Text(test[index]['nombre'].toString()),
value: test[index]['isSelected'],
onChanged: (bool newValue) {
setState(() {
test[index]['isSelected'] = newValue;
});
},
);
},
);
},
);
}
I created a newVehiclesType variable, the query did not give me a variable to use the check for.
I am new to flutter.
this is the code that i use in my application hopefully hopefully it will help you
value: isCheckedEdit.contains(_ingredientList[index].nameIngredient),
onChanged: (value) {
if (value) {
setState(() {
isCheckedEdit
.add(_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
} else {
setState(() {
isCheckedEdit.remove(
_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
}
},
);
Angular has an amazing documentation that teaches every concept in detail (60%). I highly recommend you to visit the the documentation site. There are examples and explanations for each topic.
https://angular.io/guide/reactive-forms
Thank you so much for bringing such a beautiful question up. Angular is love.
class VehiclesList extends StatefulWidget {
#override
_VehiclesListState createState() => _VehiclesListState();
}
class _VehiclesListState extends State<VehiclesList> {
List<dynamic> test;
#override
Widget build(BuildContext context) {
// your code
// when result has data, instead of
// final List<dynamic> test = newVehiclesType;
// do this
test = newVehiclesType;
}
}
What this basically does is, it uses test to hold the widget state. In your code, test is local to your build function and would be reinitialized as many times build function runs.
This way it would not reinitialize on widget rebuilds when setState() is called.

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