[Flutter] riverpod 2.0 사용기 - 2

홍석·2023년 12월 24일

riverpod 2.0을 프로젝트에 적용해보면서 사용했던 방법들 정리

이전글 - riverpod 2.0에 대한 이해

서론

riverpod 2.0 이 나온지도 이제 1년이 넘었는데! 이제는 Notifier와 친숙해지고 싶어 차근차근 StateNotifier 대신 Notifer을 사용해가면서, 강력함을 맛보고 있다. 생성자로 인한 의존성 주입을 못하는게 초기에는 어색하곤 했지만, 이제는 익숙하게 써지는거 같다.

AsyncNotifier의 build() 전후를 라이브러리단에서 처리해주기때문에
sealed class로 로딩,에러,정상 데이터를 구분해주는 보일러 플레이트 코드가 없어진게 가장 효과적이였던거 같다.

provider 종류

final name = SomeProvider.someModifier<Result>((ref) {
  <your logic here>
});

final name = SomeNotifierProvider.someModifier<MyNotifier, Result>(MyNotifier.new);
class MyNotifier extends SomeNotifier<Result> {
  
  Result build() {
    <your logic here>
  }
  <your methods here>
}

Provider

Provider FutureProvider StreamProvider
단순히 데이터를 가져오는 경우에 사용가능하다.

Notifier AsyncNotifier StreamNotifier
데이터를 가져오고 상호작용이 필요할때 사용가능하다.


modifier

autoDispose
provider가 dispose될때, 해당값을 캐싱하지않고 메모리에서 지워버린다.
family
provider를 생성시 동일한 class의 provider이지만,
각각의 provider마다 다르게 인자값을 넘기고 싶을때 사용한다.

notifier 사용법

build

class MyNotifier extends SomeNotifier<Result> {
  @override
  Result build() {
    <your logic here>
  }
  <your methods here>
}

NotifierStateNotifier와의 다른 부분이다.
기존의 StateNotifier은 초기 생성자부분에서 로직을 처리할때,
provider에서 ref.watch()로 값을 받은뒤 이를 생성자에 넣어주고, stateNotifier의 초기화블록에서 비즈니스 로직을 처리해야했다면,

Notifier 에서는 생성자로 인한 주입을 택하지않고, Notifier이 rebuild되야하는 로직을 build()에 넣어주게 된다.
build내부에 ref.watch를 통한 값이 변경될때, 해당 notifier객체가 새로 생성되지 않고, 변화에 대응해서 build된다.

build의 리턴값이 Notifier의 초기값으로 된다.
또한 ref.invalidateSelf()을 통해 Notifier의 값을 최신화하고싶을때 build의 로직이 실행된다.

Repository 패턴

기존의 StateNotifier에서는 생성자 주입의 방법으로 repository를 참조했다.
Notifier에서는 late final을 사용해서 repository를 사용하면 된다.

이렇게 쓰면 안되는것 같다.

class FavoriteNotifier extends StreamNotifier<List<NoticeModel>> {
  late final FavoriteRepository _repository = ref.read(favoriteRepositoryProvider);
  
  Stream<List<NoticeModel>> build() {
    return _repository.watchFavorite();
  }
  <other method>
}

다른 AsyncNotifer의 값을 참조

ref.watch(provider).unwrapPrevious().valueOrNull;

해당 방법을 통해 nullable인지 여부를 통해 값을 조합하거나
requireValue를 통해 이를 사용할수 있다.

view에서 사용법

watch, when

final state = ref.watch(favoriteStreamProvider);

state의 클래스를 보면 AsyncValue로 되있다.
이를 통해 해당 값이 초기화 되었는지, 에러인지, 로딩중인지를 나누어서 when을 통해 다루어 줘야한다.

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(favoriteStreamProvider);

    return state.when(
      error: (error, stackTrace) => Center(child: Text(error.toString())),
      loading: () => const Center(child: CircularProgressIndicator()),
      data: (data) {
        if(data.isEmpty){
          return CowItem(content: '즐겨찾기를 추가해 보세요.');
        }
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (context, index) {
            final model = data[index];
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
              child: GestureDetector(
                onTap: () {
                  context.goNamed(
                    NoticeWebView.favoriteRouteName,
                    pathParameters: {'url': model.url},
                  );
                },
                child: NoticeCard.fromModel(
                  model: model,
                  isFavorite: true,
                  onStarClick: (value) {
                    ref.read(favoriteStreamProvider.notifier).starClick(model: model, isDelete: true);
                  },
                ),
              ),
            );
          },
        );
      },
    );


  }

requireValue

AsyncNotifierwhen을 사용하면 세가지의 패턴매칭을 해야한다.
이는 유용할때도 있지만, 해당 provider이 이미 생성되었고, 다른곳에서 재사용될때도 모든 경우마다 when을 사용하면 코드 중복이자 비효율이지 않을까?
이에 대한 해법으로 requireValue가 존재한다.

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return const _EagerInitialization(
      // TODO: Render your app here
      child: MaterialApp(),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(myProvider);

    // Handle error states and loading states
    if (result.isLoading) {
      return const CircularProgressIndicator();
    } else if (result.hasError) {
      return const Text('Oopsy!');
    }

    return child;
  }
}
//------------
final exampleProvider = FutureProvider<String>((ref) async => 'Hello world');

class MyConsumer extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(exampleProvider);

    /// If the provider was correctly eagerly initialized, then we can
    /// directly read the data with "requireValue".
    return Text(result.requireValue);
  }
}

https://riverpod.dev/

profile
bayy1216.tistory.com <- 블로그 이전했습니다 🥹

0개의 댓글