provider view model with collection of objects - flutter

I have a state model that has collection of objects that have a collection of objects inside as well (think collection of todo lists):
class StateModel extends ChangeNotifier {
List<TodoList> todoLists;
}
simplified TodoList class looks like this:
class TodoList {
int id;
List<Item> items;
}
class Item {
int id;
String name;
bool status; // true is done, false is not done
}
Now one of my Views is ListView of my todo lists (where I display only name) and that's east. But I want to have a todo list detail view (where data of single todo list is displayed) where I want to mark todo items as done (i.e. set their status to true). How should I do it? I could have method in StateModel which would find a TodoList object by id, then mark items as done. This could look something like this:
class StateModel extends ChangeNotifier {
// (...)
void markItemAsDone(listId, itemId) {
// find todo list in StateModel
// find item in given list
// mark it as done
// notifyListeners()
}
}
But this seems wrong. What I would like to have is a way to get TodoList object view model and use its methods, not StateModel methods. How should I approach this? Can I have another view model (TodoListState), and have a collection of TodoListState objects in StateModel? Is this a use case for ProxyProvider?
I hope my question is clear, let me know if this needs more explanation.

My way is when you the pop back from the detail view, you also transfer a value:
I await for the value from the pop back then you do something with the value. Here is my code
#override
Widget build(BuildContext context) {
WordModel provider = Provider.of<WordModel>(context);
return Consumer<WordModel>(
builder: (BuildContext context, value, Widget child) => InkWell(
onTap: () async {
dynamic status = await Navigator.of(context).push(PageRouteBuilder<ListFavouriteWord>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return ListFavouriteWord(value);
}));
//do something with markedValue
//provider.update(status)
},
child: Padding(
padding: const EdgeInsets.only(top: 16.0, right: 16.0),
child: Badge(
child: Icon(Icons.face),
badgeContent: Text(
"${value.counter}",
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white, fontSize: 10.0),
),
),
),
),
);
}
I have found another way to do it. You just need:
ChangeNotifierProvider.value() to use your provider in your previous route
My code:
onTap: () async {
dynamic markedValue = await Navigator.of(context).push(
PageRouteBuilder<ListFavouriteWord>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return ChangeNotifierProvider.value(
value: value,
child: ListFavouriteWord(),
);
}));
}

Related

Flutter change state of an item in a ListView

