현재 진행하는 프로젝트에서 riverpod을 꽤 깊게 사용하고 있다. 특히, data로직 뿐 아니라 MVVM구조를 사용하며, viewModel로서 riverpod을 사용하고 있다.
riverpod의 기능과 전반적인 설명은 공식문서가 더욱 잘되어있기에 관련해서는 포스팅하지 않으려고 한다.
riverpod을 어떻게 사용하고 있는지 전반적인 평가와, 또 앞으로 어떻게 사용해 나가면 좋을 지 정리하는 시간을 가지면 좋을 것 같아 글을 한번 정리해보려고 한다.
viewModel로서의 riverpod의 단점은 소스가 많이 없다.. 사용자가 적지는 않지만 확실히 참고자료는 GetX가 더 많은 느낌이다. (실제로 riverpod 공식 홈페이지에서도 네트워크 요청에 대한 레퍼런스가 더 많다)
하지만, 장점은 꽤 잘되어있는 공식 문서 및 직관적인 문법이다. 나는 riverpod annotation을 사용해서 viewModel을 구성하는데, State
와 Controller
만 구성하면 간단하게 viewModel Provider를 제공해준다.
또한, 장점인지 단점인지는 단언할 수 없지만, 문서와 참고자료가 적은 만큼 사용자에 따라 성능이 좌우되는 폭이 GetX보다 클 것 같다는 것이다. 근간이 Provider인 만큼 자유도가 꽤 높다.
class HomeTicketManagementScreenState {
CalendarFormat calendarFormat;
DateTime? selectedDay;
DateTime focusedDay;
CombineTicket? selectedTicket;
HomeTicketManagementScreenState({
required this.calendarFormat,
required this.selectedDay,
required this.focusedDay,
required this.selectedTicket,
});
}
class HomeTicketManagementScreenController
extends _$HomeTicketManagementScreenController {
HomeTicketManagementScreenState build() {
return HomeTicketManagementScreenState(
calendarFormat: CalendarFormat.month,
selectedDay: DateTime.now(),
focusedDay: DateTime.now(),
selectedTicket: null,
);
}
void setState() {
state = HomeTicketManagementScreenState(
calendarFormat: state.calendarFormat,
selectedDay: state.selectedDay,
focusedDay: state.focusedDay,
selectedTicket: state.selectedTicket,
);
}
void handleChangecalendarFormat(CalendarFormat calendarFormat) {
state.calendarFormat = calendarFormat;
setState();
}
void handleDaySelect(DateTime date, DateTime focusedDay) {
state.selectedDay = date;
state.focusedDay = focusedDay;
setState();
}
...
}
handler
와 비지니스 로직, 특히 state
변수를 viewModel에서 관리하면서 view에서는 해당 로직을 신경써도 안되는 장점이 있다. (MVVM 패턴의 의의)controller와 state선언에 대한 부분은 직관적이여서 상당히 마음에 들었다. 하지만 한가지 문제점이 있었는데, state를 어떻게 재할당 해줄지였다.
state class자체를 state
에 다시 할당해줘야 변화가 되었음을 알 수 있기에, copyWith
를 사용해서 각 상태 변수에 대해서 state를 재할당하는 방식을 사용할까 했다.
생각보다 viewModel의 상태변수가 많아지면 copyWith를 사용하는 것이 의미없이 코드가 길어져서 가독성이 조금 떨어졌다. 그래서 setState라는 함수를 하나 정의해서 state를 재할당해주는 로직을 적용시켜 보았다.
(flutter의 setState를 안쓰고 자체로 setState를 정의하다니 웃기긴 한 부분이긴 하다.)
동작은 굉장히 잘되었고, 재사용이 좋은 함수이기에 리렌더링 타임을 자유롭게 정할 수 있었다. 최적화 관점에서는 꽝인 로직인 것 같지만, 아직 렌더링 이슈는 없어서 추후 더 좋은 구조를 생각해봐야할 것 같다.
여담이지만 zustand처럼 set이 있어서 자동으로 상태를 업데이트 하는 로직을 선언할 수 있으면 조금 더 깔끔할 것 같은데, 이것은 조금 아쉬운 부분이긴 하다.
class HomeTicketManagementScreen extends ConsumerStatefulWidget {
const HomeTicketManagementScreen({super.key});
ConsumerState<HomeTicketManagementScreen> createState() {
return _HomeTicketManagementScreenState();
}
}
class _HomeTicketManagementScreenState
extends ConsumerState<HomeTicketManagementScreen> {
Widget build(BuildContext context) {
ref.watch(homeTicketManagementScreenControllerProvider).selectedDay;
DateTime focusedDay =
ref.watch(homeTicketManagementScreenControllerProvider).focusedDay;
CalendarFormat calendarFormat =
ref.watch(homeTicketManagementScreenControllerProvider).calendarFormat;
Widget emptyContents(String title, String subtitle) {
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Png(PngsEnum.SCHOOL, width: 127.w, height: 109.h),
SizedBox(height: 15.h),
Text(
title,
style: DTypography.H4.copyWith(color: DColor.text_6),
),
Text(
subtitle,
style: DTypography.Body7.copyWith(
color: DColor.text_6,
),
),
],
),
),
);
}
return Scaffold(
backgroundColor: DColor.background2,
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
...
SliverToBoxAdapter(
child: ScheduleCalendar(
selectedDay: selectedDay,
focusedDay: focusedDay,
calendarFormat: calendarFormat,
onFormatChanged: ref
.read(homeTicketManagementScreenControllerProvider.notifier)
.handleChangecalendarFormat,
onDaySelected: ref
.read(homeTicketManagementScreenControllerProvider.notifier)
.handleDaySelect,
selectedDayPredicate: ref
.read(homeTicketManagementScreenControllerProvider.notifier)
.handlePredictSelectedDate,
),
),
...
위의 코드처럼 각 상태변수에 대한 구독이 가능하다. 사실 controller 자체를 watch
하는 것과 같은 의미이다.
var provider = ref.watch(homeTicket...Provider)
어차피 viewModel이 update되려면 새로운state
를 할당해 줘야한다. 이 과정에서 해당 controller를 구독하고 있는 모든 watcher
들은 다시 호출이 되며, 각 변수들을 따로 watch하는 것과 연관성은 없다.
나는 내가 구독하고 있는 상태 변수를 명시한다는 것에 의의를 두고 다음과 같이 사용하고 있다.
공식 문서에는 state단위로 watch할 수 있는 기능을 제공하기는 한다. 아직까지 사용할 만큼 렌더링 최적화가 필요한 부분은 만나지 못했기에, 사용해보지는 않았다.
https://riverpod.dev/ko/docs/advanced/select
notifier
안의 handler만 호출하면 되는 장점이 있다. react
나 viewModel을 사용하지 않는 flutter 패턴은 보통 Props를 통해서 nested하게 handler를 넘겨준다.
class HoldUsageTicket extends StatelessWidget {
const HoldUsageTicket({super.key});
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
List<DogTicket> usageCountTickets =
ref.watch(homeDogSummaryScreenControllerProvider).usageCountTickets;
return Container(
width: double.infinity,
color: DColor.background2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.only(
left: 20.w,
right: 20.w,
bottom: 24.h,
),
child: Text(AppStrings.holdTicket, style: DTypography.H4),
),
if (usageCountTickets.isNotEmpty)
ListView.builder(
itemCount: usageCountTickets.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return usageCountTicket(
title: usageCountTickets[index].title,
date: usageCountTickets[index].date,
onTap: () => ref
.read(homeDogSummaryScreenControllerProvider.notifier)
.handleUsageCountTicketCardTap(
usageCountTickets[index]),
);
},
),
...
이와 같이 사용하는 것이 좋을지는 모르겠지만 Consumer
Widget을 제공하기에 하위 위젯에서도 ref
를 가질 수 있다. 즉, 하위 위젯들에서 바로 viewModel에 접근이 가능하다는 점이다. (state경우에도 마찬가지)
자주 사용하면 해가될 수도 있는 패턴인 점은, view에 종속되지 않는 컴포넌트에 대해서 사용하는 것은 위험하다.
여러 view에서 사용되는 컴포넌트인데, viewModel에 대한 직접적인 접근을 가진다면, 사실 재사용이 불가능한 컴포넌트가 될 것이다. (viewModel을 가진다는 것은 view에 종속적인 거나 다름없는 얘기이다.)
그래서 우선 나는 view Screen을 세분화 할때, 하위 위젯들에서 사용되는 것이 좋아 보여 해당 기준으로 사용하고 있다. (재사용성이 다분한 컴포넌트 단위에서는 Props로 전달)
StateNotifierProvider
을 주로 거의 사용하고, 다른 종류의 Provider를 사용하고 있지는 않는데, 조금 더 다양하게 통합해볼까 생각을 하고 있다.react
가 익숙한 사람은 처음 flutter배울 때, 해당 라이브러리를 알았으면 정말 빠르게 배웠을 것 같다. react-native와 개발난이도가 비슷하지 않을까 싶다..!
잘 읽었습니다. copyWith가 최선인가 싶었는데, setState방식이 넘 인상적이네요! 저도 참고해볼까 합니다. 감사합니다~!