3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter デザインパターン (MVC / MVVM)

Last updated at Posted at 2024-07-31

デザインパターンとは

デザインパターン(設計パターン)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウに名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものです。

デザインパターンを利用するメリットは、再利用性の高い柔軟な設計ができるという点です。設計者の直感や経験などに依存していた設計が、デザインパターンを導入することで、初心者でも先人たちが詰め込んだ「知恵」を利用して設計をすることが可能になります。

もう一つのメリットとして、技術者どうしの意思疎通が容易になることが挙げられます。デザインパターンを習得している技術者どうしであれば、パターン名で設計の概要の合意を取ることが可能になります。このように、デザインパターンを学習しておくことで開発者どうしの意思疎通がスムーズになるのです。

以下は、Flutterにおける主要なデザインパターンです。

  • Model-View-Controller (MVC)
  • Model-View-ViewModel (MVVM)
  • Provider(プロバイダー) パターン
  • Bloc(ブロック) パターン
  • Singleton(シングルトン) パターン
  • Factory(ファクトリー) パターン
  • Builder(ビルダー) パターン
  • Composite(コンポジット) パターン

これらのパターンを駆使することで、開発者はアプリケーションの品質を向上させ、開発をスムーズに進めることができ、プロジェクトの規模やメンバーのスキルに応じた柔軟な設計が行えるようになります。

今回は MVCMVVM に対象を絞って解説をしていきます。

Model-View-Controller

1. 定義

MVCは、ソフトウェア開発におけるデザインパターンの一つで、アプリケーションをモデル(Model)、ビュー(View)、コントローラ(Controller) の3つのコンポーネントに分割したものです。各コンポーネントは異なる責務を持ち、分離された形でアプリケーションを構築します。

2. コンポーネント

  • Model (モデル): データやビジネスロジックを管理し、それらの変更を通知する役割を担当します。ViewやControllerとは直接的な関係を持ちません。

  • View (ビュー): ユーザーに表示される情報やインターフェースを管理します。ユーザーからの入力はControllerに伝達されます。

  • Controller (コントローラ): ユーザーからの入力を処理し、それに基づいてModelやViewを変更します。ユーザーからの入力やコマンドをModelに変換します。

簡単に言うならば、Viewはディスプレイに表示される見た目部分を担当して、Controllerはゲームをする際、ユーザが手に持って扱うコントローラ部分を担当します。それに対してModelはViewとController以外の全ての部分を担当するのです。

UI(ユーザーインターフェース)
View と Controller をあわせて UI(ユーザーインターフェース)と呼びます。その名の通り、ユーザーとアプリケーションの両者をつなぐものです。

3. メリット

  • 分離性: 各コンポーネントが独立しているため、変更が他のコンポーネントに影響を与えにくく、保守性が向上します。

  • 再利用性: 同じModelやView、Controllerを異なるコンテキストで再利用することができます。

  • 保守性: 各コンポーネントが特定の責務を持つため、コードの理解と保守が容易です。

4. サンプルコード

MVCサンプルコード
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int get count => _count;
  int _count = 0;

  void increment() {
    _count++;
    notifyListeners();
  }

  void clear() {
    _count = 0;
    notifyListeners();
  }
}

class CounterController {
  const CounterController(CounterModel counter) : _counter = counter;

  final CounterModel _counter;

  void countUp() => _counter.increment();

