I am building an app with flutter and the provider pattern. I have particular one ViewModel, that gets provided with Provider.of<AddressBookModel>(context).
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AddressBookViewModel>(
builder:(_) => AddressBookViewModel(),
child: Scaffold(
body: _getBody(context);
}
Widget _getBody(BuildContext context) {
AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);
// AddressBookViewModel holds a list of contact objects
// (id, name, street, starred etc.)
List<Contact> contacts = vm.contacts;
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) => ListTile(
title: Text(contacts[index].name),
trailing: contacts[index].starred
? Icon(Icons.star))
: null,
/**
* Changing one object rebuilds and redraws the whole list
*/
onLongPress: () => vm.toggleStarred(contacts[index]);
));
}
}
And the respective ViewModel
class AddressBookViewModel with ChangeNotifier {
final List<Contact> contacts;
AddressBookViewModel({this.contacts = []});
void toggleStarred(Contact contact) {
int index = contacts.indexOf(contact);
// the contact object is immutable
contacts[index] = contact.copy(starred: !contact.starred);
notifyListeners();
}
}
The problem I am facing is, once I am changing one contact object in the list with toggleStarred(),
the provider is rebuilding and redrawing the whole list. This is not necessary in my opinion, as only
the one entry needs to be rebuild. Is there any way to have a provider that is only responsible
for one list item? Or any other way to solve this problem?
When working with lists, you'll want to have a "provider" for each item of your list and extract the list item into a constant – especially if the data associated to your item is immutable.
Instead of:
final contactController = Provider.of<ContactController>(context);
return ListView.builder(
itemCount: contactController.contacts.length,
builder: (_, index) {
reurn Text(contactController.contacts[index].name);
}
)
Prefer:
final contactController = Provider.of<ContactController>(context);
return ListView.builder(
itemCount: contactController.contacts.length,
builder: (_, index) {
reurn Provider.value(
value: contactController.contacts[index],
child: const ContactItem(),
);
}
)
Where ContactItem is a StatelessWidget that typically look like so:
class ContactItem extends StatelessWidget {
const ContactItem({Key key}): super(key: key);
#override
Widget build(BuildContext context) {
return Text(Provider.of<Contact>(context).name);
}
}
Note : full code available on the end
Step 1 : extend Contact class with ChangeNotifier class
class Contact with ChangeNotifier { }
Step 2 : remove final form starred field
bool starred;
Step 3 : move toggleStarred method form AddressBookViewModel class to Contact class
void toggleStarred() {
starred = !starred;
notifyListeners();
}
Steps[1,2,3] Code Changes Review :
class Contact with ChangeNotifier {
final String name;
bool starred;
Contact(this.name, this.starred);
void toggleStarred() {
starred = !starred;
notifyListeners();
}
}
Step 4 : move ListTile to sprate StatelessWidget called ContactView
class ContactView extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile();
}
}
Step 5 : Change ListView itemBuilder method
(context, index) {
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
Step 6 : on the new StatelessWidget ContactView get Contact using Provider
final contact = Provider.of<Contact>(context);
Step 7 :change onLongPress to use the new toggleStarred
onLongPress: () => contact.toggleStarred(),
Steps[4,6,7] Code Changes Review :
class ContactView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final contact = Provider.of<Contact>(context);
print("building ListTile item with contact " + contact.name);
return ListTile(
title: Text(contact.name),
trailing: contact.starred ? Icon(Icons.star) : null,
onLongPress: () => contact.toggleStarred(),
);
}
}
Steps[5] Code Changes Review :
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
print("building ListView item with index $index");
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
},
);
Full Code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider<AddressBookViewModel>(
builder: (context) => AddressBookViewModel(),
child: HomeScreen(),
),
);
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AddressBookViewModel>(
builder: (context) => AddressBookViewModel(),
child: MaterialApp(
home: Scaffold(
body: _getBody(context),
),
),
);
}
Widget _getBody(BuildContext context) {
AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);
final contacts = vm.contacts;
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
print("building ListView item with index $index");
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
},
);
}
}
// product_item.dart
class ContactView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final contact = Provider.of<Contact>(context);
print("building ListTile item with contact " + contact.name);
return ListTile(
title: Text(contact.name),
trailing: contact.starred ? Icon(Icons.star) : null,
onLongPress: () => contact.toggleStarred(),
);
}
}
class AddressBookViewModel with ChangeNotifier {
final contacts = [
Contact("Contact A", false),
Contact("Contact B", false),
Contact("Contact C", false),
Contact("Contact D", false),
];
void addcontacts(Contact contact) {
contacts.add(contact);
notifyListeners();
}
}
class Contact with ChangeNotifier {
final String name;
bool starred;
Contact(this.name, this.starred);
void toggleStarred() {
starred = !starred;
notifyListeners();
}
}
Ref :
Simple app state management - Flutter
[Question] Nested Providers and Lists · Issue #151 · rrousselGit/provider
Related
I am trying to have a form when I fill it out will populate a ListView, but can't seem to get the list to popluate with any values.
I am using the following to have a bottom navigation:
class _AppState extends State<App> {
int _currentIndex = 0;
final List<Widget> body = [
AddNewStudent(),
StudentList(),
];
In a file that has the form looks like this:
class StudentClass {
String kidFirstName;
String kidLastName;
DateTime dateOfBirth;
int totalAttedance = 0;
int attedanceAtRank = 0;
StudentClass(
{required this.kidFirstName,
required this.kidLastName,
required this.dateOfBirth});
}
class AddNewStudent extends StatefulWidget {
#override
AddStudentScreen createState() => AddStudentScreen();
}
class AddStudentScreen extends State<AddNewStudent> {
List<StudentClass> studentList = [];
void addStudent(StudentClass newStudent) {
setState(() {
studentList.add(newStudent);
});
}
final formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
children: [
Form(
key: formKey,
child: Column(
children: [
kidsFirstNameFormField(),
kidLastNameFormField(),
kidDateofBirth(),
submitButton(),
],
),
),
],
)));
}
Widget submitButton() {
return ElevatedButton(
child: Text('Create New Student Profile'),
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
formKey.currentState?.save();
StudentClass newStudent = StudentClass(
kidFirstName: kidFirstName,
kidLastName: kidLastName,
dateOfBirth: dateOfBirth,
);
addStudent(newStudent);
formKey.currentState?.reset();
}
},
);
}
The listview builder is in its own file:
class StudentList extends StatelessWidget {
#override
Widget build(BuildContext context) {
List<StudentClass> studentList = [];
return Scaffold(
appBar: AppBar(
title: Text('Student List'),
),
body: StudentListState(
studentList: studentList,
),
);
}
}
class StudentListState extends StatefulWidget {
final List<StudentClass> studentList;
StudentListState({required this.studentList});
#override
_StudentListState createState() => _StudentListState();
}
class _StudentListState extends State<StudentListState> {
void addStudent(StudentClass student) {
setState(() {
widget.studentList.add(student);
});
}
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(20.0),
child: ListView.builder(
itemCount: widget.studentList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.studentList[index].kidFirstName),
subtitle: Text(widget.studentList[index].kidLastName),
trailing: Text(widget.studentList[index].dateOfBirth.toString()),
);
},
));
}
}
I am pretty stuck on figuring out how to pass the information over to the list to populate. I have gotten it to build with no errors but somehow I know I am not passing it correctly. I know I might have an extra list in here.
You are updating the studentList of the AddStudentScreen widget. And in the ListView you are rendering the studentList from StudentList widget which is a different variable and is always empty.
Also, you are initialising studentList inside the build function which means that on every setState() studentList will be initialised to an empty list.
Seems like you want to use the same data in multiple widgets. In such cases consider using a state manager.
For you scenario, I'd recommend you use stream_mixin.
Example:
Create student service using StoreService from stream_mixin package.
class StudentModel extends BaseModel { // NOTICE THIS
String kidFirstName;
String kidLastName;
DateTime dateOfBirth;
int totalAttedance = 0;
int attedanceAtRank = 0;
StudentModel({
required this.kidFirstName,
required this.kidLastName,
required this.dateOfBirth,
required String id,
}) : super(id: id);
}
class StudentService extends StoreService<StudentModel> { // NOTICE THIS
StudentService._();
static StudentService store = StudentService._(); // Just creating a singleton for StudentService.
}
To add student data in the store (note, this can be done anywhere in the app):
const student = new StudentModel(
// add student data here
)
StudentService.store.add(student);
Render this list of students
StreamBuilder<StudentModel>(
stream: StudentService.store.onChange,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text("No student added yet.");
}
return ListView.builder(
itemCount: StudentService.store.values.length,
itemBuilder: (context, index) {
const student = StudentService.store.values[index];
return ListTile(
title: Text(student.kidFirstName),
subtitle: Text(student.kidLastName),
trailing: Text(student.dateOfBirth.toString()),
);
},
)
},
)
Now, every time you add student data using StudentService.store.add(), it will emit an event which your SteamBuilder with stream: StudentService.store.onChange is listening and will update the UI to show the updated list.
This will also eliminate the necessity of StatefulWidget. Which means you can use only StatelessWidget unless otherwise you require StatefulWidget for something else.
I am using ValueListenableBuilder to update my UI based on the data provided to it. I am initializing the ValueNotifier with value. But when I tried to read that value it returns nothing.
This is my Notifier class code:
class AppValueNotifier
{
ValueNotifier<List<Food>> valueNotifier = ValueNotifier([]);
void updateDealsList(List<Food> list)
{
valueNotifier.value=list;
print('DEAL LIST IN CLASS: ${ valueNotifier.value}');
}
List<Food> getDealList()
{
return valueNotifier.value;
}
}
In a separate widget I am initializing the value like this:
class HomeWidgetState extends State<HomeWidget> {
AppValueNotifier appValueNotifier = AppValueNotifier();
.
.
.
assignList(List<Food> dealList)
{
appValueNotifier.updateDealsList(dealList);
}
..
..
.
}
Now in another widget class I am building my UI with this data like this:
AppValueNotifier appValueNotifier = AppValueNotifier();
Widget buildList()
{
return ValueListenableBuilder(
valueListenable: appValueNotifier.valueNotifier,
builder: (context, List<Food> value, widget) {
print(
'DEAL LIST: ${appValueNotifier.getDealList()}');
return DealsWidget(
key: menuItemsKey,
updateList: (oldIndex, newIndex, newList) {},
currentMenu: value,
menuItemNodes: [],
changeCellColor: (color, catid) {},
);
},
);
}
But it is returning empty list instead. Not that list which is being initialized at the start.
Anyone help me what is the issue here:
Thanks in advance
You should be able to initialize your ValueNotifier list either in the constructor or based on an action (i.e. a button click, as shown below). Notice how I'm providing the AppValueNotifier service using the Provider pattern, and one widget triggers the action while a separate widget listens to the changes being made.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
Provider(
create: (context) => AppValueNotifier(),
child: MyApp()
)
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Column(
children: [
TriggerWidget(),
Expanded(
child: MyWidget(),
)
]
),
),
);
}
}
class Food {
final String name;
Food({ required this.name });
}
class AppValueNotifier
{
ValueNotifier<List<Food>> valueNotifier = ValueNotifier([]);
void updateDealsList(List<Food> list)
{
valueNotifier.value = list;
print('DEAL LIST IN CLASS: ${ valueNotifier.value}');
}
List<Food> getDealList()
{
return valueNotifier.value;
}
}
class TriggerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
AppValueNotifier appValueNotifier = Provider.of<AppValueNotifier>(context, listen: false);
return TextButton(
child: const Text('Add Items!'),
onPressed: () {
appValueNotifier.updateDealsList([
Food(name: 'Food 1'),
Food(name: 'Food 2'),
]);
},
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
AppValueNotifier appValueNotifier = Provider.of<AppValueNotifier>(context, listen: false);
return ValueListenableBuilder(
valueListenable: appValueNotifier.valueNotifier,
builder: (context, List<Food> value, widget) {
var list = value;
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return Text(list[index].name);
}
);
},
);
}
}
You get this as output:
Also checkout this Gist you can run on DartPad.dev to check out how it works.
I have a list of objects, but I want to change the state of one object to "isLoading" where it will have a different title, etc.
I'm building my list view:
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return saveGame.isLoading
? buildListTileIsLoading(context, saveGame)
: buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
onTap: () => controller.process(saveGame)
);
}
The controller:
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[].obs;
void process(SaveGame saveGame) {
saveGame.working = true;
update();
}
}
Where have I gone wrong here?
edits: Added more code
So despite the fact, I'm only updating one object in the list and not modifying the content of the list (adding/removing objects) I still need to call saveGames.refresh();
An oversight on my end didn't think you'd need to refresh the entire list if you're just changing the property on one of the objects.
Good to know :)
update() is used with GetBuilder()
obs() is used with obx()
you need to make a change on list to update widgets
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/get_navigation.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetMaterialApp(
onInit: () {
Get.lazyPut(() => SaveGameController());
},
home: const HomePage(),
);
}
}
class HomePage extends GetView<SaveGameController> {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(), body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
tileColor: saveGame.working ? Colors.red : Colors.yellow,
title: Text(saveGame.name),
onTap: () => controller.process(saveGame));
}
}
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[
SaveGame(id: 0, name: 'a', working: false),
SaveGame(id: 1, name: 'b', working: false),
SaveGame(id: 2, name: 'c', working: false)
].obs;
void process(SaveGame saveGame) {
final index = saveGames.indexWhere((element) => element.id == saveGame.id);
saveGames
.replaceRange(index, index + 1, [saveGame.copyWith(working: true)]);
}
}
class SaveGame {
final int id;
final String name;
final bool working;
SaveGame({required this.id, required this.name, required this.working});
SaveGame copyWith({int? id, String? name, bool? working}) {
return SaveGame(
id: id ?? this.id,
name: name ?? this.name,
working: working ?? this.working);
}
}
I have the following flutter code. For some reason in the below code _cartProducts[index].quantity = state.quantity is correctly assigned but ProductListItem is not getting updated. Any ideas ?
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
CartBloc cartBloc;
TrendingProductBloc trendingProductBloc;
List<CartEntry> _cartProducts = [];
List<Product> trendingProducts;
#override
void initState() {
super.initState();
cartBloc = BlocProvider.of<CartBloc>(context);
trendingProductBloc = BlocProvider.of<TrendingProductBloc>(context);
cartBloc.add(GetCartProductsEvent()); // load cart items
trendingProductBloc.add(LoadTrendingProductsEvent()); // load trending items
cartBloc.stream.listen((state) {
if (state is GetCartProductsCompletedState) { //triggered when load cart entries
_cartProducts = state.cartProductsList;
}
if (state is UpdateQuantityCompletedState) { //triggered when update quantity of an cart item
int index = _cartProducts.indexWhere((item) => item.product.id == state.productId);
_cartProducts[index].quantity = state.quantity; // This has no effect
}
});
}
#override
Widget build(BuildContext context) {
super.build(context);
.....
Container(
height: 280.0,
child: BlocBuilder(
bloc: trendingProductBloc,
builder: (context, state) {
if (state is LoadTrendingProductsCompletedState) {
trendingProducts = state.productList;
return ListView.separated(
itemBuilder: (context, index) {
return ProductListItem( // This has no effect when a list item quantity changes.
cartProducts: _cartProducts,
product: trendingProducts[index],
cartBloc: cartBloc,
currentUser: currentUser,
);
},
)
}
},
),
),
}
}
You are updating the ProductListItem only when the state LoadTrendingProductsCompletedState is fired. Therefore, if you want to update the ProductListItem when the number of items in the cart changes, you need to listen to the UpdateQuantityCompletedState state too. But keep in mind that you feed the BlocBuilder with the trendingProductBloc bloc and the UpdateQuantityCompletedState belongs to the cartBloc bloc, which is a different bloc. So, you have two options:
Nest two BlocBuilders (one for every bloc)
Merge the two blocs and use only one BlocBuilder
If you choose the first option because you want to have two separated blocs, do something like this (Note the variables' name for the states of the different blocs are not the same to avoid using only the states of the inner BlocBuilder):
...
#override
Widget build(BuildContext context) {
super.build(context);
.....
Container(
height: 280.0,
child: BlocBuilder(
bloc: cartBloc,
builder: (context, cartState) {
return BlocBuilder(
bloc: trendingProductBloc,
builder: (context, trendingProductState) {
// Here you consider states from different blocs.
if (cartState is UpdateQuantityCompletedState || trendingProductState is LoadTrendingProductsCompletedState) {
trendingProducts = state.productList;
return ListView.separated(
itemBuilder: (context, index) {
return ProductListItem(
cartProducts: _cartProducts,
product: trendingProducts[index],
cartBloc: cartBloc,
currentUser: currentUser,
);
},
)
}
},
);
},
),
),
}
...
I want to the View update list item when provider function called. But it does not work.
Before changing normal View to FutureBuilder, it worked. I tried to use StreamBuilder instead, does not work either.
How can I solve this?
Here is the Provider:
class SelectArtistProvider extends ChangeNotifier {
List<Map> artists = [];
...
Future initArtistListVM() async { //for Future Builder
var allArtists =
await Firestore.instance.collection('artists').getDocuments();
List list = allArtists.documents
.map((artist) => {
'id': artist.documentID,
'name': artist.data['name'],
'image': "artist.data['image'],
'selected': "false",
})
.toList();
artists = list;
notifyListeners();
return artists;
}
void toggleSelected(Map item, int index) {
artists[index]['selected'] = !item['selected'];
notifyListeners();
}
View
// list view
class SelectArtist extends StatelessWidget {
Widget build(BuildContext context) {
final selectArtistProvider = Provider.of<SelectArtistProvider>(context);
return FutureBuilder(
future: selectArtistProvider.initArtistListVM(), // initial data from api
builder: (context, snapshot) {
return ListView.builder(
...
child: ArtistItem(index, selectArtistProvider.artists[index])
// item view
class ArtistItem extends StatelessWidget {
final int index;
final Map artist;
Widget build(BuildContext context) {
final selectArtistProvider = Provider.of<SelectArtistProvider>(context);
return GestureDetector(
onTap: () => selectArtistProvider.toggleSelected(artist, index),
child:
...
Visibility(
visible: artist['selected'],
child: SomeWidget()
...
// root view to place providers
class InitialProviders extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
...
ChangeNotifierProvider(create: (_) => SelectArtistProvider()),
],
child: ...
Solved This!
Change initArtistListVM() some part.
if (artists.isEmpty) {
artists = list;
notifyListeners();
}