I'm trying to use a ListTile widget inside a ListView that will change depending on some information received elsewhere in the app. In order to practice, I tried to make a little Dartpad example.
The problem is as follows: I have been able to change the booleans and data behind the items, but they don't update within the ListView.builder.
I have ListTile widgets inside the ListView that I want to have four different states as follows: idle, wait, requested, and speaking.
The different states look like this, as an example:
I am trying to change the individual state of one of these items, but I haven't been able to get them to properly update within the ListView.
The ListTile code looks like this. Most of this code is responsible for just handling what the UI should look like for each state:
class UserItem extends StatefulWidget {
String name;
UserTileState userTileState;
UserItem(this.name, this.userTileState);
#override
UserItemState createState() => UserItemState();
}
class UserItemState extends State<UserItem> {
String _getCorrectTextState(UserTileState userTileState) {
switch (userTileState) {
case UserTileState.speaking:
return "Currently Speaking";
case UserTileState.requested:
return "Speak Request";
case UserTileState.wait:
return "Wait - Someone Speaking";
case UserTileState.idle:
return "Idle";
}
}
Widget _getCorrectTrailingWidget(UserTileState userTileState) {
switch (userTileState) {
case UserTileState.speaking:
return const CircleAvatar(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
child: Icon(Icons.volume_up));
case UserTileState.requested:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_up),
);
case UserTileState.wait:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_off),
);
case UserTileState.idle:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_off),
);
}
}
void kickUser(String name) {
print("Kick $name");
}
#override
Widget build(BuildContext context) {
return ListTile(
onLongPress: () {
kickUser(widget.name);
},
title: Text(widget.name,
style: TextStyle(
fontWeight: widget.userTileState == UserTileState.speaking ||
widget.userTileState == UserTileState.requested
? FontWeight.bold
: FontWeight.normal)),
subtitle: Text(_getCorrectTextState(widget.userTileState),
style: const TextStyle(fontStyle: FontStyle.italic)),
trailing: _getCorrectTrailingWidget(widget.userTileState));
}
}
enum UserTileState { speaking, requested, idle, wait }
To try and trigger a change in one of these UserTile items, I wrote a function as follows. This one should make an idle tile become a requested tile.
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
I will then run that userRequest inside my main build function inside a button, as follows:
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
Expanded(
flex: 8,
child: ListView.separated(
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: users.length,
itemBuilder: (context, index) {
return users[index];
})),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
setState(() {
userRequest("Test1");
});
},
child: const Text("Send Request")),
]))
]));
}
}
When I tap the button to set the value within the first UserTile, nothing happens.
I don't know where to put setState, and the state of the object within the ListView isn't being updated. What is the most simple solution to this problem? Provider? I've used Provider in this same situation and can't get it to work. Is there another simpler solution to this?
How can I change update the state of a specific element within a ListView?
The code that I posted has had mixed results for some. My main project actually is actually a bit more complex and involves Provider, but I tried my best to strip the code down to its simplest parts when I posted this question. I'm wondering if something else is getting in the way of this working. (maybe it's because I threw this example together using dartpad?)
In the mean time, I've discovered that adding or removing an element to the list seems to change the entire state of the list. Using this technique, instead of trying to change an individual value, I get its index, remove it, and re-add it at that index and it works like a charm. So instead of this:
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
followed by setState(() => userRequest("name");, the following seems to work instead:
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users.removeAt(index);
users.insert(index, UserItem(name, UserTileStates.requested));
}
I believe the problem lies here in _getCorrectTextState()
Create a variable, String _status;
And do something like this;
void _getCorrectTextState(UserTileState userTileState) {
String tmpState;
switch (userTileState) {
case UserTileState.speaking:
tmpState = "Currently Speaking";
case UserTileState.requested:
tmpState = "Speak Request";
case UserTileState.wait:
tmpState = "Wait - Someone Speaking";
case UserTileState.idle:
tmpState = "Idle";
}
setState(() => {_state = tmpState});
}
then in your build:
subtitle: _state;
You can use StatefulBuilder() - by using this you will get a separate state for each item in your list, that can be updated
like this:
ListView.separated(
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: users.length,
itemBuilder: (context, index) {
return StatefulBuilder(
builder: ((context, setStateItem){
//return_your_listTile_widget_here
//use setStateItem (as setState) this will update the state of selected item
setStateItem(() {
//code for update the listView item
});
})
);
})),

Having trouble to build single pages for the items on my AlphabetS collage

I'm new on flutter. I'm working on app that I want to publish and it will my first as beginner. I built an AlphabetScrollPage in a Scaffold with 56 items in it. My goal is to have a single pages describing all item individually. Like when to click on one it automatically directly you to a new page.
I'm actually having a snackBar when you click on one it's comes with a Text saying you 'clicked on this $item'. Any help will be beneficial, thank you![enter image description here][1]
You need to pass the details of the item to the page which describes the item
this is the one simple way you can do it
it isn't the best way but it works for small apps
List items = [];
for (var i = 0; i < 56; i++) {
items.add(MyItem('id $i', 'name $i'));
}
//this is you list view
ListView.builder(
itemCount: 56,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailsScreen(id: items[index].id,itemName: items[index].name,))); //this should be item id
},
child: ListTile(
title: Text('Item ${index + 1}'),
),
);
this is the item class it's very simple
class MyItem {
final String id;
final String name;
MyItem(this.id, this.name);
}
and describing page
class DetailsScreen extends StatelessWidget {
final id;
final itemName;
const DetailsScreen({Key? key, required this.id,required this.itemName}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: Text(itemName),
),
),
);
}
}
you can do it also by using Navigate with arguments then use this id to get the item details
if your app is big you should use state management package
like provider, it is very easy and useful

How to use provider to create a commit/discard changes pattern?

