개발노트

48. [Flutter] Riverpod Provider 종류와 사용법(with MVVM 패턴) 본문

앱 개발/Flutter

48. [Flutter] Riverpod Provider 종류와 사용법(with MVVM 패턴)

mroh1226 2023. 11. 24. 17:29
반응형

Riverpod

Riverpod은 Flutter 애플리케이션에서 상태 관리를 위한 훌륭한 도구 중 하나입니다. 다양한 Provider 유형을 사용하여 효과적으로 상태를 관리할 수 있습니다. 

 

- Riverpod 링크: https://riverpod.dev/ko/

 

Riverpod

안전하게 Provider 읽기 Provider를 읽는 중 더 이상 bad state가 되지 않습니다. 만약 Provider를 읽기 위한 필요한 코드를 작성하면, 당신은 유효한 값을 얻을 수 있습니다. Provider는 비동기적으로 로드된

riverpod.dev


Provider 종류

1. Provider:

**Provider**는 가장 기본적인 상태를 제공하는 역할을 합니다. 여기서는 사용자 이름을 제공하는 Provider를 예시로 들겠습니다.

final usernameProvider = Provider<String>((ref) => 'John Doe');

2. StateProvider:

**StateProvider**는 변경 가능한 상태를 제공합니다. 예를 들어, 카운터의 초기 값이 0인 경우:

final counterProvider = StateProvider<int>((ref) => 0);

3. FutureProvider:

**FutureProvider**는 비동기 작업의 결과를 제공합니다. 예를 들어, 네트워크에서 데이터를 가져오는 경우:

final fetchDataProvider = FutureProvider<List<String>>((ref) async {
  // 비동기 작업 수행 (예: 네트워크 요청)
  List<String> data = await fetchDataFromServer();
  return data;
});

4. AsyncNotifierProvider, NotifierProvider

**NotifierProvider**는 Notifier를 수신하고 노출하는 데 사용되며,

**AsyncNotifierProvider**는 비동기 Notifier를 수신하고 노출하는 데 사용됩니다.

AsyncNotifier는 비동기적으로 초기화할 수 있는 Notifier입니다.

class AuthNotifier extends AsyncNotifier<bool> {
  AuthNotifier() : super(const AsyncLoading());

  Future<void> signIn() async {
    state = AsyncLoading();
    bool success = await performSignIn();
    state = success ? AsyncData(true) : AsyncError('Sign-in failed');
  }
}

final authNotifierProvider = AsyncNotifierProvider<AuthNotifier, AsyncValue<bool>>((ref) {
  return AuthNotifier();
});

5. StreamProvider:

**StreamProvider**는 주기적으로 값을 생성하는 가상의 스트림을 만듭니다. 예를 들어, 실시간 채팅 메시지를 처리하는 경우:

final chatMessageStreamProvider = StreamProvider<List<String>>((ref) {
  return getChatMessageStream();
});

이렇게 하면 각 Provider가 특정 상황에 맞는 의미 있는 데이터를 제공하고 관리할 수 있습니다.

 

final chatMessageStreamProvider = StreamProvider.autoDispose<List<String>>((ref) {
  return getChatMessageStream();
});

StreamProvider.autoDispose 처럼 .autoDispose를 붙여 자동으로 자원을 해제하는 방식을 선택하여 실시간으로 값을 가져오는 Stream 특성을 고려하여 조회 비용을 줄일 수 있습니다. (다른 화면이용 중 Stream이 자동으로 dispose 됨)


Riverpod Provider 중 NotifierProvider를  예시로 작성해보겠습니다. (+ AsyncNotifierProvider와 비교)

Provider를 MVVM 패턴과 함께 사용할 때 좋기 때문에 하나의 시트에 MVVM 패턴을 적용하여 예시를 만들었습니다.

 

View, ViewModel, Model 각각의 역할 정리


 

NotifierProvider 사용한 예제
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

//Model: State로 관리할 User 객체를 만들어줌
class User {
  final String name;
  final bool autoLogin;
  User({required this.name, required this.autoLogin});
}

//ViewModel: 사용할 Model을 초기화하고 함께 사용할 메서드를 정의함, + Riverpod Provider 종류를 결정함
//AsyncNotifierProvider를 사용한다면 AsyncNotifier, NotifierProvider를 사용한다면 Notifier 를 extends 로 상속하면됨
class UserViewModel extends Notifier<User> {
  User user = User(name: "name", autoLogin: false);
  @override
  User build() {
    return User(name: "name", autoLogin: false);
    //build를 통한 초기화 부분(null방지)
  }

  User getUser() {
    return User(name: state.name, autoLogin: state.autoLogin);
    //state를 통해 user정보 가져오기
  }

