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