[Flutter]Provider를 이용한 MVVM 패턴 적용

한상욱·2024년 5월 27일
1

Flutter

목록 보기
12/26
post-thumbnail

들어가며

Flutter에서 상태관리를 도입한 프로젝트는 필요에 따라서 MVC, MVP, MVVM 그 외에도 여러가지 아키텍쳐 패턴을 적용할 수 있습니다. 그 중 MVVM은 기존의 MVC, MVP의 문제점을 개선한 아키텍쳐 패턴이에요.

디자인 패턴은 코딩을 하며 자주 나타나게 되는 문제점의 해결책입니다. 아키텍쳐 패턴은 디자인 패턴처럼 특정 유형의 문제에 중점을 두는 것이 아니라 프로젝트 전체적인 문제 해결에 중점을 두었다고 할 수 있습니다.

MVVM 패턴

MVVM 패턴은 Model, View, ViewModel의 앞글자를 따서 이름을 지었어요. Model은 큰 범주에서는 객체이지만, 실질적으로는 데이터의 객체입니다. View는 말 그대로 눈에 보이는 화면을 의미해요. 실제로 Model을 사용자에게 보여주는 UI를 의미하게 됩니다. ViewModel은 View로부터 사용자의 입력을 받아 Model에 전달하고 Model은 그에 맞는 메소드를 실행하게 하는 역할을 합니다.

Provider를 이용한 MVVM 구축

Provider는 대표적은 상태관리 라이브러리입니다. 사실, Provider는 Provider 패턴으로 더 많이 사용됩니다. 그래도 어느정도 유사하니, MVVM 패턴을 구축할때 이용해보겠습니다.

이 디렉토리 구조는 게시판 서비스를 예로 MVVM 패턴의 폴더 구조를 만들어본 것입니다. 사람마다 차이가 있지만 대부분 비슷한 형식이에요.

model은 데이터의 model 클래스를 모아둔 디렉토리입니다. repository는 datasouce에 접근하여 데이터를 조회, 생성, 삭제, 수정 등의 작업을 하는 repository 객체를 모아둔 디렉토리에요. service는 실제로 repository에 접근하여 데이터를 전달받는 역활을 하게 됩니다. view는 말 그대로 사용자에게 보여주는 ui들이 모여있습니다. view는 특히, page, view, view model로 구성되어있습니다. page는 실제로 사용자에게 보여지는 하나의 UI를 의미하며 view는 UI의 구성단위에요.

자, 대충 이론적인 설명이 끝났으니 게시판 서비스를 조회하면서 MVVM 패턴을 적용해보겠습니다.

게시판 서비스

게시판 서비스는 제가 직접 만들어 놓은 게시판 서비스의 일부 메소드를 이용할 것입니다. 해당 서비스의 소스코드는 아래의 링크에서 확인할 수 있습니다.

게시판서버 소스코드

Model

model은 freezed 패키지를 이용해서 구축하겠습니다. freezed 패키지에 대한 내용은 아래의 링크에서 확인할 수 있습니다.

Freezed 패키지 관련글

import 'package:freezed_annotation/freezed_annotation.dart';

part 'post.freezed.dart';
part 'post.g.dart';


class Post with _$Post {
  factory Post({
    required int id,
    required String title,
    required String post,
    required int userId,
    required bool isPublic,
  }) = _Post;

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}

이제 터미널에서 build_runner를 실행시키면 아주 간단한 Model이 생성되었습니다.

제가 구축한 게시판 서비스는 Spring boot 기반으로 제작하였는데, 기본적으로 BaseResponse 라는 객체에 담아서 데이터를 전송합니다. 해당 객체에는 요청의 성공여부를 의미하는 status, 해당 요청에 따른 data, 성공 여부에 따른 resultMsg 필드로 데이터를 전달합니다. 이를 위해서 BaseResponse 객체도 생성해주겠습니다. 이는 toJson 같은 메소드가 필요하지 않으니, freezed를 이용하진 않겠습니다.

class BaseResponse<T> {
  final String status;
  final T data;
  final String resultMsg;

  BaseResponse({
    required this.status,
    required this.data,
    required this.resultMsg,
  });

  factory BaseResponse.fromJson(Map<String, dynamic> json) {
    return BaseResponse(
        status: json["status"],
        data: json["data"],
        resultMsg: json["resultMsg"]);
  }
}

Repository