What would be a best practice for (provider based) state management of modal widgets in flutter, where when user makes an edit changes do not propagate to parent page until user confirms/closes modal widget. Optionally, user has a choice to discard the changes.
In a nutshell:
modal widget with OK and cancel actions, or
modal widget where changes are applied when modal is closed
Currently, my solution looks like this
Create a copy of the current state
Call flutter's show___() function and wrap widgets with a provider (using .value constructor) to expose copy of the state
If needed, update original state when modal widget is closed
Example of case #2:
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
final tmpState = MeState.from(orgState);
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => ChangeNotifierProvider<MeState>.value(
value: tmpState,
builder: (context, _) => _buildEditDialogWidgets(context)),
);
// update original state (no discard option to keep it simple)
orgState.update(tmpState);
}
But there are issues with this, like:
Where should I dispose tmpState?
ProxyProvider doesn't have .value constructor.
If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
UPDATE: In my current app I have a MultiProvider widget at the top of widget tree, that creates and provides multiple filter state objects. Eg. FooFiltersState, BarFiltersState and BazFiltersState. They are separate classes because each these three extends either ToggleableCollection<T> extends ChangeNotifier or ToggleableCollectionPickerState<T> extends ToggleableCollection<T> class. An abstract base classes with common properties and functions (like bool areAllSelected(), toggleAllSelection() etc.).
There is also FiltersState extends ChangeNotifier class that contains among other things activeFiltersCount, a value depended on Foo, Bar and Baz filters state. That's why I use
ChangeNotifierProxyProvider3<
FooFiltersState,
BarFilterState,
BazFilterState,
FiltersState>
to provide FiltersState instance.
User can edit these filters by opening modal bottom sheet, but changes to filters must not be reflected in the app until bottom sheet is closed by taping on the scrim. Changes are visible on the bottom sheet while editing.
Foo filters are displayed as chips on the bottom sheet. Bar and baz filters are edited inside a nested dialog windows (opened from the bottom sheet). While Bar or Baz filter collection is edited, changes must be reflected only inside the nested dialog window. When nested dialog is confirmed changes are now reflected on bottom sheet. If nested dialog is canceled changes are not transferred to the bottom sheet. Same as before, these changes are not visible inside the app until the bottom sheet is closed.
To avoid unnecessary widget rebuilds, Selector widgets are used to display filter values.
From discussion with yellowgray, I think that I should move all non-dependent values out of proxy provider. So that, temp proxy provider can create new temp state object that is completely independent of original state object. While for other objects temp states are build from original states and passed to value constructors like in the above example.
1. Where should I dispose tmpState?
I think for your case, you don't need to worry about it. tmpState is like a temporary variabl inside function showEditDialog()
2. ProxyProvider doesn't have .value constructor.
It doesn't need to because it already is. ProxyProvider<T, R>: T is a provider that need to listen to. In your case it is the orgState. But I think the orgState won't change the value outside of this function, so I don't know why you need it.
3. If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
you can still access the orgState inside _buildEditDialogWidgets and update it by context.read(). But I think you shouldn't use same type twice in the same provider tree (MeState)
Actually when I first see your code, I will think why you need to wrap tmpState as another provider (your _buildEditDialogWidgets contains more complicated sub-tree or something else that need to use the value in many different widget?). Here is the simpler version I can think of.
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => _buildEditDialogWidgets(context,MeState.from(orgState)),
);
}
...
Widget _buildEditDialogWidgets(context, model){
...
onSubmit(){
context.read<MeState>().update(updatedModel)
}
...
}
The simplest way is you can just provide a result when you pop your dialog and use that result when updating your provider.
import 'dart:collection';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class Item {
Item(this.name);
String name;
Item clone() => Item(name);
}
class MyState extends ChangeNotifier {
List<Item> _items = <Item>[];
UnmodifiableListView<Item> get items => UnmodifiableListView<Item>(_items);
void add(Item item) {
if (item == null) {
return;
}
_items.add(item);
notifyListeners();
}
void update(Item oldItem, Item newItem) {
final int indexOfItem = _items.indexOf(oldItem);
if (newItem == null || indexOfItem < 0) {
return;
}
_items[indexOfItem] = newItem;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(_) {
return ChangeNotifierProvider<MyState>(
create: (_) => MyState(),
builder: (_, __) => MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(
builder: (BuildContext context) => Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton(
onPressed: () => _addItem(context),
child: const Text('Add'),
),
Expanded(
child: Consumer<MyState>(
builder: (_, MyState state, __) {
final List<Item> items = state.items;
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, int index) => GestureDetector(
onTap: () => _updateItem(context, items[index]),
child: ListTile(
title: Text(items[index].name),
),
),
);
},
),
),
],
),
),
),
),
),
);
}
Future<void> _addItem(BuildContext context) async {
final Item item = await showDialog<Item>(
context: context,
builder: (BuildContext context2) => AlertDialog(
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context2),
child: const Text('Cancel'),
),
FlatButton(
onPressed: () => Navigator.pop(
context2,
Item('New Item ${Random().nextInt(100)}'),
),
child: const Text('ADD'),
),
],
),
);
Provider.of<MyState>(context, listen: false).add(item);
}
Future<void> _updateItem(BuildContext context, Item item) async {
final Item updatedItem = item.clone();
final Item tempItem = await showModalBottomSheet<Item>(
context: context,
builder: (_) {
final TextEditingController controller = TextEditingController();
controller.text = updatedItem.name;
return Container(
height: 300,
child: Column(
children: <Widget>[
Text('Original: ${item.name}'),
TextField(
controller: controller,
enabled: false,
),
TextButton(
onPressed: () {
updatedItem.name = 'New Item ${Random().nextInt(100)}';
controller.text = updatedItem.name;
},
child: const Text('Change name'),
),
TextButton(
onPressed: () => Navigator.pop(context, updatedItem),
child: const Text('UPDATE'),
),
TextButton(
onPressed: () => Navigator.pop(context, Item(null)),
child: const Text('Cancel'),
),
],
),
);
},
);
if (tempItem != null && tempItem != updatedItem) {
// Do not update if "Cancel" is pressed.
return;
}
// Update if "UPDATE" is pressed or dimissed.
Provider.of<MyState>(context, listen: false).update(item, updatedItem);
}
}

