Riverpod state update does not rebuild the widget - flutter

I'm using StateProvider<List<String>> to keep track of user taps on the Tic Tac Toe board.
Actual board is a widget that extends ConsumerWidget and consists of tap-able GridView.
Within the onTap event of GridViews child - following is invoked to update the state:
ref.read(gameBoardStateProvider.notifier).state[index] = 'X';
For some reason this does not invoke widget rebuild event. Due to this I cannot see the 'X' in the GridView item which was tapped.
However, if I add additional "simple" StateProvider<int> and invoke it as well within the same onTap event then the widget gets rebuilt and I can see the 'X' in the GridView.
I am not even using or displaying this additional state provider but for some reason it invokes rebuild while my intended provided doesn't.
final gameBoardStateProvider = StateProvider<List<String>>((ref) => List.filled(9, '', growable: false));
final testStateProvider = StateProvider<int>((ref) => 0); //dummy state provider
class Board extends ConsumerWidget {
const Board({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final gameBoard = ref.watch(gameBoardStateProvider);
final testState = ref.watch(testStateProvider);
return Expanded(
child: Center(
child: GridView.builder(
itemCount: 9,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
shrinkWrap: true,
itemBuilder: ((BuildContext context, int index) {
return InkWell(
onTap: () {
//With this line only the widget does not get refreshed - and I do not see board refreshed with added 'X'
ref.read(gameBoardStateProvider.notifier).state[index] = 'X';
//??? If I add this line as well - for some reason the widget get refreshed - and I see board refreshed with added 'X'
ref.read(testStateProvider.notifier).state++;
},
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.white)),
child: Center(
child: Text(gameBoard[index]),
),
),
);
}),
),
),
);
}
}

Once you do
ref.read(gameBoardStateProvider.notifier).state[index] = 'X'
Means changing a single variable. In order to update the UI, you need to provide a new list as State.
You can do it like
List<String> oldState = ref.read(gameBoardStateProvider);
oldState[index] = "X";
ref
.read(gameBoardStateProvider.notifier)
.update((state) => oldState.toList());
Or
List<String> oldState = ref.read(gameBoardStateProvider);
oldState[index] = "X";
ref.read(gameBoardStateProvider.notifier).state =
oldState.toList();
Why testStateProvider works:
Because it contains single int as State where gameBoardStateProvider contains List<String> as State.
Just updating single variable doesn't refresh ui on state_provider, You need to update the state to force a refresh
More about state_provider
You can also check change_notifier_provider

The reason the ""simple"" StateProvider triggers a rebuild and your actual doesn't is because you aren't reassigning its value.
StateProviders works like this:
StateProvider is just a StateNotifierProvider that uses a simple implementation of a StateNotifier that exposes its get state and set state methods (and an additional update method);
StateNotifier works like this: an actual state update consists of a reassignment. Whenever state gets reassigned it triggers its listeners (just like ChangeNotifier would do). See immutability patterns.
This means that, since you're exposing a List<String>, doing something like state[0] = "my new string will NOT trigger rebuilds. Only actions such as state = [... anotherList]; will do.
This is desirable, since StateNotifier pushes you to use immutable data, which is can be good pattern in the frontend world.
Instead, your int StateProvider will basically always trigger an update, since chances are that when you need to alter its state, you need to reassign its value.
For your use case you can do something like:
state[i] = 'x';
state = [...state];
This forces a re-assignment, and as such it will trigger the update.
Note: since you're implementing a tic-tac-toe, it is maybe desirable to handle mutable data for that grid. Try using a ChangeNotifier, implement some update logic (with notifiyListeners()) and exposing it with a ChangeNotifierProvider.
Riverpod actually advises against mutable patterns (i.e. against ChangeNotifiers), but, as there is no silver bullet in Computer Science, your use case might be different.