  void setUserName(value) {
    user = User(name: value, autoLogin: state.autoLogin);
    state = user;
    //name Set 하고 state 최신화
  }

  void setUserAuthLogin(value) {
    user = User(name: state.name, autoLogin: value);
    state = user;
    //name Set 하고 state 최신화
  }
}

final userProvider = NotifierProvider<UserViewModel, User>(
  () => UserViewModel(),
);

//View: 위에서 생성한 ViewModel의 Provider을 실제 이용하는 화면
//ref로 provider를 사용하기 위해 ConsumerStatefulWidget 혹은 ConsumerWidget를 이용함
//이해를 돕기위해 두가지 모두 사용한 예시,
//ConsumerStatefulWidget으로 전체 틀을 만들고, ConsumerWidget으로 autoLogin 체크박스를 만들어봄
class UserPage extends ConsumerStatefulWidget {
  const UserPage({super.key});
  //위젯트리에서 ref를 사용하기 위해 StatefulWidget 대신 ConsumerStatefulWidget 사용

  @override
  UserPageState createState() => UserPageState();
  //UserPageState로 createState 변경
}

class UserPageState extends ConsumerState<UserPage> {
  //ConsumerState는 ConsumerStatefulWidget과 한 세트임

  final TextEditingController _textEditingController = TextEditingController();

  @override
  void initState() {
    _textEditingController.addListener(() {});
    super.initState();
  }

  @override
  void dispose() {
    _textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SizedBox(
          height: 200,
          width: 200,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                ref.watch(userProvider).name,
                style: const TextStyle(fontSize: 40),
              ),
              const StateSwitch(),
              Expanded(
                  child: TextField(
                maxLength: 10,
                style: const TextStyle(fontSize: 25),
                controller: _textEditingController,
              )),
              TextButton(
                  onPressed: () => ref
                      .read(userProvider.notifier)
                      .setUserName(_textEditingController.value.text),
                  child: const Text(
                    '이름바꾸기',
                    style: TextStyle(fontSize: 30),
                  )),
            ],
          ),
        ),
      ),
    );
  }
}

class StateSwitch extends ConsumerWidget {
  const StateSwitch({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return SwitchListTile.adaptive(
        title: const Text("AutoLogin"),
        value: ref.watch(userProvider).autoLogin,
        //watch로 리빌드함
        onChanged: (value) =>
            ref.read(userProvider.notifier).setUserAuthLogin(value));
    //메서드 사용은 provider에 .notifier를 붙여줘야함(read는 리빌드하지 않음)
  }
}

주요 소스 설명.

Notifier 클래스:

  • Notifier를 상속한 클래스(UserViewModel)는 상태를 가지고 있으며, 이 상태를 업데이트하면 UI가 리빌드됩니다.
  • build 메서드에서 초기 상태를 설정합니다.
  • getUser, setUserName, setUserAuthLogin 등의 메서드를 통해 상태를 가져오거나 업데이트합니다.

NotifierProvider:

  • **NotifierProvider**는 **Notifier**를 사용하여 상태를 관리하는 Provider입니다.
  • UserViewModel 타입의 Notifier와 그 Notifier가 관리하는 상태인 **User**를 제공합니다.
  • **() => UserViewModel()**을 통해 **UserViewModel**의 인스턴스를 생성합니다.

ConsumerStatefulWidget:

  • **ConsumerStatefulWidget**은 **ref**를 사용하여 Provider를 관찰하고 해당 Provider의 상태를 사용하여 UI를 리빌드하는 StatefulWidget입니다.

ref.watch:

  • **ref.watch(userProvider)**를 통해 Provider를 관찰하고, 해당 Provider의 상태에 따라 UI가 리빌드됩니다.

ref.read(userProvider.notifier):

