MVVM 패턴과 상태 관리 3 : MVVM 구현 (Provider)
이전 게시물에 이어서 이번에는 GetX
상태관리 라이브러리를 사용하여 구현해보았다.
class AuthService {
// 인증 번호 요청
Future<void> requestPhoneVerification({required String phoneNumber}) async {
return;
}
// 인증번호 인증
Future<bool> verifySMSCode({required String smsCode}) async {
return true;
}
// 로그인 상태 확인
Future<bool> isLoggedIn() async {
return true;
}
}
Provider
와 동일하다
import 'package:flutter_velog/services/auth_service.dart';
import 'package:get/get.dart';
enum LoginViewState { idle, error, loading, smsCodeSent, loggedIn }
class LoginViewModel extends GetxController {
final AuthService authService;
LoginViewModel({required this.authService});
// 상태 변수
RxString phoneNumber = ''.obs;
Rx<LoginViewState> state = LoginViewState.idle.obs;
// 로그인 상태 점검
Future<void> verifyLoginStatus() async {
if (await authService.isLoggedIn()) {
state.value = LoginViewState.loggedIn;
}
}
Future<void> phoneNumberLogin() async {
// 로딩
state.value = LoginViewState.loading;
// 인증요청 시도
try {
await authService.requestPhoneVerification(
phoneNumber: phoneNumber.value);
// 인증화면전환
state.value = LoginViewState.smsCodeSent;
} catch (e) {
// 인증요청 에러
state.value = LoginViewState.error;
}
}
}
class LoginViewModel extends GetxController { ... }
GetxController
를 상속받아 구현된다.
// 변수 초기화
RxString phoneNumber = ''.obs;
// 변수 사용
phoneNumber.value = '01012345555';
Text(phoneNumber.value);
GetX
고유의 Rx
타입 변수로 선언하고, 초기화 값에 .obs
를 붙여 초기화 한다..value
를 붙여 값을 재할당 또는 사용한다.Provider
를 이용한 설계와 크게 다르지 않다.import 'package:flutter_velog/services/auth_service.dart';
import 'package:get/get.dart';
enum SmsCodeViewState { idle, error, loading, verified }
class SmsCodeViewModel extends GetxController {
final AuthService authService;
SmsCodeViewModel({required this.authService});
// 인증코드
RxString smsCode = ''.obs;
// 상태
Rx<SmsCodeViewState> state = SmsCodeViewState.idle.obs;
Future<void> verifySMSCode() async {
// 로딩
state.value = SmsCodeViewState.loading;
// 인증 시도
try {
bool success = await authService.verifySMSCode(smsCode: smsCode.value);
if (success) {
// 성공 시 화면전환
state.value = SmsCodeViewState.verified;
} else {
// 실패 시 에러
state.value = SmsCodeViewState.error;
}
} catch (e) {
// 인증 에러
state.value = SmsCodeViewState.error;
}
}
}
LoginViewModel
과 동일한 형식이다.
import 'package:flutter/material.dart';
import 'package:flutter_velog/services/auth_service.dart';
import 'package:flutter_velog/viewmodels/login_view_model.dart';
import 'package:flutter_velog/views-getx/login_view.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter MVVM',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const LoginView(),
);
}
}
build(BuildContext context) {
return GetMaterialApp( ... );
}
Widget
기존의 MaterialApp
을 GetMaterialApp
으로 변경한다.
import 'package:flutter/material.dart';
import 'package:flutter_velog/home_view.dart';
import 'package:flutter_velog/services/auth_service.dart';
import 'package:flutter_velog/viewmodels-getx/login_view_model.dart';
import 'package:flutter_velog/views-getx/sms_code_view.dart';
import 'package:get/get.dart';
class LoginView extends StatelessWidget {
const LoginView({super.key});
final String phoneNumberString = "휴대폰 번호";
final String loginString = "로그인";
final String errorMessage = "전화번호를 확인해주세요요";
Widget build(BuildContext context) {
final LoginViewModel loginViewModel =
Get.put(LoginViewModel(authService: AuthService()));
return Obx(() {
WidgetsBinding.instance.addPostFrameCallback(
(_) async {
// 로그인 상태 검사
await loginViewModel.verifyLoginStatus();
if (loginViewModel.state.value == LoginViewState.loggedIn) {
// 홈화면 이동
Get.to(const HomeView());
} else if (loginViewModel.state.value == LoginViewState.smsCodeSent) {
// 인증번호 입력 이동
Get.to(const SmsCodeView());
}
},
);
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
loginViewModel.state.value == LoginViewState.loading
? const CircularProgressIndicator()
: TextField(
onChanged: (value) =>
loginViewModel.phoneNumber.value = value,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
hintText: phoneNumberString),
),
const SizedBox(height: 10),
if (loginViewModel.state.value == LoginViewState.error)
Text(
errorMessage,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 10),
SizedBox(
width: double.maxFinite,
child: FilledButton(
onPressed: () => loginViewModel.phoneNumberLogin(),
child: Text(loginString)))
],
),
);
});
}
}
final LoginViewModel loginViewModel =
Get.put(LoginViewModel(authService: AuthService()));
Get.put()
을 이용하여 의존성을 주입할 수 있다.
return Obx(() {
...
};
Obx()
위젯을 이용하여 상태 변수 값이 변경되면 자동으로 감지하여 리빌드 하도록 할 수 있다.
import 'package:flutter/material.dart';
import 'package:flutter_velog/viewmodels-getx/login_view_model.dart';
import 'package:flutter_velog/viewmodels-getx/sms_code_view_model.dart';
import 'package:flutter_velog/home_view.dart';
import 'package:get/get.dart';
class SmsCodeView extends StatelessWidget {
const SmsCodeView({super.key});
final String verificationCodeString = "인증 번호";
final String verificationString = "인증하기";
final String errorMessage = "인증번호를 확인해주세요";
Widget build(BuildContext context) {
final LoginViewModel loginViewModel = Get.find<LoginViewModel>();
final SmsCodeViewModel smsCodeViewModel =
Get.put(SmsCodeViewModel(authService: loginViewModel.authService));
return Obx(() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (smsCodeViewModel.state.value == SmsCodeViewState.verified) {
Get.to(const HomeView());
}
});
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(80),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
smsCodeViewModel.state.value == SmsCodeViewState.loading
? const CircularProgressIndicator()
: TextField(
onChanged: (value) =>
smsCodeViewModel.smsCode.value = value,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
hintText: verificationCodeString),
),
const SizedBox(height: 10),
if (smsCodeViewModel.state.value == SmsCodeViewState.error)
Text(
errorMessage,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 10),
SizedBox(
width: double.maxFinite,
child: FilledButton(
onPressed: () => smsCodeViewModel.verifySMSCode(),
child: Text(verificationString)))
],
),
),
);
});
}
}
final LoginViewModel loginViewModel = Get.find<LoginViewModel>();
Get.find()
를 이용하여 주입한 인스턴스를 불러와 사용할 수 있다.