Flutter でアプリ開発(4. 音楽プレーヤーもどき:画面レイアウト)

前回 svpl.hatenablog.jp

基本はわかってきたので、もう少し実践的なことをやっていきます。試しに音楽プレーヤーもどきを作ってみながら学んでいこうと思います。「もどき」とつくのは、実際には音楽を再生しないからです。UI の作り方や画面遷移、データの扱いなどがメインです。

完成した画面

プログラミングの話の前に、完成した画面を先にやります。

まずはデザインを考えます。デザインツールに InVision Studio を使いました。このようなソフトを使うと、UI のプロトタイプが簡単に作れます。

以下はデザイン案です。

プロトタイプを元にアプリを作成しました。以下は完成した実際の画面です。

画面こそ完成しましたが、プレイリストをスクロース以外は何も操作ができません。今回は UI のレイアウトのみです。

以上は完成した画面でしたが、この先は作り方に入っていきます。

データを用意

まずは画面に表示するデータを用意します。今回は Billboard Japan Hot 100 Year End のランキングを使用しました。

const musicChart = [
  {'title': 'ドライフラワー', 'artist': '優里'},
  {'title': 'Dynamite', 'artist': 'BTS'},
  {'title': '夜に駆ける', 'artist': 'YOASOBI'},
  {'title': '炎', 'artist': 'LiSA'},
  {'title': '怪物', 'artist': 'YOASOBI'},
  {'title': 'Butter', 'artist': 'BTS'},
  {'title': 'うっせぇわ', 'artist': 'Ado'},
  // 以下略
];

// 再生中の曲
const musicPlayingIndex = 10;

普通の音楽アプリのプレイリストならデータが変更されるのが当たり前ですが、今回はとりあえずハードコーディングで用意しました。また、再生中の曲も固定しています。

UI 実装

Flutter では、ウィジェット入れ子構造にして定義します。今回は以下のような構造になっています。*で囲んだものは自作ウィジェットです。

MaterialApp
└── *PlaylistBody*
    └── Scaffold
        ├── AppBar
        │   ├── Text(上部バーに表示されるタイトル)
        │   ├── IconButton(3本ラインボタン)
        │   └── IconButton(3点ドットボタン)
        └── Column
            ├── Expanded
            │   └── ListView
            │       ├── *Song*
            │       │   └── Container
            │       │       └── Row
            │       │           ├── Expanded
            │       │           │   └── Text(曲名)
            │       │           └── Expanded
            │       │               └── Text(アーティスト名)
            │       └── Divider
            └── *NowPlaying*
                └── Container
                    └── Row
                        ├── Expanded
                        │   └── Column
                        │       ├── Text(再生中の曲名)
                        │       └── Text(再生中のアーティス名)
                        └── Icon(一時停止ボタン)

なかなかわかりにくいですが、ビジュアルデバッグを入れると構造が視覚的にわかります。DevTool を起動して Show Guidelines をクリックすると表示されます。

main 関数

自作した PlaylistBody ウィジェットを作成するだけです。

void main() {
  runApp(
      const MaterialApp(
          home: PlaylistBody()
      )
  );
}

PlaylistBody ウィジェット

class PlaylistBody extends StatelessWidget {
  const PlaylistBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: const Text('Billboard Japan Hot 100 Year End'),
            leading: const IconButton(
              icon: Icon(Icons.menu),
              tooltip: 'メインメニュー',
              onPressed: null,
            ),
            actions: const [
              IconButton(
                icon: Icon(Icons.more_vert),
                tooltip: 'サブメニュー',
                onPressed: null,
              )
            ]),
        body:
        Column(
          children: [
            Expanded(
                child: ListView.separated(
                  itemCount: musicChart.length,
                  itemBuilder: (BuildContext context, int index) => Song(
                      musicChart[index]['title'],
                      musicChart[index]['artist'], index == musicPlayingIndex),
                  separatorBuilder:
                      (BuildContext context, int index) => const Divider(),
              )
            ),
            const NowPlaying(),
        ],
        )
    );
  }
}

