Flutter Riverpod + MVVM 구조 분석

Peter SHIN·2025년 4월 30일

Flutter 앱 구조가 복잡해질수록, 구조화를 제대로 하지 않으면 유지보수가 점점 지옥이 됩니다.
이번 글에서는 Riverpod + MVVM 패턴을 실무 스타일로 적용하는 방법을 개념 + 폴더 구조 + 실전 예시로 정리합니다.


📌 MVVM + Riverpod 핵심 구성 요약

구성요소설명
Model데이터 구조 정의 및 JSON 변환 (fromJson, toJson)
RepositoryAPI 또는 Firebase와의 통신 담당
ViewModelUI 상태 관리 + 기능 처리 (비즈니스 로직)
ProviderViewModel을 앱 전역에서 연결
View(UI)화면 구성 (Scaffold, 위젯들)
Widgets재사용 가능한 UI 컴포넌트
Utils날짜 포맷, 색상, 문자열 상수 등 공통 유틸

📁 추천 폴더 구조

lib/
├── features/
│   └── post/                        # 게시글 관련 기능
│       ├── post_page.dart          # View - 화면 구성
│       ├── post_view_model.dart    # ViewModel - 상태+로직
│       ├── post_model.dart         # Model - 데이터 구조
│       ├── post_repository.dart    # Repository - Firestore or API 통신
│       └── widgets/
│           └── post_item.dart      # 재사용 UI 위젯
├── providers/
│   └── post_provider.dart          # Provider - ViewModel 연결
├── utils/
│   ├── app_colors.dart             # 공통 색상
│   └── formatters.dart             # 포맷 유틸
└── main.dart                       # 앱 진입점

Model (post_model.dart)

class Post {
  final String id;
  final String title;
  final String content;

  Post({required this.id, required this.title, required this.content});

  factory Post.fromJson(Map<String, dynamic> json) => Post(
    id: json['id'],
    title: json['title'],
    content: json['content'],
  );

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'content': content,
  };
}

Repository (post_repository.dart)

import 'package:cloud_firestore/cloud_firestore.dart';
import 'post_model.dart';

class PostRepository {
  final _firestore = FirebaseFirestore.instance;

  Future<List<Post>> fetchPosts() async {
    final snapshot = await _firestore.collection('posts').get();
    return snapshot.docs.map((doc) => Post.fromJson(doc.data())).toList();
  }

  Future<void> addPost(Post post) async {
    await _firestore.collection('posts').add(post.toJson());
  }
}

ViewModel (post_view_model.dart)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'post_repository.dart';
import 'post_model.dart';

class PostViewModel extends StateNotifier<AsyncValue<List<Post>>> {
  final PostRepository repository;

  PostViewModel(this.repository) : super(const AsyncLoading());

  Future<void> loadPosts() async {
    try {
      final posts = await repository.fetchPosts();
      state = AsyncValue.data(posts);
    } catch (e, st) {
      state = AsyncValue.error(e, st);
    }
  }
}

Provider (post_provider.dart)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/post/post_view_model.dart';
import '../features/post/post_repository.dart';

final postRepositoryProvider = Provider((ref) => PostRepository());

final postProvider =
    StateNotifierProvider<PostViewModel, AsyncValue<List<Post>>>(
        (ref) => PostViewModel(ref.watch(postRepositoryProvider)));

View (post_page.dart)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/post_provider.dart';

class PostPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final postState = ref.watch(postProvider);

    return Scaffold(
      appBar: AppBar(title: Text("게시글 목록")),
      body: postState.when(
        data: (posts) => ListView.builder(
          itemCount: posts.length,
          itemBuilder: (_, i) => ListTile(title: Text(posts[i].title)),
        ),
        loading: () => Center(child: CircularProgressIndicator()),
        error: (e, _) => Center(child: Text("오류 발생: $e")),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(postProvider.notifier).loadPosts(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}
profile
플러터,자바,c언어

0개의 댓글