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