[TIL-024] Flutter 상태관리 라이브러리 - Riverpod

MVVM

프로그램을 작성하는 아키텍쳐(패턴, 구조)

상태 관리 라이브러리를 효과적으로, 코드 관리를 효과적으로 관리하기 위해 사용하는 구조.

 

M: Model

V: View

VM: View Model

 

  • Model: 데이터를 서버에서 가져오고 데이터를 사용할 수 있게 가공하기 위한 클래스를 작성
    • Repository: Model의 한 부분으로 데이터를 서버에서 가져오는 담당
  • View: UI를 나타내는 부분
  • View Model: 데이터 또는 상태를 관리하기 위한 클래스를 작성

 

사용 방법

1. ProviderScope

// lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_page.dart';

void main() {
  // Flutter 위젯트리에서 최상위는 MyApp이므로
  // MyApp보다 더 최상위가 돼야 하위의 모든 위젯에서 상태(데이터)에 접근 가능
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HomePage());
  }
}
  • 모든 위젯에서 상태 값(데이터)을 가져올 수 있게 Flutter 위젯트리에서 최상위가 되는 곳에 `ProviderScope(child: Widget)`을 사용

 

2. Model 클래스 작성

데이터를 가공

// lib/user.dart

class User {
  String name;
  int age;

  User({required this.name, required this.age});

  // Map -> 인스턴스
  User.fromJson(Map<String, dynamic> map)
    : this(name: map['name'], age: map['age']);

  // 인스턴스 -> Map
  Map<String, dynamic> toJson() {
    return {'name': name, 'age': age};
  }
}

 

3. Rapository 클래스 작성

데이터를 가져오고 Model클래스를 이용해 인스턴스를 반환

// lib/user_repository.dart

import 'dart:convert';
import 'package:flutter_riverpod_mvvm/user.dart';

class UserRepository {
  Future<User> getUser() async {
    // 서버에서 데이터를 가져온다는 가정을 위해 1초 딜레이를 줌
    await Future.delayed(Duration(seconds: 1));

    // 서버에서 가져온 데이터라고 가정
    String dummy = '''
      {
        "name": "jiwon",
        "age": 20
      }
    ''';

    // 1. jsonDecode로 Map으로 변환
    Map<String, dynamic> map = jsonDecode(dummy);
    // 2. Map -> 인스턴스로 변환
    return User.fromJson(map);
  }
}

 

4. ViewModel 클래스 작성

  • 상태를 생성
  • 상태를 관리(업데이트)할 뷰모델 생성
    • `Notifier<T>`클래스 사용
      • 상태 관리를 위해서 만들어진 Riverpod의 클래스
      • 이 클래스를 상속받은 클래스(ex. HomeViewModel class)에서 다양한 메서드를 만들어 상태를 관리
      • `<T>`는 상태의 타입
        • 즉, `T state;`
  • 뷰에서 상태와 뷰모델을 사용하기 위한 공급자(provider) 생성
    • `NotifierProvider<T, E>`객체
      • 뷰에서 상태와 `Notifier<T>`클래스를 사용하기 위한 Riverpod의 클래스
      • `<T, E>`
        • `T`는 상태를 관리할 뷰모델 클래스. 즉, `Notifier<T>`를 상속 받은 클래스
        • `E`는 상태의 타입
// lib/home_view_model.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/user.dart';
import 'package:flutter_riverpod_mvvm/user_repository.dart';

/* 뷰모델 - 상태를 업데이트하기 위한 클래스 */

// 1. 관리해야될 상태 클래스 만들기
class HomeState {
  // 서버에서 데이터를 가져오지 않은 상태일 수도 있으니 nullable하게 함
  User? user;

  HomeState(this.user);
}

// 2. 상태를 관리할 뷰모델 만들기 - Notifier 이용
class HomeViewModel extends Notifier<HomeState> {
  // build가 반환하는 값은 상태의 초기값
  @override
  HomeState build() {
    return HomeState(null);
  }

  // 사용자가 버튼 클릭하면 뷰모델은 리포지토리에서 데이터를 가져와서 상태를 업데이트 함
  void getUser() async {
    UserRepository userRepository = UserRepository();
    User user = await userRepository.getUser();

    state = HomeState(user);
    
    // 새로운 객체를 state에 할당하지 않으면 자신을 바라보는 View에게
    // 상태가 변경되었다고 알리지 않음
    // state.user = user // => ❌
  }
}

// 3. 뷰모델을 뷰에게 공급해줄 관리자 만들기
// 즉, 위젯에서 뷰모델을 사용하기 위한 변수
// () {return HomeViewModel();} => HomeViewModel.new 로 사용할 수도 있음
// 즉, NotifierProvider<HomeViewModel, HomeState>(HomeViewModel.new);
final homeViewModelProvider = NotifierProvider<HomeViewModel, HomeState>(() {
  return HomeViewModel();
});
  • ✅ `state = HomeState(user)`에서 `HomeState`로 새로운 인스턴스를 생성해 `state`에 할당하는 이유
    • 리렌더링이 되는(상태가 업데이트가 됐다고 판단하는) 이유는 할당 받기 전 `state`의 값과 할당하려는 `state`의 값이 다를 경우이다.
    • 그러나 이 앱의 `state`는 `HomeState`클래스에 의해 생성된 객체이다.
      • 초기값을 Map타입으로 보면 state = { user: null };  
    • 객체는 참조를 하므로 `state.user = user`를 하면 할당 받기 전 `state`의 값과 할당하려는 `state`의 값이 같을 수 밖에 없다.
    • 그래서 리렌더링을 하지 않기에 새로운 객체를 생성해 `state`에 할당하는 것이다.
  • 상태의 타입이 단순히 `int`인 경우에는 `HomeState`와 같은 클래스가 필요하지 않고, 아래처럼 변경하면 된다.
    • `Notifier<HomeState>` → `Notifier<int>` 로 변경
    • `HomeViewModel`의 `build`의 반환 값을 숫자로 변경
    • `state = HomeState(State.counter + 1)` → `state++` 로 변경

 

