【Flutter】開発時のクラス構成について(機能Widgetクラスと親Widgetクラスと制御クラス)

もくじ

こんにちは。スマホアプリをメインに開発しているロッキーカナイです。

前回は、社内でFlutterを勉強している方から「機能単位でWidgetクラスを作ったが、親クラスとの連携や制御方法が分からない」と質問を貰ったので、説明の記事を書きました。

【Flutter】開発時のクラス構成について(機能単位のWidgetクラスと親Widgetクラスの制御)

実は、上記の質問の回答としては、今回紹介する、機能Widgetを制御する制御クラスを使ってそこで制御をしてみようと回答しました。今回はその内容を紹介いたします。

前回と同じアプリを使いますので、仕様のおさらいです。

アプリ仕様

  1. 「季節」のスライド項目と「日中、夜」のセグメンとを切り替えると、該当の画像が表示されるアプリ

旧アプリのクラス構成

  1. 親クラス(メイン)はMainViewWidgetで以下の子クラスを保持している。
  2. 子クラスはCategorySlideWidgetで季節カテゴリをスライドで表示し選択できる。
  3. もう一つの子クラスはSegmentWidgetで、セグメント切り替えで「日中、夜」を選択できる。

新アプリのクラス構成

  1. 親クラス(メイン)はMainViewWidgetで以下の制御クラスを保持しており、制御クラスからの指示で画面の更新を行う。
  2. 制御クラスはMainManagerで以下のクラスを制御する。
  3. CategorySlideWidgetで季節カテゴリをスライドで表示し選択できる。
  4. SegmentWidgetで、セグメント切り替えで「日中、夜」を選択できる。

制御クラス

制御クラスとは??となってしまうかと思いますが、前回に出てきた親クラスMainViewWidgetの機能面を制御するクラスを新規で作ってそこで色々やってもらう。そして親Widgetは管理クラスで保持しているものを表示するだけといった内容です。

よくMVCといわれるデザインパターンを思い浮かべてもらうと分かりやすいのですが、今回の親WidgetのMainViewWidgetはMVCでいうところのViewになり、管理クラスのMainManagerはMVCでいうところのController(とModel)です。

CategorySlideWidgetとSegmentWidgetの実装

前回とソースコードは同じですが、記載します。

CategorySlideWidget.dart

import 'package:flutter/material.dart';

/*
 * カテゴリー
 */
enum CategorySeasons {
  spring,
  summer,
  autumn,
  winter,
}

/*
 * カテゴリーコールバック
 */
typedef CategoryCallback = void Function(CategorySeasons season);

/*
 * カテゴリウィジェット
 */
class CategorySlideWidget extends StatelessWidget {

  final List<CategorySeasons> _categoryList = [
    CategorySeasons.spring,
    CategorySeasons.summer,
    CategorySeasons.autumn,
    CategorySeasons.winter,
  ];

  // カテゴリー選択コールバック
  final CategoryCallback callback;

  CategorySlideWidget(this.callback) : super();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      height: 100.0,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemBuilder: (BuildContext context, int index) {

          return Container(
            width: 120.0,
            child: InkWell(
              onTap: () {
                if (callback != null) {
                  callback(_categoryList[index]);
                }
                print("on tap -> ${_categoryList[index].toString().split('.')[1]}");
              },
              child: Card(
                child: Center(
                  child: Text(
                    _categoryList[index].toString().split('.')[1],
                  ),
                ),
              ),
            ),
          );
        },
        itemCount: _categoryList.length,
      ),
    );
  }
}

SegmentWidget.dart

import 'package:flutter/material.dart';

/*
 * セグメント
 */
enum Segment {
  day,
  night,
}

/*
 * セグメントコールバック
 */
typedef SegmentCallback = void Function(Segment segment);

/*
 * セグメントウィジェット
 */
class SegmentWidget extends StatelessWidget {

  // セグメント選択コールバック
  final SegmentCallback callback;

  // セグメント
  Segment segment = Segment.day;

  SegmentWidget(this.segment, this.callback) : super();


  @override
  Widget build(BuildContext context) {

    return Container(
      height: 80.0,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[

          FlatButton(
            onPressed: (){
              _updateButtonState(Segment.day);
            },
            child: Text("day"),
            color: _getButtonColor(Segment.day),
          ),

          FlatButton(
            onPressed: (){
              _updateButtonState(Segment.night);
            },
            child: Text("night"),
            color: _getButtonColor(Segment.night),
          ),
        ],
      ),
    );
  }

  /*
   * ボタンの状態を更新
   */
  void _updateButtonState(Segment seg) {

    if (segment == seg) {
      return;
    }

    if (callback != null) {
      callback(seg);
    }
  }

  /*
   * 状態に応じたボタン色を返す
   */
  Color _getButtonColor(Segment seg) {

    if (segment == seg) {
      return Colors.red;
    }
    return Colors.grey;
  }
}