Big thanks to both Yeasin and venir for clarifying what it means to reassign the state and the difference between simple and complex types.
While keeping the initial StateProvider I've replaced
ref.read(gameBoardStateProvider.notifier).state[index] = 'X';
with
var dataList = ref.read(gameBoardStateProvider);
dataList[index] = 'X';
ref.read(gameBoardStateProvider.notifier).state = [...dataList];
And it works now - the state gets updated and the widget rebuilt.
I've also tried the StateNotifier approach and it also works - although it seems more convoluted to me :) I'm pasting the code bellow as it can maybe benefit someone else to see the example.
class GameBoardNotifier extends StateNotifier<List<String>> {
GameBoardNotifier() : super(List.filled(9, '', growable: false));
void updateBoardItem(String player, int index) {
state[index] = player;
state = [...state];
}
}
final gameBoardProvider = StateNotifierProvider<GameBoardNotifier, List<String>>((ref) => GameBoardNotifier());
class Board extends ConsumerWidget {
const Board({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final gameBoard = ref.watch(gameBoardProvider);
return Expanded(
child: Center(
child: GridView.builder(
itemCount: 9,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
shrinkWrap: true,
itemBuilder: ((BuildContext context, int index) {
return InkWell(
onTap: () {
ref.read(gameBoardProvider.notifier).updateBoardItem('X', index);
},
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.white)),
child: Center(
child: Text(gameBoard[index]),
),
),
);
}),
),
),
);
}
}

Related

Global key is not restoring state sometimes of child widget

Basically, in this example, I am using global key to restore value of by child,
This work,
But sometimes, it appears that the global key does not restore the value of the child items.
To make it clear, I have added a checkbox widget and a slider widget to the first and last pages of this code (green and red).
When I change the slider value on the first page and try to see it on the third page, it works fine.
Same thing if I change checkbox value (like true) on first page and then click on third page it shows mostly true, but sometimes does not.
I am unable to understand the reason behind this issue. Please refer to the following gif to better understand my question.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
final _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Global Key Example")),
body: PageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
color: Colors.green,
child: ActionPage(_key),
);
break;
case 1:
return Container(
color: Colors.blue,
child: Text("Blank Page"),
);
break;
case 2:
return Container(
color: Colors.red,
child: ActionPage(_key),
);
break;
default:
throw "Not found";
}
},
),
);
}
}
class ActionPage extends StatefulWidget {
#override
_ActionPageState createState() => _ActionPageState();
ActionPage(key) : super(key: key);
}
class _ActionPageState extends State<ActionPage> {
bool _switchValue = false;
double _sliderValue = 0.5;
#override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
value: _switchValue,
onChanged: (v) {
setState(() => _switchValue = v);
},
),
Slider(
value: _sliderValue,
onChanged: (v) {
setState(() => _sliderValue = v);
},
)
],
);
}
}
I'd recommened you to not use a GlobalKey here. Make sure to pass the needed data not the widget sate. If you take a look at the documentation the following pitfalls are listed:
GlobalKeys should not be re-created on every build. They should
usually be long-lived objects owned by a State object, for example.
Creating a new GlobalKey on every build will throw away the state of
the subtree associated with the old key and create a new fresh subtree
for the new key. Besides harming performance, this can also cause
unexpected behavior in widgets in the subtree. For example, a
GestureDetector in the subtree will be unable to track ongoing
gestures since it will be recreated on each build.
Instead, a good practice is to let a State object own the GlobalKey,
and instantiate it outside the build method, such as in
State.initState.
So you have to make sure that two widgets with the same key are never rendered on the screen at the same time. With a PageView this obviously can happen since you can display (eventhough there might be a transition state) two of your widgets with the same key at the same time. This can mess things up. Also they changed quite a bit in the past implementation wise if I recall correctly.
You should either pass the value directly to your widget or use something like an InheritedWidget to be safe.
Generally speaking I'd always try to avoid using GlobalKeys because they are quite complex and can have side effects which might not be always fully comprehensible. Also there are good alternatives.

Flutter/Riverpod: UI not updating on state change within a StateNotifierProvider