  • **ref.read**를 통해 Provider의 notifier를 읽어오고, 이를 통해 setUserName 메서드와 같은 메서드를 호출하여 상태를 업데이트합니다.

이렇게 **NotifierProvider**를 사용하면 상태 관리와 UI 간의 효율적인 상호작용을 달성할 수 있습니다. 상태가 업데이트될 때마다 UI가 자동으로 업데이트되어 쉽게 반응형 UI를 구현할 수 있습니다.


NotifierProvider로 상태관리하기 때문에 화면을 나갔다가 들어와도 값이 관리 됩니다.


AsyncNotifierProvider 예시.

AsyncNotifierProvider를 사용하면 loading:, error:, data: 으로 state 상태에 따라 보여줄 위젯을 지정하여 return 할 수 있습니다.

import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

//Model
class User{
  User({required this.name,required this.autoLogin});
  final String name;
  final bool autoLogin;
}

//ViewModel
class UserViewModel extends AsyncNotifier<User> {
  //비동기적으로 Notifier를 사용하기위해 AsyncNotifier을 상속
  @override
  FutureOr<User> build() async{
    final user = User(name: "name", autoLogin: false);
    return user;
    //FutureOr로 User 초기화
  }
  Future<User> getUser() async{
    final user = User(name: state.value!.name, autoLogin: state.value!.autoLogin);
    return user;
    //User get 메소드
  }
  Future<void> setUserName(value)async{
    state = AsyncValue.loading();
    //state를 로딩상태로 만들어줌
    await Future.delayed(Duration(seconds: 2));
    //Future.delayred로 강제로 2초간 딜레이를 줌(Loading되는 예시를 구현하기 위함)
    state = AsyncValue.data(User(name: value, autoLogin: state.value!.autoLogin));
    //state에 data를 넣어줌 (로딩완료) set 매소드
  }
  Future<void> setUserAutoLogin(value) async{
    state = AsyncValue.data(User(name: state.value!.name, autoLogin: value));
  }
}

final userProvider = AsyncNotifierProvider<UserViewModel,User >(()=> UserViewModel());
//AsyncNotifierProvider 생성

//View
class UserViewPage extends ConsumerStatefulWidget {
  const UserViewPage({super.key});

  @override
  UserViewPageState createState() => UserViewPageState();
}

class UserViewPageState extends ConsumerState<UserViewPage> {

  TextEditingController _editingController = TextEditingController();
  //텍스트 입력창에 입력된 Text를 이용하기 위해 컨트롤러 생성

  @override
  void initState() {
    _editingController.addListener(() { });
    //컨트롤러 초기화
    super.initState();
  }
  @override
  void dispose() {
    _editingController.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Container(
        color: Colors.amber.shade800,
        height: 300,
        width: 300,
        child:
        ref.watch(userProvider).when(
          //AsyncNotifierProvider 사용으로 when 기능 작성
          loading: () => CircularProgressIndicator(),
          //loading: state가 로딩 상태일 때 보여줄 위젯을 작성하여 return함

          error: (error, stackTrace) => Text("${error}"),
          //error: state가 로딩 상태일 때 보여줄 위젯을 작성하여 return함

          data: (data) => Column(
            //data: state가 데이터를 받았을 때 보여줄 위젯을 작성하여 return함
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center, 
            children: [
            Text(ref.watch(userProvider).value!.name,
                style: TextStyle(fontSize: 40),),
            TextField(controller: _editingController,cursorWidth: 2,style: TextStyle(fontSize: 20),),
            SwitchListTile.adaptive(
              title: Text("autoLogin"),
              value: ref.watch(userProvider).value!.autoLogin,
              onChanged: (value) => 
                ref.read(userProvider.notifier).setUserAutoLogin(value),),
            CupertinoButton(child: Text("이름 변경"), 
              onPressed:()=> 
                ref.read(userProvider.notifier).setUserName(_editingController.text))
        ],),) 
        
      ),),
    );
  }
}

 

주요 메서드, 소스 설명.

AsyncValue:

**AsyncValue**는 Riverpod에서 비동기적인 상태를 나타내는 클래스입니다. 이 클래스는 세 가지 상태를 가집니다.

  1. AsyncValue.data(value): 비동기 작업이 성공적으로 완료되었고, 결과 데이터 **value**를 가지고 있는 상태입니다.
  2. AsyncValue.loading(): 현재 비동기 작업이 진행 중이며, 데이터를 아직 수신하지 못한 상태입니다.
  3. AsyncValue.error(error, stackTrace): 비동기 작업이 에러로 인해 실패하였고, 에러 정보 **error**와 해당 에러의 스택 트레이스 **stackTrace**를 가지고 있는 상태입니다.

when 메소드:

when 메소드는 **AsyncValue**의 상태에 따라 다른 작업을 수행할 수 있도록 도와주는 메소드입니다. 주로 UI에서 상태에 따라 다른 위젯이나 작업을 표시하는 데 사용됩니다.

예를 들어, 다음 코드에서 **ref.watch(userProvider).when(...)**은 **AsyncValue**의 현재 상태에 따라 다른 동작을 수행합니다.

ref.watch(userProvider).when(
  loading: () => CircularProgressIndicator(),
  error: (error, stackTrace) => Text("${error}"),
  data: (data) => Column(
    children: [
      Text(data.name, style: TextStyle(fontSize: 40)),
      // 나머지 UI 요소들...
    ],
  ),
);

이 코드는 상태가 로딩 중이면 **CircularProgressIndicator**를, 에러가 발생하면 에러 메시지를, 데이터가 있다면 데이터를 표시하는 Column을 반환합니다. 이렇게 하면 상태에 따라 다른 UI를 효과적으로 처리할 수 있습니다.


AsyncNotifierProvider로 완성된 화면

반응형
Comments