guricode

Flutter + Riverpod MVVM 기본 통신 예제 본문

앱/Flutter&Dart

Flutter + Riverpod MVVM 기본 통신 예제

agentrakugaki 2025. 7. 29. 19:48

 

아래 글은 Flutter 프로젝트에 Riverpod를 적용해 간단한 통신 시나리오(MVVM 구조)를 만드는 과정을 처음부터 끝까지 풀어쓴 기록이다. 각 단계마다 필요한 코드와 개념을 최대한 상세히 설명했으니, 한 번도 Riverpod을 써 본 적이 없어도 그대로 따라오면 동작하는 예제를 얻을 수 있다

1. Riverpod 패키지 추가

터미널에서 프로젝트 루트에 다음 명령을 실행한다.

flutter pub add flutter_riverpod

pubspec.yaml의 dependencies: 블록에 flutter_riverpod: ^버전이 자동으로 들어가고, flutter pub get까지 함께 수행된다.


2. 최상위 위젯을 ProviderScope로 감싸기

Riverpod은 내부적으로 전역 상태 저장소를 두는데, 이것을 ProviderScope 위젯이 관리한다.
runApp() 바로 아래에서 앱 전체를 한 번만 감싸 주면 된다.

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

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

ProviderScope가 없으면 런타임에 No ProviderScope found 예외가 발생한다....처음에 이거 빼먹엇다가 헛고생만했다..


3. 화면을 그릴 위젯 구성

UI를 담당하는 HomePage에서는 두 가지 Riverpod API를 사용한다.

  • ref.watch(provider) – 프로바이더가 내보내는 상태를 구독한다. 값이 바뀌면 위젯이 자동 리빌드된다.
  • ref.read(provider) – 프로바이더를 한 번만 읽거나(재빌드 불필요) ViewModel 메서드를 호출할 때 쓴다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'home_view_model.dart';

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(homeViewModelProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('MVVM + Riverpod 예제')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('name : ${state.user?.name ?? "(no data)"}'),
            Text('age  : ${state.user?.age  ?? "(no data)"}'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                ref.read(homeViewModelProvider.notifier).getUser();
              },
              child: const Text('데이터 가져오기'),
            ),
          ],
        ),
      ),
    );
  }
}

4. 통신 결과를 담을 모델 클래스

예시 JSON은 { "name": "...", "age": 20 } 형태다. 이에 맞춰 User 모델을 만든다.

class User {
  final String name;
  final int age;

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

  factory User.fromJson(Map<String, dynamic> map) {
    return User(
      name: map['name'] as String,
      age : map['age']  as int,
    );
  }

  Map<String, dynamic> toJson() => {
        'name': name,
        'age' : age,
      };
}

5. 모델을 가져올 Repository 클래스

실제 앱에서는 http 패키지로 API를 호출하겠지만, 여기서는 1 초 딜레이 후 더미 JSON을 반환한다.

import 'dart:convert';
import 'user.dart';

class UserRepository {
  Future<User> getUser() async {
    await Future.delayed(const Duration(seconds: 1));

    const dummy = '''
{
  "name": "이지원",
  "age": 20
}
''';

    final map = jsonDecode(dummy) as Map<String, dynamic>;
    return User.fromJson(map);
  }
}

6. 위젯이 관찰할 상태 클래스

화면에 보여 줄 데이터가 늘어날 때마다 이 클래스에 필드를 추가한다.

import 'user.dart';

class HomeState {
  final User? user;
  HomeState(this.user);
}

7. ViewModel 작성 – Notifier 상속

Notifier<State>를 상속받으면 state 필드를 통해 상태를 읽고, 새 객체를 할당하여 갱신할 수 있다.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'home_state.dart';
import 'user_repository.dart';

class HomeViewModel extends Notifier<HomeState> {
  @override
  HomeState build() {
    // 최초 상태: user가 없다
    return HomeState(null);
  }

  Future<void> getUser() async {
    final repo = UserRepository();
    final user = await repo.getUser();
    state = HomeState(user);    // 상태 교체 → ref.watch가 붙은 위젯 리빌드
  }
}

8. ViewModel을 공급하는 NotifierProvider

NotifierProvider<ViewModel, State>는 ViewModel 인스턴스를 생성·보관하고,
동시에 해당 ViewModel이 노출하는 state를 외부 Provider로 등록한다.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'home_view_model.dart';
import 'home_state.dart';

final homeViewModelProvider =
    NotifierProvider<HomeViewModel, HomeState>((ref) {
  return HomeViewModel();
});

9. 위젯에서 상태 구독 및 함수 호출

  • 상태가 변할 때마다 UI가 갱신되길 원할 때:
  • final state = ref.watch(homeViewModelProvider);
  • 한 번만 값을 읽거나 ViewModel 메서드를 쓰고 싶을 때:
  • ref.read(homeViewModelProvider.notifier).getUser();

watch와 read의 차이를 항상 기억해 두면, 필요 이상으로 위젯이 재빌드되는 문제를 피할 수 있다.


마무리

  1. 패키지 설치 – flutter pub add flutter_riverpod
  2. ProviderScope – 앱 최상단에서 한 번 감싸기
  3. UI 위젯 – ConsumerWidget 또는 Consumer 사용
  4. Model – JSON <-> 객체 변환 로직 작성
  5. Repository – 통신 또는 로컬 DB 접근 담당
  6. State – UI가 관찰할 데이터 묶음
  7. ViewModel – Notifier로 상태 업데이트 로직 구현
  8. NotifierProvider – ViewModel+State를 전역으로 노출
  9. watch/read – 필요에 따라 상태 구독·단발성 호출 구분

이 구조를 바탕으로 실제 API 호출, 예외 처리, 로딩 스피너, 테스트 코드 등을 단계적으로 확장해 나가면 된다. 처음에는 파일이 조금 많아 보이지만, 역할이 뚜렷해 유지보수에 유리하다.