The UI of my app is not updating when I know for a fact the state is changing. I am using the watch method from Riverpod to handle this, but the changes don't take effect unless I do a hot reload.
I have a class HabitListStateNotifier with methods to add/remove habits from the list:
class HabitListStateNotifier extends StateNotifier<List<Habit>> {
HabitListStateNotifier(state) : super(state ?? []);
void startAddNewHabit(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) {
return NewHabit();
});
}
void addNewHabit(String title) {
final newHabit = Habit(title: title);
state.add(newHabit);
}
void deleteHabit(String id) {
state.removeWhere((habit) => habit.id == id);
}
}
And here is the provider for this:
final habitsProvider = StateNotifierProvider(
(ref) => HabitListStateNotifier(
[
Habit(title: 'Example Habit'),
],
),
);
Here is how the HabitList (the part of the UI not updating) is implemented:
class HabitList extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final habitList = watch(habitsProvider.state);
/////////////not updating/////////////
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return HabitCard(
habit: habitList[index],
);
},
itemCount: habitList.length,
);
/////////////not updating/////////////
}
}
And finally, the HabitCard (what the HabitList is comprised of):
class HabitCard extends StatelessWidget {
final Habit habit;
HabitCard({#required this.habit});
#override
Widget build(BuildContext context) {
/////////////function in question/////////////
void deleteHabit() {
context.read(habitsProvider).deleteHabit(habit.id);
}
/////////////function in question/////////////
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(devHeight * 0.03),
),
color: Colors.grey[350],
elevation: 3,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HabitTitle(
title: habit.title,
),
Consumer(
builder: (context, watch, child) => IconButton(
padding: EdgeInsets.all(8),
icon: Icon(Icons.delete),
/////////////function in question/////////////
onPressed: deleteHabit,
/////////////function in question/////////////
),
),
],
),
],
),
);
}
}
When I press the delete icon in a HabitCard, I know the Habit is being removed from the list, but the change is not reflecting in the UI. However, when I do a hot reload, it disappears as expected. What am I doing wrong here?
Since StateNotifierProvider state is immutable, you need to replace the data on CRUD operations by using state = - which is what will trigger UI updates as per docs.
Then assign new data using state = <newData>
Add and Delete Rewrite
You need to write your add & delete like this:
void addNewHabit(String title) {
state = [ ...state, Habit(title: title)];
}
void deleteHabit(String id) {
state = state.where((Habit habit) => habit.id != id).toList();
}
You need to exchange your old list with a new one for Riverpod to fire up.
Update code (for others)
Whilst your query does not need to update data, here is an example of how an update could be done to retain the original sort order of the list;
void updateHabit(Habit newHabit) {
List<Habit> newState = [...state];
int index = newState.indexWhere((habit) => habit.id == newHabit.id);
newState[index] = newHabit;
state = newState;
}
I don't know if this is the right way to handle things, but I figured it out. In the HabitListStateNotifier, for addNewHabit and deleteHabit, I added this line of code: to the end: state = state; and it works exactly how I want it to.
I used to solve this Issue by putting state = state; at the end, but now for some reason maybe flutter or Riverpod update, It doesn't work anymore.
Anyway this is how I managed to solve it now.
void addNewHabit(String title) {
List<Habit> _habits = [...state];
final newHabit = Habit(title: title);
_habits.add(newHabit);
state = _habits ;
}
the explanation as I understand it. when state equals an object, in order to trigger the consumer to rebuild. state must equal a new value of that object, but updating variables of the state object itself will not work.
Hope this helps anyone.

How to create buttons with for loop in a column in Flutter?

I need to implement lists of buttons in a column depending on the data entry. So, for that I have to use for loop. Each button requires two entires id, text. I can make it with List. But it accepts only string value not the integer.
This is the code I tried.
code
Widget getTextWidgets(List<String> strings)
{
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(new ButtonForHome(
lable: strings[i],
onPressed: (){},
));
}
return new Column(children: list);
}
I want to put id in onPressed event. How can I implement in the Flutter?
You should use Listview instead of Column
SAMPLE CODE
getTextWidgets() {
return ListView.builder(
itemCount: yourList.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(yourList[itemIndex]),
onPressed: () {
debugPrint('Clicked index $itemIndex');
});
});
}
Now your question
I want to put id in onPressed event. How can I implement in the Flutter?
You can create a POJO class like this
class DataModel{
String name;
int id;
DataModel(this.name, this.id);
}
Now create list of your POJO class
List<DataModel> list= List<DataModel>();
Now add data in your list like this
list.add(DataModel("name", 1));
list.add(DataModel("name", 2));
list.add(DataModel("name", 3));
Now you can use it like this way
getTextWidgets() {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(list[itemIndex].name),
onPressed: () {
debugPrint('Clicked item name '+list[itemIndex].name);
debugPrint('Clicked item ID '+list[itemIndex].id.toString());
});
});
}
Nilesh Rathod has indeed given the descriptive answer for the same. In flutter there is also, a way to achieve this, which is quite similar to POJO class, is
To create own widget and specify the fields needs to be passed when we are using the widget
Add the widget to the list, with the data specified for passing
You can track, the id, by pressing itself also
I can clearly see that, you have created your own widget named as ButtonForHome, which takes in label for now. What you can do is, to make your widget takes in two argument, and you can do it like this:
class ButtonForHome extends StatelessWidget {
final String label;
final int id; // this will save your day
// #required will not let user to skip the specified key to be left null
ButtonForHome({#required this.label, #required this.id});
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
child: Text(this.label),
onPressed: () => print(this.id) // Here you print your id using your button only
)
);
}
}
Now creating your button with a list, or adding via list
You can do it via ListView.builder()
You can do it via your way, i.e., List<Widget>.add()
I am gonna show the solution in your way only:
Widget getTextWidgets(List<String> strings){
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(ButtonForHome(
id: i, // passing the i value only, for clear int values
label: strings[i]
));
}
return Column(children: list);
}
With the new flutter in place, you don't need to do new every time while defining a widget. It understands now, so no need of const, new at all
So, wherever you populate your getTextWidget, it will show up the Widgte ButtonForHome, which has unique id, and label. Now the ButtonForHome, prints the id of that particular widget which was passed uniquely. So now, you can see your result happening.
I hope this is what you were looking for. Try it, and let me know.

