Flutter

2024. 11. 18 Flutter UI 프레임워크 riverpod 과 MVVM 활용(8)

정훈5 2024. 11. 18. 13:29

 

PostListPage 화면을 만들어 보자 그런데 뷰 모델은 어떻게 가지고 올까

post_list_view_model.dart

더보기
import 'package:class_riverprod_mvvm/repository/post_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../models/post.dart';

// 리버팟 중에 notifier 계열이 상태 관리를 담당해주는 클래스이다.

// StateNotifier
// 1. 멤버 변수로 T state 변수를 가지고 있다.
// 2. 캡슐화에 핵심이다.
class PostListViewModel extends StateNotifier<List<Post>> {

  // // 관리해야 되는 상태는 뭘까?
  // // 화면 --> [ [post], [post], [post] ]
  //
  // List<Post> postList = [];
  // // 화면을 다시 갱신 해주어야 한다.
  // // 리버팟을 사용하면 자동으로 갱신을 해 준다.


  // 통신 요청을 통해서 데이터를 가져오는 비즈니스 로직을 담당 시킨다.
  final PostRepository _postRepository;

  // T state --> List<Post> <---
  // 맨 처음 부모클래스 StateNotifier를 가지고 있는 PostListViewModel에 상태는
  // 당연히 빈 값을 들고 있다.
  PostListViewModel(this._postRepository): super([]) {
    // 통신 요청이 아니라 나의 비즈니스 로직을 호출
    fetchPosts(); // 나의 클래스에 멤버 함수 호출
  }
   
  // 비즈니스 로직
  Future<void> fetchPosts() async {
      // List<Post> = []
    try {
      final posts = await _postRepository.fetchPosts();
      // List<Post>
      state = posts;
    } catch(e) {
      // List<Post>
    }
  } // end of fetchPosts()

  // 삭제하는 비즈니스 로직을 만들어 보자.
  Future<void> deletePost(int id) async {
    try{
      await _postRepository.deletePost(id);
      // 리스트 10개중에 1개를 삭제 했음 --> List<Post> 값 상태가 변경이 되었다.
      // state --> [Post(), Post(), Post() ];

      // 상태가 변경이 되면 새로운 리스트 객체를 state 변수에 넣어 주어야 한다.
      // 상태가 변경이 되었구나 확인해서 화면을 자동으로 갱신해 준다.

      // state --> [Post(), 삭제, Post() ];
      state = state.where( (post) => post.id != id ).toList(); // 새로운 리스트 객체가 생성
      print("삭제완료");
    }catch(e) {
      print('삭제 실패 : ${e}');
    }
  } // end of deletePost()

}

 

main.dart

더보기
import 'package:class_riverprod_mvvm/view/page/post_list_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 우리 앱에서 사용하는 모든 Provider를 하나의 컨테이너(ProviderContainer)로 묶어서 관리 합니다.
void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: NovaBlog(),
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return PostListPage(

    );
  }
}

 

post_list_page.dart

더보기
import 'package:class_riverprod_mvvm/providers/state_noti_provider/post_list_view_model_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../models/post.dart';

// MVVM -> View는 뷰 모델 인스턴스만 바라보면 된다.
// ConsumerWidget은 Provider 생태를 구독하여, 상태가 변경될 때, 자동으로 UI 업데이트 되도록 설계 가능 하다.
class PostListPage extends ConsumerWidget {
  const PostListPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 게시글에 리스트 화면을 구현해야 한다.
    // 뷰 모델을 관리하는 프로바이더를 호출하는데 계속 관찰하는 입장이다.
    final postList = ref.watch(postListViewModelProvider);
    // List<Post> postList <--
    return Scaffold(
      appBar: AppBar(
        title: Text('게시글 목록 화면'),
      ),
      body: postList.isEmpty
          ? Center(
        child: Text('게시글이 존재하지 않습니다.'),
      )
          : ListView.separated(
        itemBuilder: (context, index) {
          Post post = postList[index];
          return ListTile(
              title: Text(
                post.title,
                style: TextStyle(color: Colors.orangeAccent),
              ),
              subtitle: Text(post.body),
              trailing: IconButton(
                onPressed: () async {
                  bool confirm = await showDialog(
                    context: context,
                    builder: (context) => AlertDialog(
                      title: const Text('삭제'),
                      content: Text('${post.title} 를 삭제 하시겠습니까?'),
                      actions: [
                        TextButton(
                            onPressed: () {
                              Navigator.of(context).pop(false);
                            },
                            child: Text('취소')),
                        TextButton(
                            onPressed: () {
                              Navigator.of(context).pop(true);
                            },
                            child: Text('확인'))
                      ],
                    ),
                  );

                  // 뷰모델에 접근해서 deletePost() 메서드를 호출해야 한다.
                  // 상태를 그냥 보는 것은 직접 접근이 가능하다.
                  // 해당하는 프로바이더에 상태 변경 요청은 창고 관리자한테 의뢰해야 한다.
                  if (confirm) {
                    await ref
                        .read(postListViewModelProvider.notifier)
                        .deletePost(post.id!); // null 방지
                    // 삭제 완료 후 스택바로 피드백 제공
                    ScaffoldMessenger.of(context)
                        .showSnackBar(SnackBar(content: Text('삭제 완료')));
                  }
                },
                icon: Icon(
                  Icons.delete,
                  color: Colors.redAccent,
                ),
              ),
          );
        },
          separatorBuilder: (context, index) => const Divider(
            thickness: 1,
          ),
          itemCount: postList.length),
    );
  }
}