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

もくじ

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

ついこの間、社内でFlutterを勉強している方から「機能単位でWidgetクラスを作ったが、親クラスとの連携や制御方法が分からない」と質問を貰いました。

確かに、Flutterの標準クラスの勉強をして、一通りレイアウトの作り方を覚えたら、次のステップとして、この当たりが壁になるのかなと感じました。現に自分も困った経験があったので、これもいい機会かと思い記事にしてみた次第です。

今回は簡単なアプリを例にとって、機能単位のWidgetクラスと親Widgetクラスの制御について説明してみたいと思います。

アプリの仕様として

ざっくりですが、

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

上記の様なイメージで作っていきます。

子Widgetの実装

まず「季節」がスライドで選択できる子WidgetのCategorySlideWidgetと、「日中、夜」が選択できる子Widgetの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;
  }
}

コールバック

親Widgetが子Widgetを制御する際に要になるのがコールバック機能になります。子Widgetの状態の変化を親Widgetに伝えるというものになります。これにより、親Widgetで状態を更新してあげることが可能になります。

こんな感じです。

親Widgetの実装

次に親WidgetのMainViewWidgetを作ります。

MainViewWidget.dart

import 'package:flutter/material.dart';
import 'package:test_project/CategorySlideWidget.dart';
import 'package:test_project/SegmentWidget.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> {

  // カテゴリー
  CategorySeasons _categorySeasons = CategorySeasons.spring;
  // セグメント
  Segment _segment = Segment.day;

  @override
  Widget build(BuildContext context) {

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

            /*
             * カテゴリーウィジェット
             */
            CategorySlideWidget(
              (CategorySeasons season){

                // ステート更新
                setState(() {
                  _categorySeasons = season;
                });
              },
            ),

            /*
             * セグメントウィジェット
             */
            SegmentWidget(
              _segment,
              (Segment segment){

                // ステート更新
                setState(() {
                  _segment = segment;
                });
              }
            ),

            /*
             * コンテンツと想定
             */
            Expanded(
              child: 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を持っており、各クラスでアクションがあり、コールバックがあると親のstateを変化し、_categorySeasonsと_segmentの値により表示する画像の切り替えを行うという流れです。

静止画で申し訳ないですが、きちんと動きました。

まとめ

「機能Widgetクラスと親Widgetクラスの制御」というと難しいイメージが湧くかもしれませんが、単純なことです。親のWidgetクラスがButtonWidgetを持っていて、ボタンタップすると、onPressed()のコールバックが実行されるので、そこに実装内容を記載するという、今までもやっている内容なのです。それはカスタムのWidgetでも同様なのです。

次回は、今回の続編で「開発時のクラス構成について(機能Widgetクラスと親Widgetクラスと制御クラス)」をご紹介します。

関連する記事

  • 映画公開予定通知botを作ってみた!

  • 【Flutter】通信ライブラリRetrofit(Dio)がいい感じ

  • 【Flutter】Riverpod+StateNotifier+freezedで作ろう

  • ChatGPTくんと仲良くなって手羽元の甘辛煮を作るまで

もっと見る