lib/
├── models/ # 데이터 모델 정의하는 폴더 (예: Post라는 데이터 구조를 정의)
│ └── post.dart # Post 클래스: id, title 같은 데이터를 담는 객체
│
├── repositories/ # 데이터 소스와 통신하는 역할 (DB, API 등)
│ ├── firestore_repository.dart # Firestore(DB)와 통신하는 코드 (읽고 쓰기)
│ └── api_repository.dart # 외부 REST API 서버랑 통신하는 코드 (HTTP 요청)
│
├── viewmodels/ # 앱의 비즈니스 로직, 상태 관리를 담당 (MVVM에서 ViewModel 역할)
│ └── post_viewmodel.dart # Firestore, API에서 가져온 데이터를 화면에 전달할 준비를 하는 로직
│
├── pages/ # 화면(UI) 관련 코드 모아놓은 폴더
│ └── post_page.dart # 화면에 실제 리스트 뿌려주는 UI 페이지
│
└── main.dart # 앱 시작점. ProviderScope(상태관리) 세팅하고 첫 화면(PostPage) 보여주는 코드
models/post.dart
class Post {
final String id;
final String title;
Post({required this.id, required this.title});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
);
}
}
repositories/firestore_repository.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/post.dart';
class FirestoreRepository {
final _db = FirebaseFirestore.instance;
Future<List<Post>> fetchPosts() async {
final snapshot = await _db.collection('posts').get();
return snapshot.docs.map((doc) => Post(
id: doc.id,
title: doc['title'],
)).toList();
}
}
repositories/api_repository.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/post.dart';
class ApiRepository {
final String apiUrl = 'https://jsonplaceholder.typicode.com/posts';
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse(apiUrl));
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
}
}
viewmodels/post_viewmodel.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/post.dart';
import '../repositories/firestore_repository.dart';
import '../repositories/api_repository.dart';
final postViewModelProvider = StateNotifierProvider<PostViewModel, AsyncValue<List<Post>>>(
(ref) => PostViewModel(ref),
);
class PostViewModel extends StateNotifier<AsyncValue<List<Post>>> {
PostViewModel(this.ref) : super(const AsyncLoading());
final Ref ref;
final _firestoreRepo = FirestoreRepository();
final _apiRepo = ApiRepository();
Future<void> fetchPostsFromFirestore() async {
try {
final posts = await _firestoreRepo.fetchPosts();
state = AsyncData(posts);
} catch (e, st) {
state = AsyncError(e, st);
}
}
Future<void> fetchPostsFromApi() async {
try {
final posts = await _apiRepo.fetchPosts();
state = AsyncData(posts);
} catch (e, st) {
state = AsyncError(e, st);
}
}
}
pages/post_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../viewmodels/post_viewmodel.dart';
class PostPage extends ConsumerWidget {
const PostPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final postsState = ref.watch(postViewModelProvider);
return Scaffold(
appBar: AppBar(title: const Text('Posts')),
body: postsState.when(
data: (posts) => ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post.title),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'firestore',
onPressed: () => ref.read(postViewModelProvider.notifier).fetchPostsFromFirestore(),
child: const Icon(Icons.cloud),
),
const SizedBox(height: 16),
FloatingActionButton(
heroTag: 'api',
onPressed: () => ref.read(postViewModelProvider.notifier).fetchPostsFromApi(),
child: const Icon(Icons.api),
),
],
),
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'pages/post_page.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: PostPage(),
);
}
}
사용자 → 페이지(UI) → ViewModel(상태관리) → Repository(API/DB 통신) → 데이터(Model)
의미
M (Model) | 데이터 구조, 비즈니스 로직, 실제 데이터 (예: Post 모델, API 응답 데이터)
V (View) | 사용자한테 보여지는 화면(UI) (예: Flutter 페이지, 버튼, 텍스트)
VM (ViewModel) | View와 Model을 연결해주는 중간 관리자 (상태 관리, 로직 처리)