[Flutter] Riverpod 상태 관리
버튼을 누르는 등 특정 동작을 수행했을 때 다른 화면으로 이동하도록 설정할 때는 Navigator 객체를 사용한다.
void _selectCategory(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => MealsScreen(
title: "dd",
meals: [],
),
)
);
}
Navigator.push 메서드로 새로운 라우트를 화면 스택의 맨 위에 추가하고 새로운 화면을 현재 화면 위에 띄운다.
Navigator.pop 메서드로 화면 스택의 맨 위에 있는 라우트를 제거하고 이전 화면을 보여준다.
push 메서드로 새로 띄운 화면은 안드로이드의 뒤로가기 버튼을 통해 제거할 수 있다. (pop 메서드 호출)
push와 pop으로 화면 스택을 다루고 MaterialPageRoute 객체로 페이지 전환 시 사용되는 설정을 정의한다.
화면 스택을 다루지 않고 네비게이션 바를 통한 화면 전환을 구현할 때는 BottomNavigationBar 위젯을 사용한다.
@override
Widget build(BuildContext context) {
Widget activePage = const CategoriesScreen();
var activePageTitle = 'Categories';
if (_selectedPageIndex == 1) {
activePage = const MealsScreen(meals: []);
activePageTitle = 'Your Favorites';
}
return Scaffold(
appBar: AppBar(
title: Text(activePageTitle),
),
body: activePage,
bottomNavigationBar: BottomNavigationBar(
onTap: _selectPage,
currentIndex: _selectedPageIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.set_meal),
label: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: 'Favorites',
),
],
),
);
}
탭을 누를 때 setState 메서드를 호출하도록 해서 네비게이션 바의 인덱스를 업데이트한다.
탭이 5개 이하일 경우 좋은 방법이다.
각 탭은 독립적인 네비게이션 스택을 가져야 하고, 탭 간 데이터를 공유해야 하는 경우 Bloc 등 상태 관리를 도와주는 프레임워크를 사용하는 편이 합리적이다.
햄버거 버튼같은 사이드바를 만들 때는 Drawer 위젯을 사용한다.
Scaffold(
appBar: AppBar(
title: Text('...'),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
decoration: BoxDecoration(
color: Colors.blue,
),
child: Text(
'Header',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
ListTile(
leading: Icon(Icons.message),
title: Text('Messages'),
onTap: () {
...
},
),
ListTile(
leading: Icon(Icons.account_circle),
title: Text('Profile'),
onTap: () {
....
},
),
],
),
),
);
Scaffold 위젯의 drawer 속성에 위젯을 적용한다.
ListView 같은 스크롤 가능한 위젯을 설정하고 내부에 ListTile 위젯으로 내용을 채워넣는 방식으로 사용한다.
onTap() 메서드 내부에는 Navigator.push 로 화면 스택에 새로운 화면을 추가하거나 새로운 화면을 로드하도록 설정하자.
필요에 따라 Navigator.pushReplacement 메서드로 현재 화면 위젯을 새로운 위젯으로 완전히 교체하는 작업도 필요하다.
화면을 떠날 때 특정 데이터를 함께 전송할 때는 WillPopScope 위젯을 사용한다.
return WillPopScope(
onWillPop: () async {
final shouldPop = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('닫을까요?'),
content: Text('Do you want to exit the app?'),
actions: <Widget>[
FlatButton(
child: Text('ㄴㄴ'),
onPressed: () => Navigator.of(context).pop(false),
),
FlatButton(
child: Text('ㅇㅇ'),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
);
return shouldPop ?? false;
},
child: Scaffold(
appBar: AppBar(
title: Text('WillPopScope demo'),
),
body: Center(
child: Text('Press back button to see the dialog.'),
),
),
);
해당 위젯의 매개변수인 onWillPop은 Future<bool> 타입을 반환하는 함수를 받는다.
Future의 결과에 따라 화면을 pop 하는 방식으로 작동한다. (async 키워드를 사용했다)
void _setScreen(String identifier) async {
Navigator.of(context).pop();
if (identifier == 'filters') {
final result = await Navigator.of(context).push<Map<Filter, bool>>(
MaterialPageRoute(
builder: (ctx) => FiltersScreen(
currentFilters: _selectedFilters,
),
),
);
setState(() {
_selectedFilters = result ?? kInitialFilters;
});
}
}
push를 수행할 때 await를 걸어놓으면 해당 값이 push되면서 얻어지는 값을 사용할 수 있다.
WillPopScope 에서 pop 되면서 전달되는 값을 result에 저장해서 사용한다.
이렇듯 한 화면에서 다른 화면으로 전환할 때 데이터를 전송하려면 해당 위젯의 생성자를 특정 데이터를 포함시키는 방식을 사용하는데..
애플리케이션의 규모가 커지면 다양한 위젯 간 상태 공유와 업데이트가 힘들어진다.
이런 문제를 해결하기 위해 Riverpod, Bloc, Provider 같은 상태 관리 라이브러리를 사용한다.
Riverpod 라이브러리는 여러 가지 유형의 Provider를 제공한다.
Provider는 객체를 제공하고, 위젯은 Provider가 제공하는 객체에 접근해서 사용한다. (Dependency Injection)
스프링의 @Autowired 애너테이션과 비슷한 느낌이다.
위젯에 Provider가 제공하는 객체를 주입해서 상태를 관리한다고 생각하면 된다.
Consumer는 Provider가 제공하는 데이터를 읽어 오는 위젯으로 위젯은 Provider를 참조하고 Provider로부터 데이터를 가져와서 위젯을 구축한다.
void main() {
runApp(const ProviderScope(
child: App()
),
);
}
Riverpod 라이브러리를 사용하려면 애플리케이션의 최상위 레벨인 runApp에서 ProviderScope 위젯으로 애플리케이션을 감싸 줘야 한다.
ProviderScope는 모든 Provider를 관리하고 해당 Scope를 통해 App의 어디에서든지 Provider를 참조할 수 있게 한다.
class TabsScreen extends ConsumerStatefulWidget {
...
}
class _TabsScreenState extends ConsumerState<TabsScreen> {
...
@override
Widget build(BuildContext context) {
final meals = ref.watch(mealsProvider);
final availableMeals = meals.where((meal) {
...
}
}
}
프로바이더의 상태에 접근하려면 ConsumerStatefulWidget 을 사용한다. (StatefulWidget으로도 접근할 수 있긴 하다)
생성되는 ConsumerState 객체를 통해 프로바이더의 상태에 접근할 수 있다.
ConsumerState는 ref 매개변수로 프로바이더의 상태에 접근한다. (WidgetRef 타입이다)
ref.watch(provider)
주어진 프로바이더의 현재 상태를 반환한다.
프로바이더의 상태가 바뀔 때 마다 위젯을 다시 빌드한다. (리스너 역할을 수행한다)
ref.read(provider)
프로바이더의 현재 상태만 반환한다.
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();
});
상태를 가진 객체를 프로바이더로 관리할 때는 StateNotifier 객체를 사용한다.
StateNotifier가 직접 상태를 관리해 상태를 내부에서만 변경할 수 있도록 제한한다.
위의 예시에서 state가 객체가 관리하는 상태를 나타낸다.
StateNotifier의 사용자는 state의 변화를 관찰하고 변화에 따라 위젯을 업데이트 하는 방식으로 사용한다.
즉, 프로바이더는 상태를 관리하고 상태에 대한 접근을 제어한다.
그리고 프로바이더가 관리하는 상태는 Notifier가 변경할 수 있고, 변경된 상태를 프로바이더에게 알려준다.
class MealDetailsScreen extends ConsumerWidget {
...
@override
Widget build(BuildContext context, WidgetRef ref) {
final favoriteMeals = ref.watch(favoriteMealsProvider);
final isFavorite = favoriteMeals.contains(meal);
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: Icon(isFavorite ? Icons.star : Icons.star_border),
)
]),
}
}
}
}
ref.read() 메서드를 사용하려면 ConsumerWidget을 사용한다.
read() 메서드로 프로바이더가 제공하는 객체를 가져온 후 객체의 메서드를 호출해 프로바이더의 상태를 변경하는 방식으로 동작한다.
state를 변경할 때는 staet내부를 조작하지 말고 새로운 객체의 주소를 할당하는 방식으로 진행하는 편이 합리적이다.
어차피 다트의 가비지 컬렉터가 더 이상 참조되지 않는 메모리는 정리해 주니.. 프로그램의 불변성을 위해 새로운 주소를 할당하는 방식으로 state를 조작하자.
프로바이더 간 의존성을 설정해 상태나 리소스의 관계를 정의할 수 있다.
한 프로바이더가 변경됐을 때 다른 프로바이더가 알림을 받아 특정 로직을 수행하도록 한다.
final filteredMealsProvider = Provider((ref) {
final meals = ref.watch(mealsProvider);
final activeFilters = ref.watch(filtersProvider);
return meals.where((meal) {
if (activeFilters[Filter.glutenFree]! && !meal.isGlutenFree) {
return false;
}
if (activeFilters[Filter.lactoseFree]! && !meal.isLactoseFree) {
return false;
}
if (activeFilters[Filter.vegetarian]! && !meal.isVegetarian) {
return false;
}
if (activeFilters[Filter.vegan]! && !meal.isVegan) {
return false;
}
return true;
}).toList();
});
프로바이더 내부에 ref.watch(다른 프로바이더) 구문으로 프로바이더 간 의존성을 설정한다.
'Mobile > Flutter' 카테고리의 다른 글
[Flutter] 애니메이션 (1) | 2023.06.25 |
---|---|
[Flutter] Theme와 세 가지 트리 (0) | 2023.06.19 |
[Flutter] 여러 가지 화면과 사용자 입력 관리 (0) | 2023.06.17 |
[Flutter] 위젯 렌더링과 다트 문법 (0) | 2023.06.16 |
[Flutter] Dart 언어와 StatefulWidget (0) | 2023.06.14 |
댓글
이 글 공유하기
다른 글
-
[Flutter] 애니메이션
[Flutter] 애니메이션
2023.06.25 -
[Flutter] Theme와 세 가지 트리
[Flutter] Theme와 세 가지 트리
2023.06.19 -
[Flutter] 여러 가지 화면과 사용자 입력 관리
[Flutter] 여러 가지 화면과 사용자 입력 관리
2023.06.17 -
[Flutter] 위젯 렌더링과 다트 문법
[Flutter] 위젯 렌더링과 다트 문법
2023.06.16