개발노트

47. [Flutter] MVVM + Repository 적용, 상태관리(with Riverpod, SharedPreferences 패키지) 본문

앱 개발/Flutter

47. [Flutter] MVVM + Repository 적용, 상태관리(with Riverpod, SharedPreferences 패키지)

mroh1226 2023. 11. 17. 15:47
반응형

MVVM 패턴

MVVM(Mode-View-ViewModel)은 소프트웨어 아키텍처 패턴 중 하나로, 주로 사용자 인터페이스를 구축하는 데 적합한 패턴입니다. 

  • Model: 데이터와 비즈니스 로직을 담당하는 부분.
  • View: 사용자 인터페이스를 담당하는 부분.
  • ViewModel: View와 Model 간의 중간자로, 상태 관리 및 비즈니스 로직을 처리.

MVVM 패턴은 몇 가지 주요 장점이 있어 많은 개발자들이 선호하는데, 이를 자세히 설명해보겠습니다.

  • 분리된 역할(Role Separation):
    MVVM은 각 구성 요소가 명확하게 분리되어 있습니다. Model은 데이터와 비즈니스 로직을 처리하고, View는 사용자 인터페이스를 담당하며, ViewModel은 View와 Model 간의 통신을 중개합니다. 이로써 코드가 각 부분에 집중되어 유지 보수와 테스트가 쉬워집니다.
  • 유연성(Flexibility):
    MVVM은 느슨한 결합(Loose Coupling)을 촉진합니다. 각 구성 요소는 서로에게 독립적이며, 변경이 발생해도 다른 부분에 미치는 영향을 최소화합니다. 이로써 새로운 기능 추가나 기존 기능의 변경이 간편합니다.
  • 테스트 용이성(Testability):
    ViewModel은 비즈니스 로직을 캡슐화하고, 이로써 ViewModel을 테스트하기 쉬워집니다.
  • 재사용성(Reusability):
    ViewModel은 UI와 분리되어 있기 때문에, 여러 화면에서 재사용이 가능합니다. 동일한 ViewModel을 여러 View에서 사용할 수 있어 개발 생산성을 높이고 중복 코드를 최소화합니다.
  • 데이터 바인딩(Data Binding):
    MVVM은 데이터 바인딩을 통해 View와 ViewModel 사이의 데이터 동기화를 쉽게 구현할 수 있습니다. 이로써 사용자 인터페이스 업데이트 및 사용자 입력 처리가 간편해지며, 코드의 가독성이 향상됩니다.
  • 비즈니스 로직 집중(Business Logic Focus):
    MVVM은 비즈니스 로직을 ViewModel에 중점을 두어 관리합니다. 이로써 UI 코드와 비즈니스 로직이 혼재되지 않고, 코드의 가독성이 높아집니다.
  • 반응형(Reactive) 프로그래밍 지원:
    MVVM 패턴은 반응형 프로그래밍과 쉽게 통합될 수 있습니다. 데이터 바인딩과 같은 기술을 사용하여 데이터의 변화에 반응하고 UI를 업데이트할 수 있습니다.

Repository

Repository 패턴은 데이터 소스와 애플리케이션 간의 중재자 역할을 하는 디자인 패턴입니다. 애플리케이션은 Repository를 통해 데이터에 접근하며, 데이터의 원본(로컬Local, 원격 Remote 등)에 대한 구체적인 구현은 Repository에서 캡슐화됩니다.

 

출처: flutter Awesome


시작전 패키지 설치.

Riverpod

Riverpod은 Provider 패키지의 확장으로, Flutter 애플리케이션에서 상태 관리를 용이하게 할 수 있는 라이브러리입니다. NotifierProvider를 사용하여 상태를 효과적으로 관리할 수 있습니다.

 

pubdev Riverpod 설치 링크: https://pub.dev/packages/flutter_riverpod/install

 

flutter_riverpod | Flutter Package

A simple way to access state from anywhere in your application while robust and testable.

pub.dev

SharedPreference

SharedPreferences는 Flutter 애플리케이션에서 간단한 키-값 쌍의 형태로, 기기 내의 저장소를 사용하여 데이터를 지속적으로 저장하고 검색하는 데 사용되는 패키지입니다. 앱의 설정, 사용자 기본 설정 등을 저장할 수 있습니다.

 

pubdev SharedPreferences 설치 링크: https://pub.dev/packages/shared_preferences

 

shared_preferences | Flutter Package

Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.

pub.dev


MVVM + Repository 폴더 구조

/lib
|-- /data
|   |-- repository.dart
|-- /models
|   |-- user.dart
|-- /viewmodels
|   |-- user_viewmodel.dart
|-- /views
|   |-- home_screen.dart
|-- main.dart

MVVM + Repository 예시 소스.

내용: 사용자 (User)가 이메일을 받는지(receiveEmail) Push알람을 받는지(receivePush) 여부를 상태관리하는 예시

Model

models/user.dart
//Model
class User {
  final bool receiveEmail; //이메일 받는지 여부
  final bool receivePush; //Push 알람 받는지 여부
  User({required this.receiveEmail, required this.receivePush});
}​
  • Entity와 생성자를 선언하여, 사용할 Data의 기본 형태를 만들어줍니다.
  • Model에 기본 Data가 될 User 객체는 Bool형receiveEmailreceivePush 갖습니다.


Repository

data/repository.dart
//Repository
class UserRepository {
  final SharedPreferences _preferences;
  UserRepository(this._preferences);
  //기기에 데이터 저장하기 위해 SharedPreferences 패키지 사용
  //Repository를 생성할 때, SharedPreferences 를 인자로 받도록함(생성자)