  void clear() => _counter.clear();
}

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

  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    final controller = context.read<CounterController>();
    return Scaffold(
      appBar: AppBar(
        title: const Text('MVC Count App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Count',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
            Text(
              counter.count.toString(),
              style: Theme.of(context).textTheme.headlineLarge,
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => controller.countUp(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 16),
          FloatingActionButton(
            onPressed: () => controller.clear(),
            child: const Icon(Icons.clear),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<CounterModel>(create: (_) => CounterModel()),
        ProxyProvider<CounterModel, CounterController>(
          update: (_, counter, __) => CounterController(counter),
        ),
      ],
      child: const MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}
  • CounterModel クラス (Model):

    • CounterModel クラスは、アプリケーションの状態を保持しています。 ChangeNotifier クラスを継承しており、 notifyListeners() メソッドを呼ぶことで変更通知を行います。
    • count プロパティはカウンターの現在の値を取得します。
    • increment() メソッドはカウンターを1つ増やし、 clear() メソッドはカウンターを0にリセットします。
  • CounterController クラス (Controller):

    • CounterController クラスでは、 CounterModel クラスとやり取りするためのメソッドを定義します。
    • コンストラクタで CounterModel インスタンスを受け取ります。
    • countUp() メソッドは CounterModel クラスの increment() メソッドを呼び出し、clear() メソッドは CounterModel クラスの clear() メソッドを呼び出します。
  • CounterPage クラス (View):

    • CounterPage クラスは、実際に画面に表示されるウィジェットを構築しています。
    • context.watch<CounterModel>() を使用して CounterModel クラスの変更を監視し、 context.read<CounterController>() を使用して、 CounterController クラスへのアクセスを取得しています。
    • フローティングボタンを使用してカウントを増やすボタン、およびカウントをリセットするボタンのUIを構築しています。

Model-View-ViewModel

1. 定義

MVVM(Model-View-ViewModel)は、ソフトウェア開発におけるデザインパターンの一つで、主にUI関連のコードを整理し、テスト可能な構造にするためのデザインパターンです。MVVMは主にModel (モデル)、ViewModel (ビューモデル)、View (ビュー)の3つのコンポーネントから構成されます。

2. 主なコンポーネント

  • Model: アプリケーションのデータ構造やビジネスロジックを定義します。変更通知の仕組みを使用して、ViewModelに対して変更を通知します。データの変更通知や更新処理を提供することが一般的です。

  • View: ユーザーインターフェース(UI)を表現します。ユーザーの入力をViewModelに伝達し、ViewModelからデータ変更通知を受け取ります。

  • ViewModel: ユーザーインターフェースの状態や表示ロジックを管理します。ユーザーからの入力を処理し、Modelからデータを取得してUIに表示します。ModelとViewの間で情報のやり取りを仲介し、UIロジックの一元管理を行います。

3. メリット

  • 切り離しとテスト容易性: Model、View、ViewModelが分離されているため、各コンポーネントを個別にテストしやすくなります。

  • 再利用性: ViewModelの再利用が容易であり、同じViewModelを異なるViewで使用することができます。

  • データ バインディング: ViewModelの変更が自動的にViewに反映されるデータバインディングの仕組みがあるため、手動でUIを更新する必要が減ります。

  • UI ロジックの独立性: ViewModelがUIロジックを管理するため、ViewはUIの見た目だけを担当します。これによって、UIロジックの変更がUIコンポーネント(見た目)を変更せずに可能となります。

  • 複数人での開発 MVVM は View と Model が完全に分離したアーキテクチャです。View と Model の間に依存関係はないので、 View の作成、Model の作成を複数人で担当し、並行して行うことが容易になります。

MVC と MVVM の大きな違いはView に反映するデータを保持する場所にあります。MVC では Controller が担当するのは操作のみで、データの保存は行いません、そのため Model の状態を View が確認し反映するアーキテクチャとなっています。

一方で、 MVVM では ViewModel が Model の状態を監視し、View が使いやすい状態に加工して保持します。これによって、View と Model が完全に分離したアーキテクチャとなります。

4. サンプルコード

MVVMサンプルコード
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel {
  int count = 0;

  void increment() {
    count++;
  }

  void decrement() {
    count--;
  }
}

class CounterViewModel extends ChangeNotifier {
  final CounterModel _counterModel = CounterModel();

  int get count => _counterModel.count;

  void increment() {
    _counterModel.increment();
    notifyListeners();
  }

  void decrement() {
    _counterModel.decrement();
    notifyListeners();
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider(
        create: (context) => CounterViewModel(),
        child: CounterView(),
      ),
    );
  }
}

class CounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterViewModel = Provider.of<CounterViewModel>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Count',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
            Text(
              '${counterViewModel.count}',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: counterViewModel.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
          SizedBox(width: 10),
          FloatingActionButton(
            onPressed: counterViewModel.decrement,
            tooltip: 'Decrement',
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
  • CounterModel クラス (Model):

    • CounterModel クラスはカウンターのデータとロジックを含むモデルです。 count プロパティでカウント値を保持し、incrementdecrement メソッドでカウントを増減させます。
  • CounterViewModel クラス (ViewModel):

    • CounterViewModel クラスは ChangeNotifier を継承しており、モデルとビューをつなぐ役割を果たします。
    • _counterModel インスタンスを内部に持ち、カウント値を取得するための count ゲッターを提供します。
    • incrementdecrement メソッドは、モデルのメソッドを呼び出してカウントを変更し、 notifyListeners メソッドを呼び出してリスナー(ビュー)に変更を通知します。
  • CounterPage クラス (View):

    • ChangeNotifierProvider を使って CounterViewModel を提供し、 CounterView ウィジェットを子として渡します。これにより、 CounterView 内で CounterViewModel が利用できるようになります。

まとめ

MVC は、Model、View、Controllerを明確に分離することで、アプリケーションの構造を整理し、保守性を高めることができます。一方で、MVVM は、ViewModelがUIロジックを管理することで、UIの変更に強く、テストしやすい設計を実現します。

どちらのパターンを選ぶべきかは、プロジェクトの規模、開発チームのスキル、アプリケーションの特性によって異なってくるため、状況に応じたデザインパターンを採用することが必要です。

MVC は、比較的シンプルなアプリケーションや、既存のMVCベースのフレームワークとの連携を考慮する場合に適しており、MVVM は、大規模なアプリケーションや、UIが複雑で頻繁に変わる可能性がある場合に適しています。

Flutter では、ProviderRiverpod などの状態管理ライブラリと組み合わせることで、MVVMパターンをより効果的に実装することができます。

MVCとMVVMの比較

特徴 MVC MVVM
分離 Model、View、Controller Model、View、ViewModel
状態管理 コントローラが状態を管理 ビューモデルが状態を管理
データバインディング 手動 自動
UIロジック Controllerに含まれる ViewModelに含まれる
テスト容易性 比較的高い 高い
再利用性 比較的高い 高い

参考資料

告知

最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。

みなさまからのご応募をお待ちしております。

3
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?