1. Provider를 다른 형식으로 사용해보자.
이전에는 단순 meal 데이터를 전역 상태 값으로 전달해주는 것이었다면, 이번에는 조금 더 동적인 상태값으로 전달해보자.
(함수 사용)
- providers/favorites_provider.dart(new)
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meals/models/meal.dart';
class FavoriteMealsNotifier extends StateNotifier<List<Meal>> {
FavoriteMealsNotifier() : super([]);
bool toggleMealFavoriteStatus(Meal meal) {
final mealIsFavorite = state.contains(meal);
if (mealIsFavorite) {
state = state.where((m) => m.id != meal.id).toList();
return false;
} else {
state = [...state, meal];
return true;
}
}
}
final favoriteMealsProvider =
StateNotifierProvider<FavoriteMealsNotifier, List<Meal>>((ref) {
return FavoriteMealsNotifier();
});
1) 기존 tabs.dart에서 사용했던 favorite 토글을 위젯 전역에 쓰기 위해 이곳으로 가져왔다. (기존에는 해당 함수를 계속 위젯에게 전달 -> 전달 -> 전달 해야하는 번거로움이 있었다.)
2) super([]) 는 해당 클래스의 값은 List<Meal>형식이고, 해당 값을 빈 리스트로 초기화 한다는 뜻이다.
3) super 리스트 값에 직접적으로 값을 사용하면 안된다. 따라서 별도 state값을 만들어 여기서 별도로 리스트값을 만들어준다.
4) favorite 리스트에 새로운 meal이 들어왔을 때, 이미 기존에 있으면 해당 id값이 없는 것들만 추출해서 state 리스트 값으로 사용한다.
(새로 들어온 meal값을 삭제 한다는 의미 -> 기존 리스트에 값을 remove나 add하는 것이 아니라 아예 새로운 리스트를 만들어야 한다. remove나 add를 하면 기존 리스트 값을 직접적으로 건드는 느낌이고, 아예 새로운 배열을 만들면 기존배열은 그대로 유지함. --> 불변성)
그리고, 기존에 없는 meal이면 state 리스트에 추가로 들어온 meal 값을 리스트에 추가한다.
5) favoriteMealsProvider라는 이름으로 다른 곳에서 쓰일 수 있다. 그 뒤 부분은 잘 이해가 안되지만, 형식상으로 알고 넘어가면 될듯하다.
- tabs.dart
void _showInfoMessage(String message) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
void _toggleMealFavoriteStatus(Meal meal) {
final isExisting = _favoriteMeals.contains(meal);
if (isExisting) {
setState(() {
_favoriteMeals.remove(meal);
});
_showInfoMessage('Meal is no longer a favorite.');
} else {
setState(() {
_favoriteMeals.add(meal);
_showInfoMessage('Marked as a favorite!');
});
}
}
1) tabs.dart에서 위 2개 함수를 삭제했다. 아래 토글은 provider에서 쓰기 때문에 삭제 했고, 위 스낵바 함수는 아래 토글에서 더이상 쓰이지 않기 때문에 직접적으로 meal 세부화면2에서 사용해준다.
2) 그리고 tabs.dart 및 다른 위젯에서 다른 화면으로 이동할 때 사용되었던 onToggleFavorite는 모두 삭제해준다.
- meal_details.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meals/models/meal.dart';
import 'package:meals/providers/favorites_provider.dart';
class MealDetailsScreen extends ConsumerWidget {
const MealDetailsScreen({
super.key,
required this.meal,
});
final Meal meal;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: Text(meal.title), actions: [
IconButton(
onPressed: () {
final wasAdded = ref
.read(favoriteMealsProvider.notifier)
.toggleMealFavoriteStatus(meal);
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
wasAdded ? 'Meal added as a favorite.' : 'Meal removed.'),
),
);
},
icon: const Icon(Icons.star),
)
]),
body: SingleChildScrollView(
child: Column(
children: [
Image.network(
meal.imageUrl,
height: 300,
width: double.infinity,
fit: BoxFit.cover,
),
const SizedBox(height: 14),
Text(
'Ingredients',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 14),
for (final ingredient in meal.ingredients)
Text(
ingredient,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 24),
Text(
'Steps',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 14),
for (final step in meal.steps)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(
step,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
),
),
],
),
));
}
}
1) 세부화면2은 comsumer이다. 그리고 stateless를 사용했기 때문에 ConsumerWidget를 써준다.
2) arg로 WidgetRef ref가 쓰인다.
3) onPressed값으로 쓰이기 때문에 watch가 아닌 read를 사용한다. (그때 그때 버튼을 누르면 값을 가져오기 때문에)
4) 앞에서 사용했던 스낵바를 이곳에서 쓰인다.