ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 상태관리, riverpod 사용해 보기
    개발/Flutter 2022. 2. 8. 17:42
    반응형

    Flutter의 상태관리는 Bloc, Provider, riverpod, GetX등이 있다. 각각으 장단점이 있겠지만 모두 한번씩은 사용해 봐야 한다는 생각이 있으므로, 우선 riverpod을 사용해 보려 한다.

    riverpod 과 Provider의 개발자는 같다. Provider의 부족한 부분과 단점을 보완해서 만든게 riverpod이지만 아직 Provider만큼의 기능은 하지 못한다고 한다. 그렇지만 나는 사용해볼 것이다! (flutter_hooks와 함께 사용하면 좋음)

    우선 새 프로젝트를 만들어 주고 pubspec.yaml에 flutter_riverpod: ^1.0.3 를 추가해준다.

    riverpod은 Provider를 제공하는데 그 중 자주 사용하는 3가지를 알아 보자.

    dependencies:
    	# dart
    	riverpod: 
        
        	# flutter
    	flutter_riverpod: 
        
        	# flutter & flutter_hooks
    	flutter_hooks: 
      	hooks_riverpod: 

    1. Providers

    사용할 provider를 정의하는 부분이다. 즉 위젯에서 공통적으로 사용하고 싶은 데이터를 정의한다고 생각하면 된다. riverpod에서는 여러가지 provider를 제공하는데 자주 쓰이는 3가지 provider에 대해 정리해보았다.

    1-1. Provider

    final valueProvider = Provider<int>((ref) {
      return 0;
    });

    가장 간단한 기본 형태의 Provider이다. Provider는 읽기만 가능하며 값을 변경할 수 없다.

    1-2. StateProvider

    final counterStateProvider = StateProvider<int>((ref) {
      return 0;
    });

    StateProvider는 상태를 변경할 수 있는 Provider이다. 내부 상태는 state로 접근이 가능한데, 사용하고자 하는 위젯에서 state 값을 직접 변경할 수 있다.

    1-3. StateNotifierProvider

    class Counter extends StateNotifier<int> {
      Counter() : super(0);
    
      void increment() => state++;
      void decrement() => state--;
    }
    
    final counterStateNotifierProvider = StateNotifierProvider<Counter, int>((ref) {
      return Counter();
    });

    StateNotifierProvider는 상태 뿐만 아니라 일부 로직을 함께 저장할 때 사용된다. 예를 들어 다른 Provider와 결합을 하거나, 내부에서 사용할 로직을 정의할 수 있다.

     

    이외에 FutureProvider도 있는것으로 보임.

     

    우선 riverpod를 사용하기 위해서는 main에서 MyApp()을 ProviderScope로 감싸줘야 한다.

    void main() {
      runApp(
        // Adding ProviderScope enables Riverpod for the entire project
        const ProviderScope(child: MyApp()),
      );
    }

    그리고 riverpod에서 정의한 WidgetRef를 이용해 접근이 가능한데, 이 WidgetRef는 Widget과 Provider사이에 상호 작용을 도와주는 역할을 한다. 즉 WidgetRef를 통해 특정 Widget에서 특정 Provider에 접근이 가능하다고 생각하면 된다.

    우선 그냥 Provider를 사용해 보자.

    Provider 사용 해보기

    // A shared state that can be accessed by multiple
    // objects at the same time
    final countProvider = StateProvider((ref) => 0);
    
    // Comsumes the shared state and rebuild when it changes
    class ProviderTest extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(countProvider);
        return Text('$count');
      }
    }

    위와 같이 작성을 해주면 화면 0이 떠있는 것을 볼 수 있다. 읽기만 가능하므로 값 변경은 하지못하고 화면의 가운데에 0이 있는것만 확인 할 수 있다.

     

    여기서 중간에 값을 읽어오기 위해서 ref.watch를 쓴 것을 볼 수 있다. 그외에도 다른 것들도 쓰인다.

    ref.watch

    • 반응형으로 Provider의 값이 변경되면 자체적으로 다시 build 된다.
    • 비동기적으로 호출하거나, onTab, initState 등의 생명주기에서는 사용을 하면 안된다.
    • 다른 Provider와 결합할 때 아주 유용하게 쓰인다!

    ref.listen

    • Provider의 값이 변경되면 값을 읽는 것이 아니라 정의한 함수를 실행한다.
    • ref.watch와 마찬가지로 build 안이나 Provider 안에서 사용되어야 한다.
    • SnackBar나 Dialog를 처리하는데 유용하다!

    ref.read

    • Provider의 값을 읽어오기만 한다. 값이 변경되어도 별다른 동작을 하지 않는다.
    • 공식 문서에 따르면 특별한 경우가 아니면 사용을 하지 않는 것 같다.

    위의 3개가 같이 주로 쓰인다.

    함수를 재실행 할때는 lesten을 써야 하는것을 알 수 있다. 그리고 pub.dev에서 예제를 보면 stateless에서도 StateProvider를 이용해 값을 변경하면 stateful이 아니더라도 화면의 값이 바뀌는 것을 볼 수 있다.

    StateProvider 사용해보기.

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    // A Counter example implemented with riverpod
    
    void main() {
      runApp(
        // Adding ProviderScope enables Riverpod for the entire project
        const ProviderScope(child: MyApp()),
      );
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: Home());
      }
    }
    
    /// Providers are declared globally and specifies how to create a state
    final counterProvider = StateProvider((ref) => 0);
    
    class Home extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        return Scaffold(
          appBar: AppBar(title: const Text('Counter example')),
          body: Center(
            // Consumer is a widget that allows you reading providers.
            // You could also use the hook "ref.watch(" if you uses flutter_hooks
            child: Consumer(builder: (context, ref, _) {
              final count = ref.watch(counterProvider.state).state;
              return Text('$count');
            }),
          ),
          floatingActionButton: FloatingActionButton(
            // The read method is an utility to read a provider without listening to it
            onPressed: () => ref.read(counterProvider.state).state++,
            child: const Icon(Icons.add),
          ),
        );
      }
    }

    위오와 같이 작성하면된다. floating 버튼에서 read로 값만 읽어 온 뒤 state++를 이용하여 값을 증가시켜주는 것을 볼 수 있다. listening 없이 값만 가져와서 사용하는 경우에 read를 사용 하면 된다.

    그러나 공식 문서에 따르면 ref.read의 사용은 피해야 한다고 한다. 가능하면 ref.watch를 사용하는 것을 권장하며, ref.read를 build 메소드 내에서 사용하지 말라고 권장하고 있다. 또한 build 수 감소를 위해 ref.read를 사용하는 경우, ref.watch를 사용해도 똑같은 효과를 얻을 수 있다고 한다. 그래서 read를 watch로 변경후 사용해도 정상 작동 한다.

    StateNotifierProvider 사용해보기.

    class MyCounter extends StateNotifier<int> {
      MyCounter() : super(0);
    
      void increment() => state++;
      void decrement() => state--;
      void initCount() => state = 0;
    }
    
    final myCounterStateNotifierProvider =
        StateNotifierProvider<MyCounter, int>((ref) {
      return MyCounter();
    });
    
    class StateNotifierProviderTest extends ConsumerWidget {
      StateNotifierProviderTest({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final counterRead = ref.read(myCounterStateNotifierProvider.notifier);
        final counterState = ref.watch(myCounterStateNotifierProvider);
    
        ref.listen(myCounterStateNotifierProvider, (previous, next) {
          print('바뀔때마다 동작');
          print('ref.listen: $previous');
          print('ref.listen: $next');
        });
    
        return Scaffold(
          appBar: AppBar(
            title: const Text('Riverpod Practice'),
          ),
          body: Center(
            child: Text(
              'Value: $counterState',
              style: const TextStyle(
                fontSize: 48,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          floatingActionButton: Align(
            alignment: Alignment.bottomRight,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                FloatingActionButton(
                  heroTag: '111',
                  onPressed: () => counterRead.increment(),
                  child: const Icon(
                    Icons.add,
                  ),
                ),
                const SizedBox(width: 10.0),
                FloatingActionButton(
                  heroTag: '222',
                  onPressed: () => counterRead.decrement(),
                  child: const Icon(
                    Icons.remove,
                  ),
                ),
                const SizedBox(width: 10.0),
                FloatingActionButton(
                  heroTag: '333',
                  onPressed: () => counterRead.initCount(),
                  child: const Icon(
                    Icons.refresh,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    여기까지 기본적인 3가지를 알아 봤고, 부가적인 기능에 대해서 알아보자.

    우선 Future & Stream Provider 부터 알아보자.

    final futureProvider = FutureProvider<int>((ref) {
      return Future.delayed(const Duration(seconds: 3), () => 5);
    });
    
    final streamProvider = StreamProvider<int>((ref) {
      int count = 0;
      return Stream.periodic(const Duration(seconds: 2), (_) => count++);
    });
    
    class FutureStreamProvider extends ConsumerWidget {
      const FutureStreamProvider({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final streamAsyncValue = ref.watch(streamProvider);
    
        final futureAsyncValue = ref.watch(futureProvider);
    
        return Scaffold(
          appBar: AppBar(
            title: Text('Future & Stream Provider'),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Center(
                child: streamAsyncValue.when(
                  data: (data) => Text('Value: $data'),
                  error: (error, stackTrace) => Text('Error: $error'),
                  loading: () => CircularProgressIndicator(),
                ),
              ),
              Center(
                child: futureAsyncValue.when(
                  data: (data) => Text('Value: $data'),
                  loading: () => const CircularProgressIndicator(),
                  error: (error, stackTrace) => Text('Error: $error'),
                ),
              ),
            ],
          ),
        );
      }
    }

    Future는 3초후에 로딩바가 끝나고 5의 값이 고정 되어 있다. Stream은 2초후에 로딩바다 끝나고 0부터 2초마다마다 1씩 증가한다.  Future오 Stream과 똑같이 코드를 해도 처음에 3초후 0을 불러오고 값은 증가하지는 않는다. 처음에만 값을 가져오는 것을 알 수 있다.

    기능 Provider StateProvider StateNotifierProvider FutureProvider StreamProvider
    상태값 읽기 O O O O O
    상태값 쓰기 X O O O O
    자체 메서드 X X O X X
    비동기 처리(단방향) X X X O O
    비동기 처리(양방향) X X X X O

     

    다음은 Combining Providers로 Provider 끼리의 결합 역시 손쉽게 가능한데, Provider 안에서 다른 Provider의 값을 읽기만 하면 쉽게 결합 할 수 있다.

    final cityProvider = Provider((ref) => 'London');
    
    final weatherProvider = FutureProvider((ref) async {
      final city = ref.watch(cityProvider);
    
      return fetchWeather(city: city);
    });
    

    공식 예제로 자세한 예제는 없지만 Provider안에서 다른 Provider를 읽어오면 된다는 것을 간단하게 보여준 것이다.

    반응형

    댓글

Designed by Tistory.