How to properly initialize a Future in Flutter Provider

so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.

Passing data between Widgets without using Navigator?

I've some struggling with flutter.
I've two widget Details screen so in the first Details, I have a list and I want to send it to the second Screen without using navigator.
So if there is anyone who can help me I will be very thankful.
Details :
List<String> _instructions = [];
class Details extends StatefulWidget {
#override
_DetailsState createState() => new _DetailsState();
}
class _DetailsState extends State<Details> {
Expanded(
child: Container(
margin: EdgeInsets.all(3),
child: OutlineButton(
highlightElevation: 21,
color: Colors.white,
shape: StadiumBorder(),
textColor: Colors.lightBlue,
child: Text(
'ENVOYER',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.normal,
color: Colors.lightBlue,
),
),
borderSide: BorderSide(
color: Colors.lightBlue,
style: BorderStyle.solid,
width: 1),
onPressed: () {
},
),
),
),
}}
so what I want is that,when I press the button I will send my list to the next widget without using navigator.
you can use the sharedpreferance which is the simple xml that belongs to the application. And this is how you can set it
Future<bool> setStringList(String key, List<String> value) =>
_setValue('StringList', key, value);
Future<bool> setStringList (
String key,
List<String> value
)
For more info here is a link
And you can get your List by
List<String> getStringList(String key) {
List<Object> list = _preferenceCache[key];
if (list != null && list is! List<String>) {
list = list.cast<String>().toList();
_preferenceCache[key] = list;
}
return list;
}
Also you can use sqflite
Suppose you have two pages, namely 'page1.dart' and 'page2.dart', both want to access the same list:
Create another dart file 'GlobalVariables.dart', inside this file, create a class gv.
Inside this class gv, create a static list by using:
static List <String> listAnyList = [];
import 'GlobalVariables.dart' in the 2 pages that need to access this list.
Now, in page1.dart and page2.dart,
you can use gv.listAnyList to access the 'Global List'.
Use 'Global Static Variables' if a variable is needed in many dart files, e.g. the 'User ID', then you can simply use gv.strUserID to access it in any pages you want.
I think a more appropriate approach should be similar to how android fragments connect to each other using bloc pattern or master-details flow.
I created an example repository to show the complete concept.
In short, the idea is to create a class with StreamController inside. Both widgets will have a reference to bloc instance. When the first widget wants to send data to the second it adds a new item to Stream. The second listen to the stream and updates its content accordingly.
Inside bloc:
StreamController<String> _selectedItemController = new BehaviorSubject();
Stream<String> get selectedItem => _selectedItemController.stream;
void setSelected(String item) {
_selectedItemController.add(item);
}
First fragment:
class FragmentList extends StatelessWidget {
final Bloc bloc;
const FragmentList(
this.bloc, {
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: bloc.selectedItem,
initialData: "",
builder: (BuildContext context, AsyncSnapshot<String> screenType) {
return ListView.builder(
itemBuilder: (context, index) {
return ListTile(
selected: bloc.items[index] == screenType.data,
title: Text(bloc.items[index]),
onTap: () {
bloc.setSelected(bloc.items[index]);
},
);
},
itemCount: bloc.items.length,
);
},
);
}
}
The second fragment:
class FragmentDetails extends StatelessWidget {
final Bloc bloc;
const FragmentDetails(
this.bloc, {
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
initialData: "Nothing selected",
stream: bloc.selectedItem,
builder: (BuildContext context, AsyncSnapshot<String> screenType) {
final info = screenType.data;
return Text(info);
},
),
);
}
}