repository는 실질적인 데이터를 조회, 생성, 삭제, 수정 등의 역활을 담당합니다. 여기서는 조회 기능만 구축하겠습니다.

import 'dart:convert';

import 'package:http/http.dart' as http;

import '../model/base_response.dart';

class PostRepository {
  Future<BaseResponse> getAllPosts() async {
    final response =
        await http.get(Uri.parse("[실제 서버의 API URL]"));
    if (response.statusCode == 200) {
      final Map<String, dynamic> data =
          jsonDecode(utf8.decode(response.bodyBytes));
      return BaseResponse.fromJson(data);
    } else {
      throw Exception();
    }
  }
}

REST API를 위해서 http 라이브러리를 이용했습니다. 요청이 성공할 경우 데이터의 한글깨짐 현상을 방지하기 위해 utf8.decode 메소드를 이용해서 데이터를 가져왔습니다.

Service

service는 repository로부터 데이터를 요청 및 응답을 반환하는 역활을 합니다.

import 'package:flutter_provider_rest_api/src/model/post.dart';
import 'package:flutter_provider_rest_api/src/repository/post_repository.dart';

class PostService {
  final PostRepository postRepository;
  PostService({required this.postRepository});

  Future<List<Post>> getAllPosts() async {
    try {
      final result = await postRepository.getAllPosts();

      final List data = result.data;
      return data.map((json) => Post.fromJson(json)).toList();
    } on Exception catch (e) {
      print(e.toString());
      return [];
    }
  }
}

ViewModel

viewModel은 이제 사용자로부터 전달받은 입력을 model에 전달해서 데이터를 조작하게 됩니다. 실제로 여기서 사용자로부터 입력받는 것은 필요가 없고, 오직 데이터를 서버로 부터 가져와서 해당 데이터를 뿌려주는 역활을 할 것입니다.

import 'package:flutter/cupertino.dart';
import 'package:flutter_provider_rest_api/src/model/post.dart';
import 'package:flutter_provider_rest_api/src/service/post_service.dart';

class PostViewModel extends ChangeNotifier {
  List<Post> _posts = [];

  List<Post> get posts => _posts;

  final PostService postService;

  PostViewModel({required this.postService}) {
    fetchData();
  }

  Future<void> fetchData() async {
    final result = await postService.getAllPosts();
    if (result.isNotEmpty) {
      _posts.clear();
      _posts.addAll(result);
      notifyListeners();
    }
  }
}

View

view는 viewModel로부터 데이터를 전달받아 사용자에게 UI 컴포넌트로써의 역활을 하게 됩니다.

import 'package:flutter/material.dart';
import 'package:flutter_provider_rest_api/src/model/post.dart';
import 'package:flutter_provider_rest_api/src/view/post/post_view_model.dart';
import 'package:provider/provider.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("게시판"),
        ),
        body: Consumer<PostViewModel>(builder: (context, provider, child) {
          return ListView.builder(
              itemCount: provider.posts.length,
              itemBuilder: (context, index) {
                final Post post = provider.posts[index];
                return ListTile(
                  title: Text(
                    post.title,
                    style: TextStyle(
                        color: Colors.black, fontWeight: FontWeight.bold),
                  ),
                  subtitle: Text(
                    post.post,
                    style: TextStyle(color: Colors.black),
                  ),
                );
              });
        }));
  }
}

Page

page는 실제로 사용자에게 보여지는 하나의 UI입니다. 여기에는 여러가지 view가 모여있을수도 있죠. provider를 통해 해당 viewModel을 모두 전달해줄수도 있습니다. 우선은 하나의 Provider를 통해 View를 구성했으므로, 단순히 ChangeNotifierProvider를 통해 Provider를 전달해주도록 하겠습니다.

import 'package:flutter/material.dart';
import 'package:flutter_provider_rest_api/src/repository/post_repository.dart';
import 'package:flutter_provider_rest_api/src/service/post_service.dart';
import 'package:flutter_provider_rest_api/src/view/post/post_view.dart';
import 'package:flutter_provider_rest_api/src/view/post/post_view_model.dart';
import 'package:provider/provider.dart';

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

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => PostViewModel(
          postService: PostService(postRepository: PostRepository())),
      child: const PostView(),
    );
  }
}

이로써, MVVM패턴을 적용한 게시판 앱이 완성되었습니다.

전체 소스코드 링크

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

1개의 댓글

comment-user-thumbnail
2024년 5월 28일

잘 참고하고 갑니다 :)

답글 달기