新しく登場したのは ListView です。itemCount に表示させる件数を指定し、itembuilder にウィジェットを作成する関数を指定します。itemBuilder にはリストのインデックスが渡されるので、それを使ってウィジェットを作ります。
separatorBuilder には、項目の間に表示するウィジェットを指定します。Divider を指定して、区切り線と適度なスペースを表示しました。

ListView には、

  • ListView
  • ListView.builder
  • ListView.separated

の 3 種類がありますが、項目を更新できるのは builder だけです。今回の例はプレイリストなので、本当は builder を使うべきでした。

NowPlaying ウィジェット

再生中の音楽の情報と、一時停止の操作ができます。ですが、今回は見た目だけで何もできません。

class NowPlaying extends StatelessWidget {
  const NowPlaying({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final TextStyle defaultTextStyle = DefaultTextStyle.of(context).style;
    return Container(
        height: 90,
        color: Colors.grey[300],
        padding: const EdgeInsets.symmetric(horizontal: 20),
        child: Row(
          children: [
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('勿忘',
                      style: defaultTextStyle.apply(
                        fontSizeFactor: 1.5,
                        fontWeightDelta: 1,
                      )),
                  Text(
                    'Awesome City Club',
                    style: defaultTextStyle.apply(
                      fontSizeFactor: 1.1,
                      color: Colors.black54,
                    ),
                  ),
                ],
              ),
            ),
            const Icon(Icons.pause_circle_outline, size: 60),
          ],
        )
    );
  }
}

文字サイズの指定では、fontSizeFactor に数値を直接指定するのではなく、DefaultTextStyle.of(context).style.apply を使っています。こうすると、現在のデフォルト設定からの相対的な文字サイズを指定でき、Android の文字サイズ設定なども反映されます。

Song ウィジェット

ListView で使う、曲の情報を表示するウィジェットです。

class Song extends StatelessWidget {
  const Song(this.title, this.artist, this.playing, {Key? key})
      : super(key: key);
  final String? title;
  final String? artist;
  final bool playing;

  @override
  Widget build(BuildContext context) {
    final TextStyle defaultTextStyle = DefaultTextStyle.of(context).style;
    final Widget playIcon;

    if (playing) {
      playIcon = const Icon(Icons.play_arrow_outlined, size: 30);
    } else {
      playIcon = const SizedBox(width: 30);
    }

    return Container(
      height: 50,
      padding: const EdgeInsets.only(right: 20, left: 3),
      child: Row(children: [
        playIcon,
        Expanded(
          flex: 3,
          child: Text(
            title != null ? '$title' : '',
            style: defaultTextStyle.apply(fontSizeFactor: 1.3),
          ),
        ),
        Expanded(
          flex: 2,
          child: Text(
            artist != null ? '$artist' : '',
            style: defaultTextStyle.apply(color: Colors.black54),
            textAlign: TextAlign.right,
          ),
        )
      ]),
    );
  }
}

再生中は左端に再生マークを、そうでないときは SizedBox で空白を表示します。
Expanded に Flex を指定すると、その割合でウィジェットの大きさが割り当てられます。ここでは、タイトルとアーティスト名の表示領域が 3:2 の割合になります。

まとめ

思った以上に簡潔な記述で UI が構築できました。かっこが多いので長く感じますけれども。レイアウトに使える様々なウィジェットやアイコンが最初から入っているのは良いですね。

次回は、再生する曲を選んだり、メニューを表示させたいと思います。

Flutter でアプリ開発(3. 状態の変化とウィジェットへの反映)

前回 svpl.hatenablog.jp

当たり前のことですが、何かしらの入力があったときは画面が変化するものです。画面を変化させるためには変数を定義して、変更に応じて反映させる必要があります。
今まで使っていた StatelessWidget ではそれができないので、今回は別の方法でやっていきたいと思います。

参考ページ

StatefulWidget を使う

Flutter には StatelessWidget の他に、StatefulWidget というクラスも用意されています。

以下のソースは公式のチュートリアルのものです。
画面にボタンが表示され、押した回数だけカウンターが上がっていきます。

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

前回は自作のウィジェットを作成するために、 1 つのクラス(StatelessWidget を継承したクラス)しか定義しませんでしたが、今回は以下のクラスを定義しています。

  • StatefulWidget をオーバーライドした Counter クラス
    • 画面には表示されない
  • State をオーバーライドした _CounterState クラス
    • データを保持する(ここでは _counter 変数)
    • ウィジェットを生成・再生成する

