flutter mvvm

topics 500-모바일개발 501 Flutter
types 이론 실습
tags
references ctoahn.tistory.com/15 blog.naver.com/islove8587/220602340150

Flutter MVVM

Flutter에서 MVVM 아키텍처 패턴을 적용하는 방법이다.

MVVM이란

  • Model: 데이터와 비즈니스 로직
  • View: UI 레이어 (Widget)
  • ViewModel: View와 Model 사이의 중재자

왜 MVVM을 쓸까: UI 로직과 비즈니스 로직을 분리해서 테스트와 유지보수가 쉬워진다. View는 ViewModel만 알면 되고, ViewModel은 Model만 알면 된다.

폴더 구조

lib/
├── models/
│   └── user.dart
├── repositories/
│   └── user_repository.dart
├── viewmodels/
│   └── user_viewmodel.dart
└── views/
    └── user_screen.dart

구현 예시 (Riverpod)

Model

// models/user.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
  }) = _User;

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

Repository

// repositories/user_repository.dart
class UserRepository {
  final ApiClient _api;

  UserRepository(this._api);

  Future<User> getUser(String id) async {
    final response = await _api.get('/users/$id');
    return User.fromJson(response);
  }

  Future<void> updateUser(User user) async {
    await _api.put('/users/${user.id}', user.toJson());
  }
}

// Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepository(ref.watch(apiClientProvider));
});

ViewModel (Notifier)

// viewmodels/user_viewmodel.dart
final userViewModelProvider = AsyncNotifierProvider<UserViewModel, User?>(() {
  return UserViewModel();
});

class UserViewModel extends AsyncNotifier<User?> {
  @override
  Future<User?> build() async {
    return null;  // 초기값
  }

  Future<void> loadUser(String id) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final repo = ref.read(userRepositoryProvider);
      return repo.getUser(id);
    });
  }

  Future<void> updateName(String name) async {
    final user = state.value;
    if (user == null) return;

    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final updated = user.copyWith(name: name);
      await ref.read(userRepositoryProvider).updateUser(updated);
      return updated;
    });
  }
}

View

// views/user_screen.dart
class UserScreen extends ConsumerWidget {
  final String userId;

  const UserScreen({required this.userId, super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userViewModelProvider);

    // 초기 로드
    ref.listen(userViewModelProvider, (_, __) {});
    useEffect(() {
      ref.read(userViewModelProvider.notifier).loadUser(userId);
      return null;
    }, [userId]);

    return Scaffold(
      body: userState.when(
        loading: () => const CircularProgressIndicator(),
        error: (e, _) => Text('Error: $e'),
        data: (user) => user == null
            ? const Text('No user')
            : UserContent(user: user),
      ),
    );
  }
}

class UserContent extends ConsumerWidget {
  final User user;

  const UserContent({required this.user, super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        Text(user.name),
        ElevatedButton(
          onPressed: () {
            ref.read(userViewModelProvider.notifier).updateName('New Name');
          },
          child: const Text('Update Name'),
        ),
      ],
    );
  }
}

데이터 흐름

┌─────────┐       ┌─────────────┐       ┌────────────┐       ┌─────────┐
│  View   │ ───►  │  ViewModel  │ ───►  │ Repository │ ───►  │   API   │
│(Widget) │ ◄───  │  (Notifier) │ ◄───  │            │ ◄───  │         │
└─────────┘       └─────────────┘       └────────────┘       └─────────┘
    watch           state 변경            데이터 요청         네트워크

MVVM vs MVC vs MVP

패턴 View-Logic 연결 테스트 용이성
MVC Controller가 직접 조작 어려움
MVP Presenter가 View 참조 보통
MVVM 데이터 바인딩 쉬움

Flutter에서 MVVM이 좋은 이유: 선언적 UI라서 데이터 바인딩 개념과 잘 맞는다. ViewModel의 상태가 바뀌면 Widget이 자동으로 리빌드된다.

관련 문서