GoRouter와 Riverpod으로 네비게이션 기능 구현하기 w.OffStage
유저가 로그아웃 버튼을 클릭했을 때 홈탭(화면아님)으로 이동시키는 기능을 구현해야 했어요.
BottomNavigationBar를 관리하는 화면은 위 화면과 같이 구성이 되어있었는데, 제가 이 프로젝트에 합류하기 전의 개발자 분이 구현해놓으신 구조였어요.
OffStage란?
offstage와 child라는 프로퍼티를 가진 객체인데, 자식 위젯이 위젯 트리에 삽입은 되있으나 UI에 보이게 할지 말지를 결정할 수 있는 객체에요.
즉,
- stage를 off할 거냐? 네 할게요(true) -> 화면에서 child 위젯이 안보임
- off 할 거냐? 아니요 안할래요(false) -> 화면에서 child 위젯이 보임.
따라서 위 구조는 Stack 위젯의 하위로 OffStage가 3개가 쌓여있고 BottomNavigaionBar의 currentIndex로 offstage값을 관리해 하나의 화면만 Stack에서 보이게 하여 탭을 관리하는 구조였어요.
BottomNavigationBar란?
위 이미지에서 보이는 것과 같이 모바일 앱의 화면에서 하단에 위치하여 화면을 이동시켜주는 위젯이에요.
여기서
- currentIndex는 현재 유저가 클릭한(active) item을 BottomNavigationBar객체에게 알려주는 것이고
- onTap 메서드는 item중 하나가 눌렸을 때 호출되는 함수에요.
현재 만들고 있는 제품은 일단 플러터로 웹앱을 만들고 있어 네비게이션 관리를 GoRouter 패키지를 사용해서 관리하고 있었어요.
문제 시작
1. 화면 간 이동이 안됨.
그런데 왠걸, context.go(홈화면); 을 했는데 로그아웃 버튼을 눌러도 해당 화면으로 이동이 안되는 문제가 있었어요.
이유를 추측한 바, 화면 간 이동이 아니라 탭으로 화면이 관리되고 있었음을 알게 되었어요.
따라서 다음과 같은 분석을 내놓게 됐는데요!
그런데 이렇게만 하면 문제가 해결되는 것이 아니였어요.
2. GoRoute로 경로 파라미터를 동적으로 관리.
해당 화면은 3개의 탭(경로)을 관리하고 있었기 때문에 GoRoute 코드는 다음과 같았어요.
GoRoute(
name: MainNavigationScreen.routeName,
path: "/:tab(personalBranding|tuti|profile)",
builder: (context, state) {
String tab = state.params['tab'] ?? 'tuti';
return const MainNavigationScreen(tab: tab);
},
즉, path를 tab이라는 파라미터로 동적으로 할당하고 있는데, 유저가 어떤 경로를 택하느냐에 따라 총 3가지 경로로 이동할 수 있어요.
그렇게 동적으로 할당된 경로 값을 state.params[‘tab’]으로 받아 MainNavigation 화면의 tab 프로퍼티로 넘겨주게 됩니다.
final List<String> tabs = [
'personalBranding',
'tuti',
'profile',
];
int selectedIndex = tabs.indexOf(tab);
메인 네비게이션 화면에서는 그렇게 받은 tab 값을 tabs라는 리스트에 indexOf 메서드(리스트를 순회하며 tab과 일치하는 값의 index를 반환)를 사용해 selectedIndex로 초기화하고 이를 해당 화면의 BottomNavigationBar와 OffStage에서 사용하는 구조였습니다.
해결 방법
문제를 종합해 분석해보면,
- tab 값이 router.dart에서 mainNavigationScreen으로 공유된다.
- 프로필 화면의 selectedIndex 값이 메인 네비게이션 화면의 selectedIndex로 공유된다.
- 그렇게 공유받은 selectedIndex값을 사용해
- BottomNavigationBar의 currentIndex와
- OffStage의 offstage 값을 계산하는 데 사용된다.
따라서 위젯 간 데이터(상태)가 공유되기 때문에 상태관리 솔루션(Riverpod)을 사용하기로 결정했어요.
여기서는 StateProvider 객체를 선택했는데, 공유되는 값이 String과 int로 간단하기 때문이었어요.(공식문서 참조)
따라서
- router에서 tab값을 tabProvider로 관리
final tabsProvider = StateProvider<String>((ref) => 'tuti');
GoRoute(
name: MainNavigationScreen.routeName,
path: "/:tab(personalBranding|tuti|profile)",
builder: (context, state) {
String tab = state.params['tab'] ?? 'tuti';
ref.read(tabsProvider.notifier).state = tab;
return const MainNavigationScreen();
},
1) tabsProvider과 관리하는 데이터를 tab 값으로 업데이트 2) MainNavigationScreen의 tab 프로퍼티를 없앰.
- selectedIndex값을 Provider로 관리
final navigationSelectedIndexProvider = StateProvider<int>((ref) {
final List<String> tabs = [
'personalBranding',
'tuti',
'profile',
];
final tab = ref.watch(tabsProvider);
int selectedIndex = tabs.indexOf(tab);
return selectedIndex;
});
이렇게 위에서 언급한 문제 2가지가 해결됐어요.
1에서 tab값을 업데이트해 바로 위 코드에서 read하여 tab 변수에 할당해 selectedIndex에 초기화시켜 값을 return 합니다.
- 탭을 관리하는 화면의 build 메서드에서 index 값을 watch
Widget build(BuildContext context) {
int selectedIndex = ref.watch(navigationSelectedIndexProvider);
- onTap 메서드에서 setState로 관리
setState(() {
// 유저가 클릭한 index를 navigationSelectedIndexProvider의 state에 할당
ref.read(navigationSelectedIndexProvider.notifier).state = index;
});
이렇게 3번 문제까지 해결함으로써 로그아웃 버튼 클릭 시 탭화면이 이동되도록 router와 riverpod을 이용해 해결해봤습니다 :)
긴 글 읽어주셔서 감사합니다!
댓글남기기