StatelessWidget の時とは違い、build 関数は StatefulWidget には定義されていません。build 関数は State で定義され、データが変更されるたびにウィジェットを生成して表示するようになっています。
アプリが動いている間、State は残り続ける一方、データが変更されるたびに作られたウィジェットは破棄されうることになります。

また、State クラス内で変数を変更するときは setState 関数にコールバックを渡して行う必要があります。そうすることで、値が変更されたことが Flutter に通知され、正しく UI が更新されます。

以下は実行画面です。

f:id:sulp:20220130221424p:plain

ボタンを追加する

先程の例をアレンジして、カウンターを減らすボタンを追加します。
build 関数を以下のように変更しました。

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      // 追加:Columnを使ってボタンを縦に配置
      Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: _increment,
              child: const Text('+ Increment'),
            ),
            //追加:数を減らすボタン
            ElevatedButton(
              onPressed: _decrement,
              child: const Text('- Decrement'),
            )
          ]
      ),
      const SizedBox(width: 16),
      Text('Count: $_counter'),
    ],
  );
}

カウンターを減らす関数は次の通りです。

void _decrement() {
  setState(() {
    _counter--;
  });
}

実行画面です

f:id:sulp:20220130221444p:plain

これでも良いのですが、2 つのボタンを 1 つのウィジェットとしてまとめ、それを子ウィジェットとして作成した方がわかりやすくなります。以下の例では、2 つのボタンを持つ CounterButtons という StatelessWidget を作りました。

以下は、公式チュートリアルをアレンジして作った例です。

import 'package:flutter/material.dart';

class CounterButtons extends StatelessWidget {
  const CounterButtons({
    required this.onIncrement,
    required this.onDecrement,
    Key? key
  }) : super(key: key);

  final VoidCallback onIncrement;
  final VoidCallback onDecrement;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: onIncrement,
            child: const Text('+1 Increment'),
          ),
          ElevatedButton(
            onPressed: onDecrement,
            child: const Text('-1 Decrement'),
          )
        ]
    );
  }
}

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  void _decrement() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        CounterButtons(
          onIncrement: _increment,
          onDecrement: _decrement,
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

CounterButtons は 2 つのボタンを build 関数で作成します。ボタンを押した時に呼ばれる関数は、コンストラクタで渡されるようにしました。それらの関数を経由してカウンターの値を変更しています。

たった 2 つのボタンを表示したいというだけでは、ありがたみもそんなに感じないですが、もっと UI が複雑になってくると必須だと思います。また、ウィジェットを使い回すこともできます。

画面は前と同じです。

Reverpod を使う

ウィジェットがデータを持ってしまうと、めんどくさいことになりがちなので、別途、状態管理ライブラリを使用することができます。いろいろ選択肢はあるようですが、今回は Riverpod を使ってみます。

pubspec.yaml の dependencies に必要なパッケージ(flutter_hooks / hooks_riverpod)を追加します。今回は hooks_riverpod を使いませんでしたが、後で使いそうなので入れておきました。

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.2
  hooks_riverpod: ^1.0.3

Android Studio の下端の Terminal ボタンを押して、flutter pub get と入力して実行すると、ライブラリがダウンロードされます。

f:id:sulp:20220130221459p:plain

先程のサンプルを、Riverpod を使って書き直しました。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 追加:状態を保持するオブジェクト
final counterProvider = StateProvider((ref) => 0);

// 親クラスを ConsumerWidget に変更
class CounterButtons extends ConsumerWidget {
  const CounterButtons({Key? key}) : super(key: key);

  // build 関数の引数に WidgetRef が追加される
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            // 変更:counterProvider を経由して値を変更
            onPressed: () => ref.watch(counterProvider.notifier).state++,
            child: const Text('+1 Increment'),
          ),
          ElevatedButton(
            // 変更:同上
            onPressed: () => ref.watch(counterProvider.notifier).state--,
            child: const Text('-1 Decrement'),
          )
        ]
    );
  }
}

