Recently I started to learn riverpod and provider as state management method. Therefore I wanted to rebuild my current application.
My Problem
In my application I have a list (ListView.builder) which displays multiple cards. When a card is clicked (GestureDetector), it should be flipped. This worked with the previous state management (setState()) without any problems. When using riverpod, all cards in the ListView are now flipped when clicking on one card.
Here is my code:
Provider
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@immutable
class TestMovieCard {
const TestMovieCard({
required this.id,
required this.title,
});
final String id;
final String title;
}
class MoviesNotifier extends StateNotifier<List<TestMovieCard>> {
MoviesNotifier()
: super([
const TestMovieCard(id: "1", title: "Im Westen nichts Neues"),
const TestMovieCard(
id: "2", title: "Star Wars Das Erwachen der Macht")
]);
double _flipAngle = 0;
double get flipAngle => _flipAngle;
void flipMovieCard() {
_flipAngle = (_flipAngle pi) % (2 * pi);
state = [...state];
}
bool checkIfCardIsFront(double animationValue) {
if (animationValue >= (pi / 2)) {
return false;
} else {
return true;
}
}
}
final movieCardProvider =
StateNotifierProvider<MoviesNotifier, List<TestMovieCard>>((ref) {
return MoviesNotifier();
});
Widget
class Test extends ConsumerWidget {
const Test({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
Size size = MediaQuery.of(context).size;
List<TestMovieCard> movieCards = ref.watch(movieCardProvider);
return DefaultTabController(
length: 2,
child: Scaffold(
body: Container(
padding: const EdgeInsets.all(defaultPadding),
child: Column(
children: [
SizedBox(
height: size.height * 0.05,
),
Expanded(
child: SizedBox(
width: double.infinity,
child: TabBarView(children: [
MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView.builder(
itemCount: movieCards.length,
itemBuilder: (context, index) {
return GestureDetector(
// key: Key(movieCards[widget.listIndex].id),
onTap: () {
ref
.watch(movieCardProvider.notifier)
.flipMovieCard();
},
child: TweenAnimationBuilder(
tween: Tween<double>(
begin: 0,
end: ref
.watch(
movieCardProvider.notifier)
.flipAngle),
duration: const Duration(seconds: 1),
builder: (BuildContext context,
double val, __) {
return (Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(val),
child: ref
.watch(movieCardProvider
.notifier)
.checkIfCardIsFront(val)
? Card(
elevation: 5,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(
Radius.circular(
20.0))),
child: Container(
height: size.height *
0.25,
decoration:
const BoxDecoration(
borderRadius:
BorderRadius.all(
Radius.circular(
20.0)),
),
child: const Center(
child:
Text("Front"),
)))
: Transform(
alignment:
Alignment.center,
transform:
Matrix4.identity()
..rotateY(pi),
child: Card(
elevation: 5,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius
.all(Radius
.circular(
20.0))),
child: Container(
height:
size.height *
0.25,
width:
double.infinity,
decoration:
const BoxDecoration(
borderRadius:
BorderRadius
.all(
Radius.circular(
20.0),
),
),
child: const Center(
child:
Text("Back"),
),
)),
)));
}),
);
})),
Placeholder()
])),
)
],
)),
bottomNavigationBar: const CustomBottomNavigationBar(),
),
);
}
}
I hope i made my point clear. Thank you in advance.
I have already tried to implement the consumer in several places, but the state of all list items is always reloaded.
EDIT: New Provider code based on the help of Axel
@immutable
class TestMovieCard {
TestMovieCard({
required this.id,
required this.title,
});
final String id;
final String title;
double flipAngle = 0;
void flip() {
flipAngle = (flipAngle pi) % (2 * pi);
}
}
class MoviesNotifier extends StateNotifier<List<TestMovieCard>> {
MoviesNotifier()
: super([
TestMovieCard(id: "1", title: "Im Westen nichts Neues"),
TestMovieCard(
id: "2",
title: "Star Wars Das Erwachen der Macht",
)
]);
void flipMovieCard(String idToFlip) {
state = [
...state.where((x) => x.id != idToFlip),
...state.where((x) => x.id == idToFlip).flip()
];
}
bool checkIfCardIsFront(double animationValue) {
if (animationValue >= (pi / 2)) {
return false;
} else {
return true;
}
}
}
final movieCardProvider =
StateNotifierProvider<MoviesNotifier, List<TestMovieCard>>((ref) {
return MoviesNotifier();
});
CodePudding user response:
You should have the _flipAngle
attribute in your TestMovieCard
so you can distinguish which cards must be flipped and which not.
Then, once you call flipMovieCard
, pass the card index to flip only the selected card.
The code should be something like:
void flipMovieCard(int index) {
state[index].flipAngle = (state[index].flipAngle pi) % (2 * pi);
}
Edit post OP's comment:
The reason why the widget is not rebuilding automatically is because you need to recreate the entire list.
You could implement a flip
method in the TestMovieCard
class and then do this:
TestMovieCard flippedCard = state.where((x) => x.id == idToFlip).first;
flippedCard.flip();
state = [...state.where((x) => x.id != idToFlip), flippedCard];
Instead of:
state[index].flipAngle = (state[index].flipAngle pi) % (2 * pi);