Flutter

MVVM 패턴과 상태 관리

whs5758 2025. 8. 20. 17:46

Monolithic (모노리스) 구조의 특징

하나의 코드 파일에 UI, 비즈니스 로직, 프레젠테이션 로직을 모두 넣는 형식을 흔히 Monolithic Architecture 또는 간단히 Monolith라고 부릅니다.

 

먼저 MVVM 패턴 없이 간단한 구조로 코드를 작성해보자. 모든 로직과 상태 관리를 하나의 파일에 통합하여, UI와 데이터 처리가 한 클래스에서 이루어지는 방식으로 코드를 작성할 수 있다. 이 방식은 MVVM과 같은 디자인 패턴이 없어도 간단한 앱에서는 빠르게 개발할 수 있는 장점이 있다.

MVVM에 대한 기본 개념 잡기

MVVM은 세 가지 주요 구성 요소

 

MVVM은 소프트웨어를 Model / View / ViewModel로 관심사 분리하여 View를 쉽게 변경할 수 있도록 만들어줍니다.

  • View : UI 담당
  • ViewModel : View 상태 및 로직 담당
  • Model : 비즈니스 로직 & 데이터 입출력 담당

 

모델 클래스 정의
// 할 일 모델 클래스 정의

class Todo {
  final String id;
  final String title;

  Todo({required this.id, required this.title});
}

 

뷰 모델 클래스 설계
import 'package:class_todo_app/todo.dart';

// ViewModel 클래스
// 뷰의 상태와 UI 관련 로직을 관리
class TodoViewModel {
  // 화면과 관련된 데이터를 관리
  List<Todo> todoList = [];

  // 할 일 추가 메서드
  void addTodo(String index, String title) {
    final newTodo = Todo(
      id: index,
      title: title,
    );
    todoList.add(newTodo);
  }

  // 할 일 삭제 메서드
  void removeTodo(int index) {
    todoList.removeAt(index);
  }
}

 

UI 클래스 설계

import 'package:class_todo_app/todo_view_model.dart';
import 'package:flutter/material.dart';

/**
 * 모노리스 구조로 코드를 작성해 보자.
 */

void main() => runApp(const TodoApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: TodoScreen(),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  // UI 로직 : 사용자가 입력하는 텍스트를 관리하는 컨트롤러
  final TextEditingController _controller = TextEditingController();
  final todoViewModel = TodoViewModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurpleAccent[100],
        title: Text(
          'Todo App',
          style: TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              children: [
                // TextField, icons
                SizedBox(
                  width: 320,
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      labelText: '작업을 입력하세요',
                    ),
                  ),
                ),
                IconButton(
                    onPressed: () {
                      // 기존에 만들었던 index + 1
                      String lastIndex =
                          (todoViewModel.todoList.length + 1).toString();
                      todoViewModel.addTodo(lastIndex, _controller.text);
                      _controller.clear();
                      setState(() {});
                    },
                    icon: Icon(Icons.add))
              ],
            ),
            Expanded(
              child: ListView.builder(
                itemCount: todoViewModel.todoList.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(todoViewModel.todoList[index].title),
                    trailing: IconButton(
                        onPressed: () {
                          todoViewModel.removeTodo(index);
                          setState(() {});
                        },
                        icon: Icon(
                          Icons.delete_forever,
                        )),
                  );
                },
              ),
            )
          ],
        ),
      ),
    );
  }
}

 


count.dart
class Count {
  int number;

  Count({required this.number});
}

 

count_view_model.dart
import 'package:class_todo_app/_test/count.dart';

class CountViewModel {
  Count count = Count(number: 0);

  void addCount() {
    count.number++;
  }

  void removeCount() {
    if (count.number > 0) {
      count.number--;
    }
  }
}

 

main.dart
import 'dart:math';

import 'package:class_todo_app/_test/count_view_model.dart';
import 'package:flutter/material.dart';

void main() => runApp(const CountApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: CountScreen(),
    );
  }
}

class CountScreen extends StatefulWidget {
  const CountScreen({super.key});

  @override
  State<CountScreen> createState() => _CountScreenState();
}

class _CountScreenState extends State<CountScreen> {
  final TextEditingController _controller = TextEditingController();
  final countViewModel = CountViewModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('간단한 뷰와 모델 예제'),
            Text('${countViewModel.count.number}'),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.blue[200],
                      foregroundColor: Colors.white),
                  onPressed: () {
                    setState(() {
                      countViewModel.addCount();
                    });
                  },
                  child: Text('++'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.blue[200],
                      foregroundColor: Colors.white),
                  onPressed: () {
                    setState(() {
                      countViewModel.removeCount();
                    });
                  },
                  child: Text('--'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

'Flutter' 카테고리의 다른 글

블로그 프로젝트 - 모델 클래스 설계  (0) 2025.08.22
블로그 프로젝트 - 디자인 시안, 구조, 주소 수정  (0) 2025.08.20
구글 맵 3단계 마커 활용  (0) 2025.08.20
SharedPreferences  (0) 2025.08.20
구글 Map API 사용  (0) 2025.08.20