// 親クラスを ConsumerWidget に変更
class Counter extends ConsumerWidget {
  const Counter({Key? key}) : super(key: key);

  // build 関数の引数に WidgetRef が追加される
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // counterProvider を経由して値を取得
    final counter = ref.watch(counterProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        const CounterButtons(),
        const SizedBox(width: 16),
        // counter の値は自動で反映される
        Text('Count: $counter'),
      ],
    );
  }
}

void main() {
  runApp(
      const ProviderScope(  // 追加
        child:
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: Counter(),
            ),
          ),
        ),
      )
  );
}

StatelessWidget と StatefulWidget のどちらも ConsumerWidget で置き換えています。

StatefulWidget を使ったやり方では、Counter に変数があり、コールバック関数を経由して操作を行っていました。
対して上記のやり方では、グローバル変数の counterProvider を経由して値の変更を行っています。なので Counter を意識せずとも、CounterButtons から直接カウンターの値を変更できます。
また、値が変わっても自動で反映されるのは変わりません。
特殊な書き方をする必要がありますが、プログラムの構造は分かりやすくなります。

Dart 言語で気になったこと

関数定義

以下のどのようにも書けるようです。

// 普通
int add(int a, int b){
  return a + b;
}
// アロー演算子
int add_oneline(int a, int b) => a + b;

// 匿名関数
int Function(int, int) add_lambda = (int a, int b) {
  return a + b;
};
// 匿名関数 + アロー演算子
int Function(int, int) add_lambda_oneline = (int a, int b) => a + b;

型推論を使えば、以下のようにも書けます。

// 型推論
var add_lambda_oneline = (int a, int b) => a + b;

まとめ

ユーザーの入力を受け付けるようになり、ようやくアプリっぽくなってきました。
状態変化の処理は Riverpod のおかげで、相当かんたんに書けるようになりました。これじゃあ StatefulWidget なんていらないじゃないか。入れるのは必須と言って良いかもしれません。勉強量が増えますけど……
次回はここまでの応用で、もっとアプリっぽい見た目の物を作ってみようと思います。

次回 svpl.hatenablog.jp

Flutter でアプリ開発(2. 初めてのアプリ開発)

前回 svpl.hatenablog.jp

環境構築ができたので、早速アプリ開発をやっていきたいと思います。Flutter は(Dart も)全くわからない状態なので、頑張ってやっていきたいと思います。


参考サイト

Hello World

Widget とは

画面に表示する UI 部品のこと。Web 系の React の影響を受けて設計されているらしい。

以下は公式チュートリアルのサンプル。ファイルは lib/main.dart です。
他のファイルは、前回のプロジェクト作成時にできたファイルをそのまま使っています。

import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

main 関数から始まるのは C 言語からの伝統ですね。
runapp 関数は、引数にウィジェットを取って、画面全体に配置する。ここでは Center ウィジェットを指定しています。また、Center ウィジェットは、その子ウィジェットとして Text ウィジェットを指定しています。ここでは textDirection の指定は必須のようです。ltr は左から右(left to right)ですね。
実行すると、以下の画面になりました。

f:id:sulp:20220126213158p:plain

コンパイル・実行方法

基本的に実機でデバッグしていきたい思います。上の Debug ボタンを押すと、アプリがコンパイルされ、スマホに転送されて実行されます。下の稲妻ボタンで Hot Reload、その隣が Hot Restart ができます。

f:id:sulp:20220126213256p:plain

Hot Reload・Hot Restart を使うと、変更したソースが数秒もしないでアプリに反映されます。使い分けはよくわかってませんが、変更が大きくて Hot Reload をしても反映されないときに、Hot Restart を使っています。
ちなみに、初回起動は約 1 分かかりました(CPU:AMD A8-7600・ストレージ:M.2 SSD)。

その他の基本的な Widget

  • Text
  • Row, Column
    • 縦や横に並べる。CSS の flexbox と似たような機能ということらしい。
  • Stack
  • Container
    • 四角いボックス。背景、境界線、影などの見た目を設定できる。
  • Expanded

    • Row や Column の子ウィジェットを、余った残りの空白を埋めるように拡張する。

