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]),
),
),
);
}),
),
),
);
}
}
CodePudding user response:
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
CodePudding user response:
The reason the ""simple"" StateProvider
triggers a rebuild and your actual doesn't is because you aren't reassigning its value.
StateProvider
s works like this:
StateProvider
is just aStateNotifierProvider
that uses a simple implementation of aStateNotifier
that exposes itsget state
andset state
methods;StateNotifier
works like this: an actual state update consists of a reassignment. Wheneverstate
gets reassigned it triggers its listeners (just likeChangeNotifier
would do)
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 a good pattern in general.
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
.
CodePudding user response:
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]),
),
),
);
}),
),
),
);
}
}