프로젝트 디자인 패턴에는 여러 가지가 있지만, 저는 그중에서 MVVM(Model, View, View Model) 아키텍처를 가장 선호합니다. MVVM 아키텍처는 화면과 로직이 분리되어 더욱 직관적으로 파일 관리가 가능합니다. View에는 UI와 관련된 내용만, View Model에는 동작 로직과 관련된 내용만 작성해서 보다 직관적으로 파일 내용을 유추할 수 있도록 하는 것이죠.
GoRouter은 플러터에서 제공하는 공식 패키지 중 하나인데요, 라우팅 기능을 통해 편리하게 페이지 이동이 가능하도록 돕는 패키지입니다. GoRouter를 사용하면 에러, 리디렉팅 등 여러 라우팅 관련 기능들을 적용할 수 있습니다. 이번 포스트에서는 GoRouter를 사용하면서 MVVM 아키텍처를 적용하는 방법에 대해서 알려드리려 합니다.
우선, 라우팅 관리를 위한 go_router와 데이터 관리를 위한 provider 패키지를 설치해 주도록 하겠습니다. 터미널에서 아래 명령어를 입력해 패키지를 설치해 주세요.
flutter pub add go_router
flutter pub add provider
UserModel 생성
MVVM 아키텍처의 첫 구성 요소인 모델(Model)을 먼저 만들어 보겠습니다. 모델에는 전역적으로 관리되어야 하는 변수들이 저장됩니다. 예를 들어, 사용자 모델에는 이름, 아이디, 나이 등의 정보들을 저장하게 되죠. 이번 포스트에서는 간단하게 nickName
하나의 데이터만 다루는 모델을 만들어 보겠습니다.
import 'package:flutter/material.dart';
class UserModel with ChangeNotifier {
String? nickName;
UserModel({this.nickName});
}
Home View Model 생성
모델을 만들었다면, 이제는 모델에 저장된 데이터를 활용해야겠죠. HomeView에서 사용할 HomeViewModel을 만들어 보겠습니다. 우선은 UserModel
과 BuildContext
를 매개 변수로 받아주어야 합니다. UserModel
은 사용자의 데이터를 저장하고 있는 모델이고요, BuildContext
는 위젯 조작을 위해 필요한 객체인데, 특히 페이지 라우팅 등에서 사용됩니다. 마지막으로, 뷰모델이 생성되면 resetNickname()
함수를 통해서 닉네임을 초기화해 주도록 하겠습니다.
UserModel userModel;
BuildContext context;
HomeViewModel({required this.userModel, required this.context}) {
// 닉네임 초기화
resetNickname();
}
resetNickname()
함수는 랜덤 닉네임을 생성하고 모델에 저장하는 함수입니다. 0~99 사이의 랜덤 숫자를 선택해 닉네임을 모델에 저장합니다. 모델에 데이터를 저장하는 방법은 간단합니다. 6번 줄처럼 변수에 바로 접근하면 되는데요, 중요한 점은 닉네임을 변경한 후 notifyListeners()
함수를 사용해 화면을 새롭게 그려주어야 한다는 것입니다.
/// 닉네임을 랜덤하게 변경하는 함수
///
/// 0 ~ 99 사이 랜덤한 숫자 생성 후 `닉네임 00` 형식으로 닉네임 지정
void resetNickname() {
int num = Random().nextInt(100);
userModel.nickName = '닉네임 $num';
notifyListeners();
}
onPressedNicknameBtn()
함수는 페이지 이동 버튼을 클릭했을 때 NicknameView
로 화면 이동을 하는 함수입니다. 처음에 매개 변수로 받았던 BuildContext
를 여기서 사용하게 되는데요, GoRouter의 go()
함수와 함께 사용해서 /home/nickname
주소로 라우팅해 주겠습니다.
/// Nickname View로 이동하는 함수
void onPressedNicknameBtn() {
context.go('/home/nickname');
}
전체 코드입니다.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mvvm_go_router/models/user_model.dart';
class HomeViewModel with ChangeNotifier {
UserModel userModel;
BuildContext context;
HomeViewModel({required this.userModel, required this.context}) {
// 닉네임 초기화
resetNickname();
}
/// 닉네임을 랜덤하게 변경하는 함수
///
/// 0 ~ 99 사이 랜덤한 숫자 생성 후 `닉네임 00` 형식으로 닉네임 지정
void resetNickname() {
int num = Random().nextInt(100);
userModel.nickName = '닉네임 $num';
notifyListeners();
}
/// Nickname View로 이동하는 함수
void onPressedNicknameBtn() {
context.go('/home/nickname');
}
}
Home View 생성
이번에는 HomeView를 만들어 보겠습니다. 간단한 구성입니다. 화면 중앙에 닉네임을 표시하고, 그 아래로 닉네임 변경과 화면 이동 버튼을 배치했습니다.
15번 줄을 살펴보면 provider
를 통해서 HomeViewModel
을 받아오고 있습니다. 이 뷰모델을 사용해서 텍스트 문구를 출력하거나 버튼 클릭 이벤트 등을 처리하는 것을 보실 수 있습니다. UserModel
에 접근해야 할 때도 25번 줄처럼 뷰모델을 통해서 접근하시면 됩니다. 이렇게 하면 View 파일에서는 UI를 그리는 함수들만 존재하게 됩니다. 가독성이 매우 올라가게 되죠.
import 'package:flutter/material.dart';
import 'package:mvvm_go_router/view_models/home_view_model.dart';
import 'package:provider/provider.dart';
class HomeView extends StatefulWidget {
const HomeView({super.key});
@override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
@override
Widget build(BuildContext context) {
HomeViewModel homeViewModel = context.watch<HomeViewModel>();
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 닉네임 텍스트
Text(homeViewModel.userModel.nickName ?? '-'),
const SizedBox(height: 64),
// 닉네임 변경 버튼
ElevatedButton(
onPressed: homeViewModel.resetNickname,
child: const Text('닉네임 변경하기'),
),
const SizedBox(height: 16),
// Nickname View 이동 버튼
ElevatedButton(
onPressed: homeViewModel.onPressedNicknameBtn,
child: const Text('페이지 이동'),
)
],
),
),
);
}
}
NicknameView, NicknameViewModel 생성
이어서 Nickname View와 Nickname View Model을 만들어 보겠습니다. 우선 Nickname View에서 닉네임을 화면 중앙에 표시해 주고, Home View로 되돌아갈 수 있도록 뒤로 가기 버튼도 하나 만들어 주도록 하겠습니다.
import 'package:flutter/material.dart';
import 'package:mvvm_go_router/view_models/nickname_view_model.dart';
import 'package:provider/provider.dart';
class NicknameView extends StatelessWidget {
const NicknameView({super.key});
@override
Widget build(BuildContext context) {
NicknameViewModel nicknameViewModel = context.watch<NicknameViewModel>();
return Scaffold(
appBar: AppBar(
title: const Text('Nickname'),
leading: IconButton(
onPressed: nicknameViewModel.onPressedBackBtn,
icon: const Icon(Icons.arrow_back_ios_new),
),
),
body: Center(child: Text(nicknameViewModel.userModel.nickName ?? '-')),
);
}
}
NicknameViewModel에는 뒤로 가기 버튼을 눌렀을 때 HomeView로 되돌아갈 수 있도록 onPressedBackBtn()
함수를 만들어 주겠습니다. pop()
함수를 사용해서 한 화면 뒤로 이동할 수 있습니다. /home/nickname
경로를 통해 들어왔으니 /home
으로 되돌아가겠죠.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mvvm_go_router/models/user_model.dart';
class NicknameViewModel with ChangeNotifier {
UserModel userModel;
BuildContext context;
NicknameViewModel({required this.userModel, required this.context});
/// 뒤로 가기 버튼 클릭
void onPressedBackBtn() {
context.pop();
}
}
AppRouter 생성
필요한 페이지를 다 만들었으니 이제 페이지간 라우팅을 구현해 보도록 하겠습니다. GoRouter()
함수의 routes
속성에 GoRoute
클래스를 사용해서 라우팅 될 페이지들을 등록해 주면 됩니다. path
속성에 uri를 입력해 주고, ChangeNotifierProvider()
를 사용해서 뷰모델과 화면을 생성해 주면 됩니다. GoRoute
내에도 routes
를 지정할 수 있기 때문에 하위 페이지들을 계속해서 생성할 수 있습니다.
initialLocation
에는 첫 페이지 uri를 입력하면 됩니다. 대부분의 경우에는 로그인 화면 또는 홈 화면이 되겠죠.
import 'package:go_router/go_router.dart';
import 'package:mvvm_go_router/models/user_model.dart';
import 'package:mvvm_go_router/view_models/home_view_model.dart';
import 'package:mvvm_go_router/view_models/nickname_view_model.dart';
import 'package:mvvm_go_router/views/home_view.dart';
import 'package:mvvm_go_router/views/nickname_view.dart';
import 'package:provider/provider.dart';
class AppRouter {
final UserModel userModel;
AppRouter({required this.userModel});
static GoRouter getRouter(UserModel userModel) {
return GoRouter(
initialLocation: '/home',
routes: [
GoRoute(
path: '/home',
builder: (context, state) => ChangeNotifierProvider(
create: (context) =>
HomeViewModel(userModel: userModel, context: context),
child: const HomeView(),
),
routes: [
GoRoute(
path: 'nickname',
builder: (context, state) => ChangeNotifierProvider(
create: (context) =>
NicknameViewModel(userModel: userModel, context: context),
child: const NicknameView(),
),
)
],
)
],
);
}
}
MainApp 수정하기
마지막으로 MainApp
클래스만 수정하면 됩니다. 기존 MaterialApp
위젯을 통해 화면을 바로 그리던 것을 MaterialApp.router
를 사용해서 라우터를 적용시켜 주면 됩니다. 우선 UserModel
과 라우터를 선언하고, 이어서 MaterialApp
의 routerConfig
속성으로 라우터를 입력해 주면 됩니다.
import 'package:flutter/material.dart';
import 'package:mvvm_go_router/config/app_router.dart';
import 'package:mvvm_go_router/models/user_model.dart';
void main() {
runApp(const MainApp());
}
// Models
UserModel userModel = UserModel();
// Router
final _router = AppRouter.getRouter(userModel);
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
이제 앱을 실행하고 버튼들을 클릭해 보면 닉네임이 변경되고, 페이지 이동을 하더라도 변경된 닉네임이 그대로 유지되는 것을 보실 수 있습니다. 지금은 닉네임 하나만 사용했지만, 실제 앱에서는 더 복잡하고 많은 변수를 저장하게 되겠죠. 모델을 여러 개 만들어서 사용하는 것도 가능합니다.
'프로그래밍 > 플러터' 카테고리의 다른 글
Homebrew를 사용한 맥북 플러터 설치하기 (0) | 2024.08.09 |
---|---|
플러터 커스텀 카메라 만들기 (2) | 2024.07.19 |
플러터 다크모드 설정 페이지 만들기 (0) | 2024.07.10 |
플러터 시작하기 05. 버튼 종류를 알아보자! (0) | 2024.05.16 |
플러터 시작하기 02. Appbar 꾸미기 (0) | 2024.05.14 |