5. View 클래스 작성

Consumer 위젯 사용

  • `StatelessWidget`이나 `StatefulWidget`에서 사용한다.
  • 리렌더링을 위한 `ref.watch`는 `Cousumer`의 `builder`속성 내에서 사용해야 한다.
// lib/home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_view_model.dart';

/* view 클래스 */

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Consumer(
        builder: (context, ref, child) {
          // ✅ watch에 provider변수를 할당하면 '상태'를 반환
          // ✅ 즉, 이 '상태'를 구독하기 때문에 업데이트가 일어나면 리렌더링 시작
          final homeState = ref.watch(homeViewModelProvider);
          return Column(
            children: [
              Text('name: ${homeState.user?.name}'),
              Text('age: ${homeState.user?.age}'),
              ElevatedButton(
                onPressed: () {
                  // ✅ read에 provider변수를 할당하면 '상태'를 반환
                  // final instanceOfHomeState = ref.read(homeViewModelProvider);

                  // ✅ read에 provider.notifier를 할당하면 '상태를 관리하는' 뷰모델을 반환
                  final viewModel = ref.read(homeViewModelProvider.notifier);

                  // ✅✅ 이 부분이 있어야 ref.watch가 동작하면서 리렌더링이 일어남
                  // ✅✅ viewmodel.getUser의 호출로 viewmodel.getUser내부에 작성한
                  // ✅✅ state에 업데이트가 일어났기 때문
                  viewModel.getUser();
                },
                child: Text('데이터 가져오기'),
              ),
            ],
          );
        },
      ),
    );
  }
}

 

ConsumerWidget 사용

  • `StatelessWidget` 대신 사용한다.
  • 리렌더링을 위한 `ref.watch`는 `CousumerWidget`의 `build`메서드 내에서 사용해야 한다.
// lib/home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_mvvm/home_view_model.dart';

/* view 클래스 */

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ✅ ConsumerWidget은 build에 ref가 자동으로 생성됨
    // ✅ watch에 provider변수를 할당하면 '상태'를 반환
    // ✅ 즉, 이 '상태'를 구독하기 때문에 업데이트가 일어나면 리렌더링 시작
    final homeState = ref.watch(homeViewModelProvider);

    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          Text('name: ${homeState.user?.name}'),
          Text('age: ${homeState.user?.age}'),
          ElevatedButton(
            onPressed: () {
              // ✅ read에 provider변수를 할당하면 '상태'를 반환
              // final instanceOfHomeState = ref.read(homeViewModelProvider);

              // ✅ read에 provider.notifier를 할당하면 '상태를 관리하는' 뷰모델을 반환
              final viewModel = ref.read(homeViewModelProvider.notifier);

              // ✅✅ 이 부분이 있어야 ref.watch가 동작하면서 리렌더링이 일어남
              // ✅✅ viewmodel.getUser의 호출로 viewmodel.getUser내부에 작성한
              // ✅✅ state에 업데이트가 일어났기 때문
              viewModel.getUser();
            },
            child: Text('데이터 가져오기'),
          ),
        ],
      ),
    );
  }
}

 

ConsumerWidget, ConsumerStatefulWidget, Consumer 비교

뷰에서 상태나 뷰모델을 가지고 오기 위해서는 Riverpod에서 제공하는 `ref`객체가 있어야 한다.

`ref`객체는 `ConsumerWidget`, `ConsumerStatefulWidget`, `Consumer` 위젯을 통해 접근할 수 있다.

Widget 종류 기능 사용 상황
`StatelessWidget` 상태 없음 기본 Flutter
`StatefulWidget` 상태 있음 기본 Flutter
`ConsumerWidget` 상태 없음, Provider 사용 Riverpod
`ConsumerStatefulWidget` 상태 있음 + Provider Riverpod
`Consumer (Builder)` 특정 위젯에서만 Provider Riverpod
  • `ConsumerWidget`
    • `StatelessWidget` 대신 사용하는 위젯으로 `setState`는 사용 불가
    • 대신 Riverpod을 사용하여 상태 관리가 가능
    • `build`메서드의 매개변수로 `ref`가 생성 됨
  • `ConsumerStatefulWidget`
    • `StatefulWidget` 대신 사용하는 위젯으로 `setState`도 사용 가능
    • 당연히 Riverpod을 사용하여 상태 관리도 가능
    • `ConsumerState`클래스 내에서는 `ref`객체를 어디에서나 그냥 사용할 수 있게 내부적으로 설계 되어 있음
  • `Consumer`
    • 업데이트 하기 위한 부분에만 사용하므로 `StatelessWidget`, `StatefulWidget` 두 곳 모두에서  사용 가능
    • `setState`와는 관련이 없음
    • `bulider`속성의 매개변수로 `ref`가 생성 됨