制御クラスの実装

MainManager.dart

import 'package:flutter/material.dart';
import 'package:test_project/CategorySlideWidget.dart';
import 'package:test_project/SegmentWidget.dart';

class MainManager {

  // 画面更新の際に呼ぶコールバック
  final VoidCallback updateStateCallback;
  // カテゴリ
  CategorySeasons _categorySeasons = CategorySeasons.spring;
  // セグメント
  Segment _segment = Segment.day;

  MainManager({
    @required
    this.updateStateCallback
  }) : super() {
    assert(updateStateCallback != null);
  }

  /*
   * カテゴリーウィジェット
   */
  Widget getCategoryWidget() {
    return CategorySlideWidget(
          (CategorySeasons season){
        _categorySeasons = season;
        updateStateCallback();
      },
    );
  }

  /*
   * セグメントウィジェット
   */
  Widget getSegmentWidget() {
    return SegmentWidget(
      _segment,
          (Segment segment){
        _segment = segment;
        updateStateCallback();
      },
    );
  }

  /*
   * コンテンツウィジェット
   */
  Widget getContentWidget() {
    return Container(
      color: Colors.blue,
      child: Image.asset(_getContentsImagePath()),
    );
  }

  /*
   * コンテンツに表示する画像のパス文字列を返す
   */
  String _getContentsImagePath() {

    // TODO : このあたりは仮なので適当に実装しました。
    bool isDay = _segment == Segment.day;
    switch(_categorySeasons) {
      case CategorySeasons.spring:
        return isDay ? "images/spring-day.jpg" : "images/spring-night.jpg";

      case CategorySeasons.summer:
        return isDay ? "images/summer-day.jpg" : "images/summer-night.jpg";

      case CategorySeasons.autumn:
        return isDay ? "images/autumn-day.jpg" : "images/autumn-night.jpg";

      case CategorySeasons.winter:
        return isDay ? "images/winter-day.jpg" : "images/winter-night.jpg";
    }
  }
}

前回では、MainViewWidgetで行なっていたCategorySlideWidgetやSegmentWidgetの制御はMainManagerへ移動してます。

これで仕様面の機能はこのMainManagerで行うことが出来ました。

親クラスの実装

MainViewWidget.dart

import 'package:flutter/material.dart';
import 'package:test_project/MainManager.dart';


void main() => runApp(Main());

class Main extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainViewWidget(),
    );
  }
}


class MainViewWidget extends StatefulWidget {

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


class _MainViewWidgetState extends State<MainViewWidget> {

  MainManager _manager;

  @override
  void initState() {
    super.initState();

    // マネージャの初期化
    _manager = MainManager(updateStateCallback: () {

      // ステート更新
      setState(() {
      });
    });
  }


  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("App Title"),
      ),
      body: Container(
        margin: EdgeInsets.all(5.0),
        child: Column(
          children: <Widget>[

            /*
             * カテゴリーウィジェット
             */
            _manager.getCategoryWidget(),

            /*
             * セグメントウィジェット
             */
            _manager.getSegmentWidget(),

            /*
             * コンテンツと想定
             */
            Expanded(
              child: _manager.getContentWidget(),
            )
          ],
        ),
      ),
    );
  }
}

MainViewWidgetでは、MainManagerからWidgetを返してもらって表示するだけになりました。Viewとして画面の表示のみ担当しているイメージです。

動作も前回と同様の動きになりました。

まとめ

この記事を書くきっかけとなった質問の回答として、「制御クラスを作ってそこで制御しよう」と言ったのは以下の理由からです。

「親Widget」 – 「制御クラス」 – 「各機能パーツ」

といった、それぞれの担当にわりふった仕事を実装することで、コードが見やすくなり、不具合が減る。よって、手直しが少なくなるので工数が削減できる、クオリティの高いものができる、などメリットが多いことからです。

よくiOS界隈で処理がViewControllerに集中しすぎていて肥大化すると言った話が出ますが、Flutterも同様で、前回の様に親Widgetに処理を持たせすぎると肥大化します。なので制御クラスで処理を一括してあげる(又は、制御を細分化して別クラス、複数管理クラスにする)などで回避できますし、コードがすっきりします。

Flutterだけでなく、すべてのコーディングに通ずるものと私は思ってます。

次回はなにを書こうかな。

関連する記事

もっと見る