0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[RiverPod]ロジックとUIをちゃんと分けよう

Posted at

初心者の頃は何もわからず、AIにコードを書いてもらうと一つのファイルに全部の機能を書いて出してよしとしている人が多くいると感じます。
改めてRiverPodの講習を行ったので書いていこうと思います。

講座の資料⬇️
https://www.notion.so/RiverPod-26a430c9c40680e7846fedc5c13c83a1?source=copy_link

目次

1.RiverPodとは

2.ロジックとUIを分ける大切さ

3.まとめ

1.RiverPodとは

まずRiverPodについて軽く解説していこうかなと思います。
RiverPodとはFlutterの公式推奨の状態管理ツールのことです。どうして使っているかというと状態のロジックをwidgetから切り離すためだったり、アプリの設計を管理するような役割を持っています。

状態とは 

状態とは、アプリケーションが保持するデータのことです。

RiverPodを使っていない状態管理として
よくあるsetStateの例を見ていこう

// setState の問題
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;
  
  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Count: $_counter'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('+1'),
        ),
      ],
    );
  }
}

これの問題点として

  1. 状態がWidgetの中に閉じ込められている
    _counterという状態は、クラスの内部に入っていてこの値を他のところで表示させようとしても、この値に直接アクセスすることができないです
  2. UIとロジックが混合している
    UIを構築するbuildメソッドと状態を更新する_incrementメソッドが同じクラス内に存在している。このコードぐらいだとわかるが、色々なロジックが加わるとコードの可読性がかなり下がり、テスト等もしづらくなる。

2.ロジックとUIを分ける大切さ

この記事ではNotifierにロジックを書いて、UIに表示するやり方を書いていこうかと思います。(Todoリスト)
今回使うディレクトリー構造として

lib/
├── main.dart
└── todo/
    ├── model/
    │   └── todo.dart
    ├── todo.dart
    ├── todo_notifier.dart
    └── todo_provider.dart

todo_notifier.dartとtodo_provider.dartは合わせても大丈夫です。

1. まずTodoのモデルを作成します

ここではタスクのクラスを定義します。
ファイルは model/todo.dart

class Todo {
  final String id; //TodoのID
  final String title; //Todoのタイトル
  final bool isCompleted; //完了状態
  
  Todo({          //コンストラクタ
    required this.id, //インスタンス作成時にrequiredが必要
    required this.title,
    required this.isCompleted,
  });

  Todo copyWith({
    String? title,
    bool? isCompleted,
  }) {
    return Todo(
      id: id,             //IDは常に同じ
      title: title ?? this.title, //新しい値か既存の値
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

2.次にNotifierにロジックを書いていきます

ファイルは todo_notifier.dart

class TodoNotifier extends Notifier<List<Todo>> {.  //<List,Todo>>型の状態を管理
  @override
  List<Todo> build() {. //アプリ起動時やProviderが初めて読み込まれた時に呼ばれる
    return []; // 初期状態は空のリスト
  }

  void add(String title) { 
    state = [
      ...state,  //スプレッド演算子
      Todo(
        id: DateTime.now().toString(),  //現在時刻をIDに使用
        title: title,
        isCompleted: false,
      ),
    ];
  }

  void toggle(String id) {. //完了状態の切り替え
    state = state
        .map((todo) => todo.id == id
            ? todo.copyWith(isCompleted: !todo.isCompleted)//対象のtodoは反転
            : todo) //それ以外はそのまま
        .toList();
  }

  void remove(String id) { //Todo削除
    state = state.where((todo) => todo.id != id).toList();
  }
}

3. Providerを定義

ファイルは todo_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

//グローバル変数として定義
final todoProvider = NotifierProvider<TodoNotifier, List<Todo>>( //Notifierクラスの型、管理する状態の型
  () => TodoNotifier(),
);

todo_notifier.dartにこれを書く人もいれば分ける人もいる
これでロジック側の部分を書きました。後はUI側のコードを書けばいいです。

4.UI側

ファイルは todo.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo/todo_provider.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoProvider); //現在のtodoリストを取得
    final notifier = ref.read(todoProvider.notifier); //操作を実行するためのNotifierを取得

    return Scaffold(
      appBar: AppBar(title: const Text("Todo アプリ")),
      body: Column(
        children: [
         // 入力フォーム
          Padding(
            padding: const EdgeInsets.all(8.0), //上下左右に8ピクセルの余白を追加
            child: TextField(
              decoration: const InputDecoration(hintText: "やることを入力"),
              onSubmitted: (text) { //Enterキーを押した時の処理
                if (text.isNotEmpty) { //空文字じゃないかチェック
                  notifier.add(text); //Todoを追加
                }
              },
            ),
          ),

          // Todo リスト
          Expanded( //余ったところを全部使うようにする
            child: ListView.builder(
              itemCount: todos.length, //todoの数を指定
              itemBuilder: (context, index) {
                final todo = todos[index]; //index番目のtodoを取得
                return ListTile(
                  leading: Checkbox( //左側
                    value: todo.isCompleted,
                    onChanged: (_) => notifier.toggle(todo.id),
                  ),
                  title: Text(todo.title), //中央
                  trailing: IconButton( //右側
                    icon: const Icon(Icons.delete),
                    onPressed: () => notifier.remove(todo.id),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

このUI側でimportしているのはtodo_providerで実際のロジックの部分は直接は使わないようになっています。
図で分かりやすくしてみました。
スクリーンショット 2025-10-03 22.50.38.png

以上のやり方でロジックとUIをしっかり分けることができました。
細かいコードの解説は講座資料に書いています。

3.まとめ

Flutter開発においてUIとビジネスロジックを分離することは、アプリの品質を長期的に維持するために必要です。

  • 関心の分離: UIは表示、ロジックは状態管理にそれぞれ専念させる。

  • テスト容易性: ロジックをUIから切り離すことで、単体テストが容易になる。

  • 再利用性: 独立したロジックは、様々なUIで再利用できる。

  • Riverpodの活用: Providerでロジックをカプセル化し、UIはrefを介してやり取りする。

今回はRiverPodを使ってロジックとUIを分けましたが、他にもClean Architectureを使ったusecaseにロジックを書くなど、Architectureによってロジックを書く場所が異なるので試しながらプロダクトに合うやり方を見つけていきましょう!

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?