これらを使って、以下のソースを作ってみました。

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(

      // 上から下に並べる
      child: Column(
        textDirection: TextDirection.ltr,
        children: <Widget>[

          // テキスト
          const Text('テキスト1', textDirection: TextDirection.ltr),
          const Text('テキスト2', textDirection: TextDirection.ltr),

          // 四角形
          Container(
            width: 50,
            height: 50,
            color: Colors.green[700],
          ),

          // 残りの画面を埋める
          const Expanded(
            child: const Text('テキスト3', textDirection: TextDirection.ltr),
          ),

          // 画面が埋められたので、一番下に表示される
          const Text('テキスト4', textDirection: TextDirection.ltr),
        ],
      ),
    )
  );
}

これを実行すると、以下の画面になりました。

f:id:sulp:20220126213348p:plain

マテリアルコンポーネントを使う

マテリアルデザインができたり、便利な機能がある MaterialApp ウィジェットを使うことが推奨されているようです。
以下は公式チュートリアルのサンプル

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Flutter Tutorial',
      home: TutorialHome(),
    ),
  );
}

class TutorialHome extends StatelessWidget {
  const TutorialHome({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for
    // the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: const Text('Example title'),
        actions: const [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: const Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: const FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

ソースを見ると、まず StatelessWidget を継承した TutorialHome クラス(ウィジェット)を作成し、build 関数をオーバーライドして、その中で画面のデザイン等を作成しているようですね。runApp 関数では MaterialApp ウィジェット を作成し、その中で TutorialHome ウィジェットも作成しています。

ルートに置かれているのは Scaffold ウィジットで、いい感じのマテリアルデザインになるように、子ウェジットを配置してくれるようです。
ここでは主に、以下のウィジェットを使っています。

  • appBar -> AppBar
  • body -> Center
  • floatingActionButton -> FloatingActionButton

これを実行すると、以下の画面になりました。

f:id:sulp:20220126213421p:plain

このサンプルをアレンジして、以下のエセ音楽プレーヤー作成しました。

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      // titleは、Androidでは開いているアプリ一覧などに表示される
      // iOSでは使われない(未確認)
      title: '音楽プレーヤー',
      home: MusicPlayerHome(),
    ),
  );
}

class MusicPlayerHome extends StatelessWidget {
  const MusicPlayerHome({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      // 上部の青色のバー
      appBar: AppBar(
        // タイトル
        title: const Text('音楽プレーヤー'),
        // 左上のメニューボタン
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'メニュー',  // アイコン長押しで表示されるテキスト
          onPressed: null,
        ),
        // 右上のボタン
        actions: const [
          // 音符アイコン
          IconButton(
            icon: Icon(Icons.audiotrack),
            tooltip: '再生リスト',
            onPressed: null,
          ),
          // スピーカーアイコン
          IconButton(
            icon: Icon(Icons.speaker),
            tooltip: 'イコライザー',
            onPressed: null,
          ),
        ],
      ),

      // メインの部分
      body: Center(
        // 音楽タイトル一覧
        child: Column(
          // 中央に配置
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          // 表示するテキスト
          children: const [
            Text('ドライフラワー', textScaleFactor: 2),
            Text('Dynamite', textScaleFactor: 1.5),
            Text('夜に駆ける', textScaleFactor: 1.5),
            Text('炎', textScaleFactor: 1.5),
            Text('怪物', textScaleFactor: 1.5),
          ],
        ),
      ),

      // アクションボタン(右下に表示される)
      floatingActionButton: const FloatingActionButton(
        tooltip: '開く',
        child: Icon(Icons.folder),
        onPressed: null,
      ),
    );
  }
}

実際に動かすと、以下のような画面になります。

f:id:sulp:20220126213510p:plain

StatelessWidget と StatefulWidget

