Flutterでアプリ開発するうえでの基礎

Stateful widget

Stateless widgetsは変わらないもの、全ての値がfinalとして扱われる。そのため、変数の変更もできない。
対して、Stateful widgetsはwidgetのライフサイクルで変更していく。
Stateful widgetsを実装する際、以下に条件が必須。

項目名役割
Text指定したスタイルのテキストを表示する
Row / ColumnRowは縦方向、Columnは横方向に並べるようにレイアウトを配置する。
Webでいうflexboxレイアウトモデルに該当するもの。
Stack上下左右の相対的位置を指定して表示することができる。
Webでいうabsolute positioningレイアウトモデルに該当するもの。
Container直訳すると、「目に見える四角形の要素」。つまりはViewのこと。
背景や境界線、影といったものを作成するために使用される。
AppBar画面上部に56ピクセル(端末に依る)、内部に8pixelのpaddingがある。
Rowのレイアウトで構成されている。
Scaffold縦方向のColumn。その最上部はMyAppBarの代わりになっている。
RadioListTileテキスト付きラジオボタン
title: ラジオボタン横の文字列
value: 代入する値
groupValue: valueを代入する変数
onChanged: タップ時の処理

AppBar

以下の画像は公式ドキュメントより

The leading widget is in the top left, the actions are in the top right,
the title is between them. The bottom is, naturally, at the bottom, and the
flexibleSpace is behind all of them.

外部ライブラリのインストール

外部からライブラリをインストールする際は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代入する値
groupValuevalueを代入する変数
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

プロパティ役割
valueswitchのON / OFF
activeColorON時のスイッチの丸部分の色
activeTrackColorON時のスイッチのバー部分の色
inactiveThumbColorOFF時のスイッチのバー部分の色
inactiveTrackColorOFF時のスイッチの丸部分の色
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を使用する)
fitBoxFit型。どのように表示するか
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の処理です");
  }
}

分解してみてみよう。

  1. ListView.builderで実装する。リスト化したいWidgetの数が多かったり、まだ決まってない場合に使われる。(参照:https://flutternyumon.com/how-to-use-listview/
  2. shrinkWrapをtrueにする。(shrinkWrapをtrueにすることで、表示最低限しか表示されないように高さを設定する)ListView内でColumnなど使用する場合、高さ不明となるため、この設定が必要となる。(参照:https://www.choge-blog.com/programming/flutterlistview-columnuse/
  3. itemCountはそのまま。セクションの数と思ってくれれば良い。
  4. itemBuilderでindexを使いリストの要素を指定してWidgetを返すことで、表示を動的に行う。
  5. ヘッダー部分とデータ部分を縦に並べた、ColumnをitemBuilderのWidgetとして設定する
  6. ヘッダー部分はContainerの中にRowを設定し、アイコンなど並べられるようにする
  7. データ部分は好きなUIを。今回はGridViewを設定。
  8. physicsにNeverScrollableScrollPhysics()を設定することで、GridView自体はスクロールできないようにする。

TextField

プロパティ役割
enabledbool型。入力可能かどうか設定する
maxLengthint型。入力文字数最大数
maxLengthEnforcedbool型。入力文字数を超えて入力できるかどうか。
trueの場合は、入力文字数までしか入力できない。
styleTextStyle型。文字色などを設定する
maxLinesint型。入力可能な行数。
obscureTextbool型。入力した内容をパスワードのようにマスクしてくれる。
onChangedメソッド。文字を入力したときに行われる処理?
decorationInputDecoration型。ラベルやヒントなどを表示する。
プロパティ役割
iconテキスト上部にアイコンを表示させる
hintTextplaceholder。未入力の場合に表示されるテキスト
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側] メソッドコールの結果を確認

参照:https://qiita.com/kurun_pan/items/db6c8fa94bbfb5c0c8d7