Stateful widget
Stateless widgetsは変わらないもの、全ての値がfinalとして扱われる。そのため、変数の変更もできない。
対して、Stateful widgetsはwidgetのライフサイクルで変更していく。
Stateful widgetsを実装する際、以下に条件が必須。
項目名 | 役割 |
Text | 指定したスタイルのテキストを表示する |
Row / Column | Rowは縦方向、Columnは横方向に並べるようにレイアウトを配置する。 Webでいうflexboxレイアウトモデルに該当するもの。 |
Stack | 上下左右の相対的位置を指定して表示することができる。 Webでいうabsolute positioningレイアウトモデルに該当するもの。 |
Container | 直訳すると、「目に見える四角形の要素」。つまりはViewのこと。 背景や境界線、影といったものを作成するために使用される。 |
AppBar | 画面上部に56ピクセル(端末に依る)、内部に8pixelのpaddingがある。 Rowのレイアウトで構成されている。 |
Scaffold | 縦方向のColumn。その最上部はMyAppBarの代わりになっている。 |
RadioListTile | テキスト付きラジオボタン title: ラジオボタン横の文字列 value: 代入する値 groupValue: valueを代入する変数 onChanged: タップ時の処理 |
AppBar
以下の画像は公式ドキュメントより
外部ライブラリのインストール
外部からライブラリをインストールする際はpubspec.yaml
にライブラリを記載する。
以下の例では、マテリアルアイコン(よく見かける「矢印」「ファイル」といった汎用的なアイコン)も外部ライブラリのため開発者側で記載する必要がある。
name: {アプリ名}
flutter:
uses-material-design: true
Gestureの操作
以下、サンプルコード
import 'package:flutter/material.dart';
class MyButton extends StatelessWidget {
const MyButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print('MyButton was tapped!');
},
child: Container(
height: 50.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: const Center(
child: Text('Engage'),
),
),
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: MyButton(),
),
),
),
);
}
GestureDetector
は目に見える要素を持たないが、ユーザーのgestureを検知できる。
ユーザーがContainerをタップした時、GestureDetectorはOnTap()というメソッドを呼ぶ。上記の例はコンソールにメッセージを表示。
また、ElevatedButton
, とFloatingActionButton
は onPressed()
メソッドがタップを検知できる。
メソッド名 | 検知するGesture |
onTap | ボタンのタップ時 |
onTapDown | ボタンに触れた時 |
onDoubleTap | 素早く二度タップされた時 |
onHorizontalDragDown | 水平方向に移動させた時 |
onHorizontalDragStart | 水平方向に移動開始時 |
onHorizontalDragEnd | 水平方向に移動終了時 |
onVerticalDragDown | 垂直方向に移動させた時 |
onVerticalDragStart | 垂直方向に移動開始時 |
onVerticalDragEnd | 垂直方向に移動終了時 |
onLongPress | 長時間長押しした時 |
onLongPressDown | ? |
onLongPressEnd | 長押しが終わった時 |
onPressed | ボタンが押された時 |
StatefulWidget
and State
Widgetsは一時的なオブジェクトで、アプリの現状を表示する。Stateはbuildメソッドが呼ばれている間、情報を記憶している。
ほとんどの複雑なアプリはwidgetの異なる部分で違う懸念点が担当するかもしれない。
例えば、widgetは場所や日付といった特定の情報を集めることが目的の
ラジオボタン
プロパティ | 役割 |
title | ラジオボタン横の文字列 |
value | 代入する値 |
groupValue | valueを代入する変数 |
onChanged | タップ時の処理 |
RadioListTileを使用すると、テキスト付きラジオボタンを実装できるようです。
実装しての注意点は以下でした。
- RadioListTileを使用する場合、Columnを使わなければならない(縦並びのみ)
- タップ時の挙動を実装する際、setStateメソッドを使用しないとラジオボタンが切り替わらない
class WinTypeScreen extends StatefulWidget {
@override
WinTypeContainer createState() => WinTypeContainer();
}
enum WinType {
tsumo,
ron
}
class WinTypeContainer extends State<WinTypeScreen> {
WinType _winType = WinType.tsumo;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child:Column(
children: [
SizedBox(width: 200, height: 50.0),
RadioListTile(
title:Text("ツモ"),
value: WinType.tsumo,
groupValue: _winType,
onChanged:(value) => _selectedWinType(value),
),
RadioListTile(
title:Text("ロン"),
value: WinType.ron,
groupValue: _winType,
onChanged:(value) => _selectedWinType(value),
)
],
)
)
);
}
_selectedWinType(value) {
setState(() {
_winType = value;
});
}
}
参考:https://www.choge-blog.com/programming/flutterradiobutton-horizontal/
CupertinoSlidingSegmentedControl
プロパティ | 役割 |
children | 選択肢(Map型のオブジェクト) |
thumbColor | 選択しているセルの背景色 |
groupValue | 代入先の変数 |
onValueChanged | タップ時に行われる処理 |
iOSでいう、SegmentControlを実装できる。横並びで選択式のUIを実装したい時に使えるかも。
- cupertino.dartをインポートする
- Map型の配列を用意して、それをchildrenプロパティにセットする
- onValueChangedに直接書く必要があるのか…?
import 'package:flutter/cupertino.dart';
enum FourWindOrderType {
east,
south,
weat,
north
}
class FourWindOrderScreen extends StatefulWidget {
@override
FourWindOrderContainer createState() => FourWindOrderContainer();
}
class FourWindOrderContainer extends State<FourWindOrderScreen> {
FourWindOrderType _windType = FourWindOrderType.east;
final Map<FourWindOrderType, Widget> windSegment = const <FourWindOrderType, Widget>{
FourWindOrderType.east: Text("東家"),
FourWindOrderType.south: Text("南家"),
FourWindOrderType.weat: Text("西家"),
FourWindOrderType.north: Text("北家"),
};
@override
Widget build(BuildContext context) {
return Scaffold(
body: CupertinoSlidingSegmentedControl(
children: windSegment,
thumbColor: CupertinoColors.activeBlue,
groupValue: _windType,
onValueChanged: (newValue) {
setState(() {
_windType = newValue as FourWindOrderType;
print(_windType);
});
}),
);
}
}
参考:https://flutteragency.com/cupertinoslidingsegmentedcontrol-widget/
Switch
プロパティ | 役割 |
value | switchのON / OFF |
activeColor | ON時のスイッチの丸部分の色 |
activeTrackColor | ON時のスイッチのバー部分の色 |
inactiveThumbColor | OFF時のスイッチのバー部分の色 |
inactiveTrackColor | OFF時のスイッチの丸部分の色 |
onChanged | スイッチを切り替えた時に処理する内容 |
ここは比較的、注意すべき点はなかった。
class ReachScreen extends StatefulWidget {
@override
ReachContainer createState() => ReachContainer();
}
class ReachContainer extends State<ReachScreen> {
bool isReach = false;
@override
Widget build(BuildContext context) {
Switch reachSwitch = Switch(value: this.isReach,
activeColor: Colors.purple,
activeTrackColor: Colors.black,
inactiveThumbColor: Colors.yellow,
inactiveTrackColor: Colors.green,
onChanged: _changeReachSwitch);
return Scaffold(
body: Row(children: [
Text("立直"),
reachSwitch,
],)
);
}
_changeReachSwitch(bool isReach) {
setState(() {
this.isReach = isReach;
print("立直:${this.isReach}");
});
}
}
参考:https://flutter.ctrnost.com/basic/interactive/form/switch/
DropdownButton
プロパティ | 役割 |
value | 選択した時の値 |
items | 表示する選択肢(DropdownMenuItemのList) |
onChanged | 選択時に行われる処理 |
DropdownMenuItem
プロパティ | 役割 |
child | 表示するUI(Textなど) |
value | 選択された時の |
要はプルダウンのUIです。
比較的楽に実装できるのはありがたい。
実装してみたら簡単。Listの初期化は慣れてないのでちょっと悩みましたが。
class ConsecutiveWinsScreen extends StatefulWidget {
@override
ConsecutiveWinsContainer createState() => ConsecutiveWinsContainer();
}
class ConsecutiveWinsContainer extends State<ConsecutiveWinsScreen> {
List<DropdownMenuItem> _items = [];
int _consecutiveWins = 0;
@override
void initState() {
super.initState();
setItems();
_consecutiveWins = _items[0].value;
}
void setItems() {
for(var i = 0; i < 9;i++) {
_items.add(DropdownMenuItem(
child: Text("${i}本場"),
value: i,
));
}
}
@override
Widget build(BuildContext context) {
DropdownButton consecutiveWinsDropButton = DropdownButton(
value: _consecutiveWins,
onChanged: (value) {
setState(() {
_consecutiveWins = value;
print("${_consecutiveWins}本場");
});
},
items: _items,
);
return Scaffold(
body: Row(children: [
Text("積み棒"),
consecutiveWinsDropButton,
],)
);
}
}
参考:https://cbtdev.net/flutter-buttons/
Delegateメソッド
iOSのデリゲートメソッドを実装したい。
GridViewなどでセルをタップした時、他のViewに対して処理を行いたい。
そういった処理を実現するために使える。
これは、端的に言うと、メンバ変数にメソッドを組み込むことで実装できる。
class TileButtonCellScreen extends StatefulWidget {
final String imagePath;
final Function cellTapped;
TileButtonCellScreen({required this.imagePath, required this.cellTapped});
@override
TileButtonCellContainer createState() => TileButtonCellContainer(imagePath: this.imagePath, cellTapped: this.cellTapped);
}
class TileButtonCellContainer extends State<TileButtonCellScreen> {
int _tapCount = 4;
final String imagePath;
final Function cellTapped; //****** デリゲートメソッドの受け取り先 ******//
TileButtonCellContainer({required this.imagePath, required this.cellTapped});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
setState(() {
if (_tapCount > 0) {
_tapCount--;
print("count:${_tapCount}");
this.cellTapped(); //****** デリゲートメソッド実行部分 ******//
}});
},
child: /* 中略 */
);
}
}
class TileButtonScreen extends StatefulWidget {
@override
TileButtonContainer createState() => TileButtonContainer();
}
class TileButtonContainer extends State<TileButtonScreen> {
//****** デリゲートメソッドの定義 ******//
void _cellTapped() {
print("TileButtonContainerの処理です");
}
TileButtonContainer() {
_viewData.add(TileButtonCellScreen(imagePath: /** imagePath **/,
cellTapped: _cellTapped)); //****** デリゲートメソッド代入 ******//
}
var _viewData = <Widget>[];
@override
Widget build(BuildContext context) {
return Scaffold(
body: /** 中略 **/
children: _viewData),
);
}
}
参考:https://betterprogramming.pub/data-flow-and-delegation-in-flutter-apps-1a6fedce90ef
Image 画像を表示する
画像を表示するためのもの
プロパティ | 役割 |
image | 画像のファイルパス(AssetImageを使用する) |
fit | BoxFit型。どのように表示するか |
width | 画像の幅 |
height | 画像の高さ |
Boxfifのプロパティ
プロパティ | 役割 |
fill | |
contain | |
cover | |
fitWidth | |
fitHeight | 高さに合わせて幅を等倍? |
none | |
scaleDown |
参考:https://soudan.hatenablog.jp/entry/flutter-image-boxfit
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
setState(() {
if (_tapCount > 0) {
_tapCount--;
print("count:${_tapCount}");
this.cellTapped();
}});
},
child: Row(
children: [
Image(image: AssetImage(this.imagePath),
fit: BoxFit.fitHeight, height: 700),
Text("${_tapCount}")
],
// mainAxisAlignment: MainAxisAlignment.center,
),
);
}
Section付きGridView
探してみても、iOSのようなSectionがついたUIが見つからなかった。
そのため、自作する必要がある様子。
class TileButtonContainer extends State<TileButtonScreen> {
List<List<Widget>> _allTiles = [];
var _manTiles = <Widget>[];
var _pinTiles = <Widget>[];
var _souTiles = <Widget>[];
var _charTiles = <Widget>[];
List<String> sectionTitle = ["萬子", "筒子", "索子", "字牌"];
TileButtonContainer() {
/******** _manTiles要素追加 ********/
/******** _pinTiles要素追加 ********/
/******** _souTiles要素追加 ********/
/******** _charTiles要素追加 ********/
_allTiles = [_manTiles, _pinTiles, _souTiles, _charTiles];
}
@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.orange,
body: new Container(
child: new ListView.builder(
shrinkWrap: true,
itemCount: _allTiles.length,
itemBuilder: (context, index) {
return new Column(
children: <Widget>[
new Container(
color: Colors.green,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(sectionTitle[index],
style: new TextStyle(
fontSize: 20.0, color: Colors.white)),
],
),
),
new Container(
child: new GridView.count(
shrinkWrap: true,
crossAxisCount: 5,
physics: NeverScrollableScrollPhysics(),
children: _allTiles[index],
),
),
],
);
},
),
),
);
}
void _cellTapped() {
print("TileButtonContainerの処理です");
}
}
分解してみてみよう。
- ListView.builderで実装する。リスト化したいWidgetの数が多かったり、まだ決まってない場合に使われる。(参照:https://flutternyumon.com/how-to-use-listview/)
- shrinkWrapをtrueにする。(shrinkWrapをtrueにすることで、表示最低限しか表示されないように高さを設定する)ListView内でColumnなど使用する場合、高さ不明となるため、この設定が必要となる。(参照:https://www.choge-blog.com/programming/flutterlistview-columnuse/)
- itemCountはそのまま。セクションの数と思ってくれれば良い。
- itemBuilderでindexを使いリストの要素を指定してWidgetを返すことで、表示を動的に行う。
- ヘッダー部分とデータ部分を縦に並べた、ColumnをitemBuilderのWidgetとして設定する
- ヘッダー部分はContainerの中にRowを設定し、アイコンなど並べられるようにする
- データ部分は好きなUIを。今回はGridViewを設定。
- physicsにNeverScrollableScrollPhysics()を設定することで、GridView自体はスクロールできないようにする。
TextField
プロパティ | 役割 |
enabled | bool型。入力可能かどうか設定する |
maxLength | int型。入力文字数最大数 |
maxLengthEnforced | bool型。入力文字数を超えて入力できるかどうか。 trueの場合は、入力文字数までしか入力できない。 |
style | TextStyle型。文字色などを設定する |
maxLines | int型。入力可能な行数。 |
obscureText | bool型。入力した内容をパスワードのようにマスクしてくれる。 |
onChanged | メソッド。文字を入力したときに行われる処理? |
decoration | InputDecoration型。ラベルやヒントなどを表示する。 |
プロパティ | 役割 |
icon | テキスト上部にアイコンを表示させる |
hintText | placeholder。未入力の場合に表示されるテキスト |
labelText | テキスト上部に表示させる。 イメージとしては、アカウント登録する際の、項目名のような感じ。 |
文字入力をする際に使用されるもの。
@override
Widget build(BuildContext context) {
return Scaffold(
body: TextField(enabled: true,
maxLength: 2,
style: TextStyle(color: Colors.black),
maxLines: 1,
onChanged: _handleText,
decoration: InputDecoration(
hintText: "立直棒の数"
)
)
);
}
参考:https://flutter.ctrnost.com/basic/interactive/form/textfield/
画像表示
画像を表示する際、相対パスとかでできないなと思っていたのですが、Assetから画像を取得するということで解決できました。
flutter:
uses-material-design: true
assets:
// ディレクトリ名は自由と思われる
- assets/images/
// 画像呼び出し箇所
Image.asset("assets/images/man$1.jpeg", fit: BoxFit.fitHeight, height: 700),
参考:https://qiita.com/yu124choco/items/a2710ec004d3425a2a0b
Widget
プロパティ | 役割 |
initStateメソッド | State のサブクラスを作成し、initState をオーバーライドすることでウィジェットの作成時に任意の処理を行うことができる。State のサブクラスをしようするので必然的にStatefulWidget のサブクラスも使用することになります。Androidの onCreate 、iOSのviewDidLoad 的な役割 |
disposeメソッド | Stateオブジェクトが不要になるときに呼び出される。 タイマーの終了やデータのアンサブスクライブなどの終了処理を行う。 |
BLocデザインパターン
入力はSinkを使い、値の出力はStreamを使う。
そして、Logic部分とUI部分を分離させる。Providerを使用することで、複数のWidgetで一つの状態を管理できる。
参照:https://qiita.com/tetsufe/items/7b2f8592f5161104d1cd
参照:https://qiita.com/sekitaka_1214/items/b087f9e9fc13424a64bb
MethodChannel
呼び出し先のメソッド名と引数のデータの2つを引数として渡す。
Flutter Engineはデータを受け取ると、そのデータをMethod ChannelのAPIの形に変更し、対象のプラットフォームのAPIをコールします。
Dart → プラットフォーム
- [プラットフォーム側] MethodChannel#setMethodCallHandlerでコールバックを登録
- [Dart側] MethodChannel#invokeMethodで呼び出したいメソッド名とデータをセットして非同期でコール
- [プラットフォーム側] 受け取ったデータ (メソッド名) を見て、対象の処理を実施し、Result#success (エラーの場合はerror) をコール
- [Dart側] メソッドコールの結果を確認