Flutter のアプリ開発では、自分でウィジェットを作成することがよくあるようです。上記での MusicPlayerHome も TutorialHome も自分で作ったウィジェットですね。ウィジェットを作って、再利用したりまったりするのでしょう。Vue.js でのコンポーネントと近い感じがします(React もそうですかね)。
ウィジェットは StatelessWidget か StatefulWidget のどちらかを継承するようでうが、以下のような違いがあります(StatefulWidget はまだ使ってないけど)。

  • StatelessWidget
    • 一度表示したら、内容が変わらないウィジェット
    • 書き換えが発生しないのでパフォーマンスが良い。できるだけこちらを使いたい。
  • StatefulWidget
    • 表示したあと、保持しているデータが変更され、UI の表示も変わるウィジェット
    • 書き換えが発生すると、Widget を再生成する処理がされるようなので、パフォーマンスに影響がある。

とりあえず今回は StatelessWidget だけ使うことにします。

ジェスチャーの検知(タップも)

次はジェスチャーの検知をやってみます。なぜここでジェスチャーをやるかと言うと、公式チュートリアルがそうなっているからです。

以下は公式のチュートリアルのサンプル。

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ),
  );
}

class MyButton extends StatelessWidget {
  const MyButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50.0,
        decoration: BoxDecoration(
          color: Colors.lightGreen[500],
        ),
        child: const Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

GestureDetector は何も表示されないウィジェットで、コールバックと子ウィジェットを指定すると、各種のジェスチャを拾ってくれるようです。ここでは、Container がタップされる(onTap)されると、メッセージがコンソールに表示されるようになっています。

スマホ上ではこのような画面になります。

f:id:sulp:20220126213608p:plain

緑のボタンのような所をタップすると、Android Studio のコンソールに MyButton was tapped!と出力されます。

f:id:sulp:20220126213616p:plain

ジェスチャというくらいなので、他にも実装してみました。

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ),
  );
}

class MyButton extends StatelessWidget {
  const MyButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('タップされました');
      },
      onDoubleTap: (){
        print('ダブルタップされました');
      },
      onLongPress: (){
        print('長押しされました');
      },
      onHorizontalDragEnd: (DragEndDetails details){
        print('フリックされました');
      },
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.lightGreen[500],
        ),
        child: const Center(
          child: Text('操作してください'),
        ),
      ),
    );
  }
}

f:id:sulp:20220126213533p:plain

f:id:sulp:20220126213634p:plain

Dart 言語で気になったこと

一旦 Flutter を離れて Dart の話です。
静的型付け言語なので、スクリプト言語よりは Java などのような雰囲気があります。

名前付き引数

上は普通の関数定義、下は名前付き引数を使った関数定義。どちらも BMI を計算する関数です。

double calcBmi(double weight, double height){
  double height_cm = height / 100;
  return weight / (height_cm * height_cm);
}

// 名前付き引数
double calcBmiNamed({double weight = 65, double height = 170}){
  double height_cm = height / 100;
  return weight / (height_cm * height_cm);
}

// 名前付き引数 + null許容型
double calcBmiNullable({double? weight, double? height}){
  if (weight == null || height == null){
    return -1;
  }
  double height_cm = height / 100;
  return weight / (height_cm * height_cm);
}

名前付き引数を定義したいときは、波括弧で囲む必要があります。名前付き引数は指定されない場合があるので、デフォルト引数を指定するか @required をつけて必須にする必要があります。また、null 許容型にすることでも解決できます。

呼び出し方は以下のとおりです。引数はweight: wのようにコロンで区切って指定します。

double bmi = calcBmi(w, h);
double bmiNamed = calcBmiNamed(weight: w, height: h);
double bmiNullable = calcBmiNullable();

型推論

明示的に型を書かなくてもいい場合があります。あくまで型推論なので、静的型付けには変わりないが。
たとえば、以下のどの書き方でも OK みたいです。

List<String> area1 = <String>['東日本', '中日本', '西日本'];
List<String> area2 = ['東日本', '中日本', '西日本'];
List area3 = <String>['東日本', '中日本', '西日本'];
List area4 = ['東日本', '中日本', '西日本'];
var area5 = ['東日本', '中日本', '西日本'];

クラスのインスタンス

new はいらない。

class SomeClass{
}

void main(){
  SomeClass obj = SomeClass();
}

まとめ

まあなんとなく、Flutter もできるような気もしてきました。他言語や Java でのアプリ開発の経験があるからかもしれません。
次回は StatefulWidget を使って、インタラクションがあるアプリを作る予定です。

次回 svpl.hatenablog.jp