こんにちは。スマホアプリをメインに開発しているロッキーカナイです。
ついこの間、社内でFlutterを勉強している方から「機能単位でWidgetクラスを作ったが、親クラスとの連携や制御方法が分からない」と質問を貰いました。
確かに、Flutterの標準クラスの勉強をして、一通りレイアウトの作り方を覚えたら、次のステップとして、この当たりが壁になるのかなと感じました。現に自分も困った経験があったので、これもいい機会かと思い記事にしてみた次第です。
今回は簡単なアプリを例にとって、機能単位のWidgetクラスと親Widgetクラスの制御について説明してみたいと思います。
アプリの仕様として
ざっくりですが、
- 「季節」のスライド項目と「日中、夜」のセグメンとを切り替えると、該当の画像が表示されるアプリ
- 親クラス(メイン)はMainViewWidgetで以下の子クラスを保持している
- 子クラスはCategorySlideWidgetで季節カテゴリをスライドで表示し選択できる
- もう一つの子クラスはSegmentWidgetで、セグメント切り替えで「日中、夜」を選択できる

上記の様なイメージで作っていきます。
子Widgetの実装
まず「季節」がスライドで選択できる子WidgetのCategorySlideWidgetと、「日中、夜」が選択できる子WidgetのSegmentWidgetを作ってみます。
CategorySlideWidget.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
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クラスと制御クラス)」をご紹介します。
