본문 바로가기
전체보기

[TIL-2]📘 Flutter ViewModel / Model / Provider 구조 이해 & 실제 사례

by 오늘도잡학다식 2025. 4. 11.

 

📘 Flutter ViewModel / Model / Provider 구조 이해 & 실제 사례

🌱 구조 개요

  • Model: 데이터를 구조화 (예: Post)
  • Repository: 데이터 소스 접근 (Firestore, API 등)
  • ViewModel: UI에 필요한 상태 및 로직 보유
  • Provider: ViewModel을 관리하고 UI에서 구독 가능하게 함

1. 🧩 Model - Post 클래스

Firestore에서 가져온 데이터를 구조화하는 클래스

class Post {
  final String id;
  final String writer;
  final String title;
  final String content;
  final DateTime createdAt;
  final String imgUrl;

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

  Post.fromJson(Map<String, dynamic> json)
    : id = json['id'],
      writer = json['writer'],
      title = json['title'],
      content = json['content'],
      createdAt = DateTime.parse(json['createdAt']),
      imgUrl = json['imgUrl'];

  Map<String, dynamic> toJson() => {
    'writer': writer,
    'title': title,
    'content': content,
    'createdAt': createdAt.toIso8601String(),
    'imgUrl': imgUrl,
  };
}

2. 🏗 Repository - PostRepository 클래스

Firestore와 직접 통신하여 데이터를 CRUD 처리

class PostRepository {
  const PostRepository();

  Future<List<Post>> getAll() async {
    final snapshot = await FirebaseFirestore.instance.collection('posts').get();
    return snapshot.docs.map((doc) {
      return Post.fromJson({'id': doc.id, ...doc.data()});
    }).toList();
  }

  Future<bool> insert(Post post) async {
    final doc = FirebaseFirestore.instance.collection('posts').doc();
    await doc.set(post.toJson());
    return true;
  }
}

3. 🧠 ViewModel - HomeViewModel 클래스

리스트 상태를 관리하고 Repository와 연결

class HomeViewModel extends Notifier<List<Post>> {
  final postRepository = const PostRepository();

  @override
  List<Post> build() {
    fetchData();
    return [];
  }

  void fetchData() async {
    final posts = await postRepository.getAll();
    state = posts;
  }
}

4. 🧪 Provider - ViewModel을 UI에 연결

Provider를 통해 ViewModel을 구독하고 상태 반영

final homeViewModel = NotifierProvider<HomeViewModel, List<Post>>(
  () => HomeViewModel(),
);

5. 🖼 UI에서 상태 사용

Consumer 위젯을 사용해 ViewModel의 상태를 구독

Consumer(
  builder: (context, ref, _) {
    final posts = ref.watch(homeViewModel);
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        return Text(posts[index].title);
      },
    );
  },
)
💡 TIP: familyautoDispose를 같이 쓰면 화면별 상태 분리 + 메모리 관리가 편리함!

📌 마무리 요약

  • Model: 데이터를 담는 틀
  • Repository: 데이터를 실제로 불러오고 저장
  • ViewModel: 데이터를 가공/관리하며 UI 상태 책임
  • Provider: ViewModel을 구독하고 View에 바인딩
❗ ViewModel에 비즈니스 로직을 넣고, UI는 상태에 따라 반응만 하도록 역할을 분리하는 것이 핵심입니다.
 
 

📌 Today I Learned - Flutter에서 Riverpod은 pub/sub 구조와 유사하다

Flutter에서 MVVM 아키텍처를 사용할 때 Riverpodpub/sub (publish-subscribe) 패턴과 매우 유사한 동작을 한다. ViewModel이 상태를 변경하면 View가 자동으로 업데이트되므로, 발행자 → 구독자 구조가 자연스럽게 형성된다.

🔧 기본 구조

  • Model: 데이터 구조 및 비즈니스 로직
  • ViewModel: 상태를 관리하고 비즈니스 로직을 수행
  • View: UI 렌더링 및 ViewModel과 연동

🔁 pub/sub 구조와의 비교

요소 pub/sub Riverpod MVVM
Publisher 이벤트 또는 메시지를 발행 StateNotifier (ViewModel)
Subscriber 이벤트를 수신 ref.watch(), Consumer, WidgetRef
Bus / Channel 발행자와 구독자를 연결 Provider (예: StateNotifierProvider)

🧪 예제 코드 요약

1. ViewModel (Publisher)


final counterProvider = StateNotifierProvider<CounterViewModel, int>(
  (ref) => CounterViewModel(),
);

class CounterViewModel extends StateNotifier<int> {
  CounterViewModel() : super(0);

  void increment() => state++;
}
  

2. View (Subscriber)


class CounterView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    final vm = ref.read(counterProvider.notifier);

    return Scaffold(
      body: Center(child: Text('Count: \$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: vm.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}
  

📝 느낀 점

  • Riverpod은 View와 상태를 강결합 없이 관리할 수 있도록 돕는다.
  • MVVM 구조에 자연스럽게 어울리고, pub/sub처럼 명확한 역할 분리가 가능하다.
  • DI, 상태 공유, 비동기 처리까지 폭넓게 확장 가능하다.