How to create and update the value of Dynamic Widgets through Flutter Provider

So I am implementing something like below:
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
int _count = 0;
int get count => _count;
Future<List<Widget>> getList() async{
addToList(Text('$count'));
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
increment(){
_count += 1;
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return Container(
child: futureProvider == null
? Text('Loading...'
: ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index){
return futureProvider[index];
}
),
));
}
}
Basically, what this does is that a List of Widgets from a Future is the content of ListView Builder that I have as its objects are generated from a database query. Those widgets are buttons that when pressed should update the "Count" value and should update the Text Widget displaying the latest "Count" value.
I was able to test the buttons and they seem to work and are incrementing the _count value via backend, however, the displayed "Count" on the Text Widget is not updating even if the Provider values are updated.
I'd like to ask for your help for what's wrong here, with my understanding, things should just update whenever the value changes, is this a Provider anti-pattern, do I have to rebuild the entire ListView, or I missed something else?
I'm still getting myself acquainted with this package and dart/flutter in general, hoping you can share me your expertise on this. Thank you very much in advance.
so I have been on a lot of research and a lot of trial and errors last night and this morning, and I just accidentally bumped into an idea that worked!
You just have to have put the listening value on a consumer widget making sure it listens to the nearest Provider that we have already implemented higher in the widget tree. (Considering that I have already finished drawing my ListView builder below the FutureProvider Widget)
..getList() async{
Consumer<ChallengeViewProvider>(
builder: (_, foo, __) => Text(
'${foo.count}',
),
);
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
I have also refactored my widgets and pulled out the Button as a stateless widget for reuse. Though make sure that referenced Buttons are subscribed to the same parent provider having the Counter value and have the onTap property call out the increment() function through Provider<>.of
Hoping this will help anyone in the future!

BlocBuilder partially update ListView

Project
Hi, I'm trying to use a bloc pattern to create a list view that can be filtered by a TextField
Here is my code
bloc:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
List<Book> displayList = [];
....
#override
HomeState get initialState => UnfilteredState();
#override
Stream<HomeState> mapEventToState(
HomeEvent event,
) async* {
....
//handle filter by input
if(event is FilterListByTextEvent) {
displayList = displayList.where((book){
return book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}
}
}
view
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
final HomeBloc bloc = Provider.of<HomeBloc>(context);
print(bloc.displayList);
return ListView.builder(
shrinkWrap: true,
itemCount: bloc.displayList.length,
itemBuilder: (context, index) {
return Dismissible(
key: UniqueKey(),
background: Container(
color: selectedColor,
),
child: Container(
height: 120,
padding: const EdgeInsets.fromLTRB(20, 4, 20, 4),
child: BookCard(
book: bloc.displayList[index],
),
),
onDismissed: (DismissDirection direction) {
},
);
},
);
},
);
}
}
Problem
I've read some other discussion about bloc pattern and List view but the issue I'm facing here is different.
Every time my Filter event is called, bloc correctly generate a new displayList but, when BlocBuilder rebuild my UI, listview is not correctly updated.
The new filtered list is rendered but old results do not disappear. It seems like the new list is simply stacked on top of the old one.
In order to understand what was happening I tried printing the list that has to be rendered, inside the BlocBuilder before the build method is called.
The printed result was correct. In the console I see only the new filtered elements while in the UI I see both the new one and the old one, one below the other.
What am I missing here?
Thanks
Keep an intermediate event, eg. a ListInit for which you will display a CircularProgressIndicator. BlocBuilder will be stuck on previous state unless you update it over again.
So in your bloc, first yield the ListInit state and then perform filtering operations, and then yield your FilteredState.
Make new state for loading and yield before displayList.
if(event is FilterListByTextEvent) {
yield LoadFilterList();
displayList = displayList.where((book)
{
return
book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}