  User? getUser() {
    final receiveEmail = _preferences.getBool('receiveEmail');
    final receivePush = _preferences.getBool('receivePush');
    //SharedPreferences.getString()으로 기기에 저장된 key의 value를 가져옴

    if (receiveEmail == null || receivePush == null) {
      return User(receiveEmail: false, receivePush: false);
    }
    return User(receiveEmail: receiveEmail, receivePush: receivePush);
    //값이 없으면 return null, 있으면 User에 값을 채워 return함
  }

  Future<void> setReceiveEmail(bool value) async {
    await _preferences.setBool('receiveEmail', value);
    //SharedPreferences.setString()으로 key 값을 기기에 저장함
  }

  Future<void> setReceivePush(bool value) async {
    await _preferences.setBool('receivePush', value);
  }
  
}​​
  • Repository 에는 SharedPreference를 사용하여 기기에 저장된 데이터를
    getBool('key이름') 불러오고, setBool('key이름')저장 할 수 있는 기능들을 구현합니다.
  • getUser(): 기기에 저장된 'receiveEmail', 'receivePush' 값을 가져옴
  • key 값이 아직 없을 경우를 위해 if 문으로 예외처리해줍니다.
  • setReceiveEmail(bool value): 기기에 저장된  key 'receiveEmail' 에 value 값을 저장함
  • setReceivePush(bool value):  기기에 저장된  key 'receivePush' 에 value 값을 저장함

ViewModel

viewmodels/user_viewmodel.dart
class UserViewModel extends Notifier<User> {
  final UserRepository _repository;
  UserViewModel(this._repository);
  //UserRepository를 인자로 받는 UserViewModel 생성

  void setReceiveEmail(bool value) {
    _repository.setReceiveEmail(value);
    //Repository의 set 메소드 호출로 값을 저장함
    state = User(receiveEmail: value, receivePush: state.receivePush);
    //state를 통해 상태를 업데이트 시키고 User의 변화를 Notifier에 알림
  }

  void setReceivePush(bool value) {
    _repository.setReceivePush(value);
    state = User(receiveEmail: state.receiveEmail, receivePush: value);
  }

  @override
  User build() {
    // UserRepository를 통해 현재 유저 정보를 기기 저장소로 부터 가져와서 build()로 초기설정함
    User? user = _repository.getUser();
    return User(
        receiveEmail: user!.receiveEmail, receivePush: user.receivePush);
  }
}


final userViewModelProvider =
    NotifierProvider<UserViewModel, User>(() => throw UnimplementedError());
  • ViewModel
  • Notifier<>에 User를 넣어 User의 state 를 관리할 수 있도록합니다.
  • view에서 사용될 메소드들 setReceiveEmail(), setReceivePush() 을 만들어주고, Repository를 통해 값을 저장하고 state로 변화를 Notifier에 알리도록 작성합니다.
  • @override User build()를 사용하여 User의 초기 값을 저장소로부터 가져오도록 작성합니다.

StatefulWidget 👉🏻 ConsumerStatefulWidget 변경예시)


View

views/home_screen.dart

1. ConsumerWidget 을 사용할 경우 (StatelessWidget 과 유사)
//view
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Column(
        children: [
          SwitchListTile.adaptive(
            title: const Text('receiveEmail'),
            value: ref.watch(userViewModelProvider).receiveEmail,
            onChanged: (value) {
              ref.read(userViewModelProvider.notifier).setReceiveEmail(value);
            },
          ),
          SwitchListTile.adaptive(
            title: const Text('receivePush'),
            value: ref.watch(userViewModelProvider).receivePush,
            onChanged: (value) {
              ref.read(userViewModelProvider.notifier).setReceivePush(value);
            },
          ),
        ],
      ),
    );
  }
}​
2. ConsumerStatefulWidget 사용할 경우(StatefulWidget 과 유사)
//view
class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});

  @override
  HomePageState createState() => HomePageState();
}

class HomePageState extends ConsumerState<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          SwitchListTile.adaptive(
            title: const Text('receiveEmail'),
            value: ref.watch(userViewModelProvider).receiveEmail,
            onChanged: (value) {
              ref.read(userViewModelProvider.notifier).setReceiveEmail(value);
            },
          ),
          SwitchListTile.adaptive(
            title: const Text('receivePush'),
            value: ref.watch(userViewModelProvider).receivePush,
            onChanged: (value) {
              ref.read(userViewModelProvider.notifier).setReceivePush(value);
            },
          ),
        ],
      ),
    );
  }
}​
  • NotifierProvider로 선언해주었던 userViewModelProvider로 아래와 같이 이용합니다.
  • 리빌드 필요 시, ref.watch(userViewModelProvider).receiveEmail; -> ex) 변수값 사용
  • 리빌드 불필요 시, ref.read(userViewModelProvider).setReceivePush(value); -> ex) 메소드 사용
    위와 같이 리빌드가 필요할 경우 watch, 리빌드가 필요하지않을 경우 read를 사용합니다.


main.dart 에 NotifierProvider 추가(필수)

main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final preferences = await SharedPreferences.getInstance();
  final repository = UserRepository(preferences);

  runApp(ProviderScope(
    overrides: [
      userViewModelProvider.overrideWith(() => UserViewModel(repository))
    ],
    child: const MyApp(),
  ));
}​
  • main() 함수에 위와 같이 ProviderScope를 추가하고 overrides: [] 에 Provider를 추가합니다.
  • child: 속성에 최상위 위젯을 넣어줍니다.

 


MVVM  + Repository 형태에 Riverpod, SharedPreferences 를 적용하여 상태관리하는  모습

  • receiveEmail , receivePush 로 앱을 종료하고 실행 시켜도 상태관리가 되는 모습을 볼 수 있습니다.

필기.

반응형
Comments