fuzzy study

仕事・趣味で勉強したことのメモ

スマホアプリ開発初心者がFlutterのチュートリアルを読んでできるようになったこと

以前よりスマホアプリ開発に興味があったのですが、最近クロスプラットフォームスマホアプリ開発フレームワークのFlutterが話題なので、 チュートリアルを読んで入門してみました。

Flutterとは

FlutterGoogle製のフレームワークで、開発言語はDartです。
昨年登場し、一気に流行しはじめました。

もともとスマホアプリは作ってみたいと思っていたのですがなかなか手が出なかったので、 今回のFlutter流行に乗っかって今度こそと思い、始めてみました。

実際使ってみたところ、少なくとも簡単なアプリならとてもお手軽に作れるので、 初心者にもオススメなのではないかと思います。
私としては、Flutterがスマホアプリ開発の最初の選択肢になるくらい流行ったらいいなあと思います。

チュートリアルを読んでできたこと

  • Flutter開発環境の構築
  • Widgetの理解
  • UIレイアウト方法の理解
  • イベントの理解
  • 状態管理の設計指針の概要理解
  • 画面遷移方法の理解
  • 簡単なアプリの作成
  • Dartの基礎文法理解

本記事では自分が学んだことを簡単にメモしていこうと思います。

※注)自分はAndroidしか持っていないので、AndroidアプリONLYの内容になっている箇所もあるかもしれません。。

Flutter開発環境の構築

環境構築はとても簡単で、いくつも参考記事があるので割愛します。

自分はVSCodeでの開発を選びました。好みだと思いますが、VSCodeは軽量なので、それほど複雑ではないアプリを作る程度であればよい選択肢なのではないかと思います。

flutter.io

Widgetの理解

Flutterでは画面に配置するものをはじめ、ほとんどすべての要素がWidgetというものでできています。
基本はStatelessWIdgetとStatefulWidgetで*1、以下のような性質を持ちます。

  • StatelessWidget : 自分自身は状態を保持しない。親Widgetからもらったパラメータと定数に従って描画される。
  • StatefulWidget : 状態を持ち、ボタンタップやタイマー等のイベント契機で状態を変更し、再描画できる。

StatefulWidfetの子にStatelessWidgetを置き、StatefulWidgetが持つ状態に応じてStatelessWidgetを再作成することでも画面全体としては再描画できます。
このときFlutter内部では、StatelessWidgetはすべてが再作成されるわけではなく、変更があった箇所のみが再作成されるようになっている、らしいです。

Bringing it all together

When the parent receives the onCartChanged callback, the parent updates its internal state, which triggers the parent to rebuild and create a new instance of ShoppingListItem with the new inCart value. Although the parent creates a new instance of ShoppingListItem when it rebuilds, that operation is cheap because the framework compares the newly built widgets with the previously built widgets and applies only the differences to the underlying RenderObject.

UIレイアウト方法

Widgetをネストさせてレイアウトを作ります。

runApp()で指定したWidgetが最初に起動されます。
基本的には、いろいろな便利機能を使うため、MaterialAppをルートにするのが吉のようです。

import 'package:flutter/material.dart';
    
void main() {
  runApp(MaterialApp(home: MyHomePage()));
}

多くの種類のWidgetが標準で用意されており、公式のカタログで確認できます。

簡単なアプリを作るにあたってよく使うであろうWidgetはそれほど多くないので、メモしておきます。

基本レイアウト

  • Row, Column
    • children: に指定したWidgetを横・縦に並べる。これをネストしていくだけで大体のレイアウトは作れる。
      レイアウトの仕方は公式チュートリアルの図がめっちゃわかりやすい。
    • mainAxisAlignment: で主軸方向(Rowなら横、Columnなら縦)方向のWidgetの並べ方を指定できる。以下の記事がとてもわかりやすい。
      medium.com
  • Stack
    • 並べるのではなく、重ねたい場合に使う。
  • ListView
    • スクロールできるRow、Columnのイメージ。
    • 横スクロールのListViewも可能。
    • 無限スクロールにはListView.builderコンストラクタのitemBuilder:を使う。 itemBuilterには(context, index){}形式の関数を与える。indexがListViewのアイテムのインデックスになり、関数が返したWidgetが描画される。 詳細はGetStartedにある。

配置、装飾

  • Center
    • 子を真ん中に配置する。
  • Expanded
    • 子を画面内に配置できる全体に広げる。
  • SizedBox
    • 子のサイズを決め打ちする。
  • Container
    • margin, padding, width, height, color, などなど、やりたいことは大体入っているコンテナ。
    • 細かいことをしたいときはdecoration:にBoxDecorationをつけてやればいい。
  • SingleChildScrollView
    • child:に与えたWidgetがスクロールできるようになる。
    • 配置するWidgetは、ExpandやContainerで描画サイズを決めてあげることで、その枠内でスクロール可能な動きになる。

部品系

  • Text, Icon, Image, RaisedButton, IconButton, TextField, Checkboxなどなど
  • カタログを見ながら、かなり直感的に使えます。

MaterialComponents

  • あらかじめMaterialDesignの標準的なレイアウトを簡単に実現できるように用意されているWidgetがあり、簡単なアプリを作る際には便利。
  • Scaffold, AppBar, TabBar, Drawer, SnackBarなどなど

イベント(ジェスチャ)ハンドリング

RaisedButtonやIconButtonなど、はじめからonPressed:などのプロパティを持っているWidgetはそれらにイベント用関数を与えればOKです。

そうでないWidgetの場合は、GestureDetectorを使います。
GestureDetectorのonTap:などに与えた関数は、child:に与えたWidgetに対するイベントとして登録されます。

状態管理の設計指針の概要

アプリの状態を表すパラメータをどこで管理するべきか、についての設計指針について チュートリアル内にあった説明を簡単にまとめます。

  • 状態をどこで管理するか
  • 大まかな指針としては、
  • Widget単位で見ると以下の3通りがある
    • 全状態を親に置いて、StatelessWidgetにする
    • 全状態を自分の中に閉じて、StatefulWidgetにする
    • 親に置くものと自分で閉じるものを組み合わせて、StatefulWidgetにする
  • 親が管理したいと思うであろうプロパティを親に置くと良い

という感じのようです。

状態変更はStatefulWidgetのsetState()で行うのが基本ですが、複雑なアプリになると 他の方法(後述)も知っておく必要があるようです。

リソース(画像等)の使い方

pubspec.yamlのassets: にファイルのパス(ディレクトリの場合は配下のファイル全て)が プログラム中からリソースとして利用できるようになります。

assetsディレクトリ配下全てを使うなら以下のようにします。

flutter:
  assets:
    - assets/

ロードは以下のようにします。

文字列

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
    
Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}

Futureで受け取ることになるので、FutureBuilderを使ってWidgetを作るなどして使用します。
以下の例では、ファイルのロードが完了するまでは"loading..."、完了したらファイルの内容を表示するText Widgetを作ります。

Text TextFromFile(){
  return FutureBuilder(
    future: loadAsset(), // ここにFutureを返す関数を指定
    builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
      // snapshot.dataにFutureで受け取る結果が格納される
      if (snapshot.hasData) {
            return Text(snapshot.data);
      } else {
        // snapshot.hasDataがfalseならまだ処理が終わっていない
        return Text("loading...");
      }
    },
  );
}

画像

Image Widgetとしてロードするなら以下のようにします。

Image loadImage() {
  return Image(image: AssetImage("assets/my_icon.jpg"));
}

同名の画像ファイルを、2.0xや3.0xのような名前のディレクトリに配置すると、MipMap として使えます。

assets/
├── 2.0x
│   └── my_icon.jpg
├── 3.0x
│   └── my_icon.jpg
├── 4.0x
│   └── my_icon.jpg
└── my_icon.jpg

上記の場合、ロードはassets/my_icon.jpgのみででき、利用時もAssetImage("assets/my_icon.jpg")とするだけで、 現在の画面の解像度に合った画像がロードされるようにできます。

ディレクトリ名の数字はdart:uiパッケージにあるwindow.devicePixelRatioの値を指しており、 最も近い数字のディレクトリ内の画像が使われます。
devicePixelRatioの値は約96dpiで1.0のようです。

画面遷移方法

画面遷移はNavigatorを使います。

画面もWidgetなので、Widget1からWidget2へ遷移したいとします。

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => Widget2()),
);

Widget2から戻るときは、

Navigator.pop(context);

です。

MaterialPageRouteは、MaterialAppの子孫になっているWidgetでないと使えません。

次に、パラメータを渡す・受け取る場合。

  • 遷移元では、Navigator,pushの引数に与える遷移先Widgetのコンストラクタにパラメータを渡します。
  • 遷移先では、Navigator.popの引数に返したいパラメータを与えます。
  • 遷移元での値受取は、Future型でNavigator.pushの戻り値として受け取ります。
// Widget2に10を渡す
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => Widget2(param: 10)),
);
// 100を返す
Navigator.pop(context, 100);

簡単なアプリの作成

簡単なアプリということで、マインスイーパを作ってみました。

github.com

設計的には、RowとColumnで画面レイアウトを作り、 Mineが隠されているマスをStatelessWidget、盤面全体をStatefulWidgetとして作りました。
マスの状態も全体でリストとして管理する形です。

Flutterでマインスイーパを作ってみたという記事はいくつかあり、自分は以下の記事を参考にした上で イチから作成しました。

medium.com

www.youtube.com

後者の動画内で紹介されている内容で役に立ったものについていくつか紹介します。

stopwatch

dart:coreにStopwatchクラスがあります。 時間を測るのに非常に便利です。
使い方も以下の要領で、簡単です。

import "dart:async"; // Future.delayedを使ってスリープさせるためだけに利用

void main() async {
  Stopwatch stopwatch = Stopwatch();

  print("is running? ${stopwatch.isRunning}"); // is running? false 

  stopwatch.start();

  print("is running? ${stopwatch.isRunning}"); // is running? false 

  await Future.delayed(new Duration(seconds: 3)); 3秒スリープ

  stopwatch.stop();

  print(stopwatch.elapsedMilliseconds); // 3005
  print("is running? ${stopwatch.isRunning}"); // is running? false 

  stopwatch.reset();

  print(stopwatch.elapsedMilliseconds); // 0
}

Timer

定期的な処理を実行するときに使います。 import 'dart:async';が必要です。

以下の例では、1秒ごとに定期的な画面更新をする場合のコードです。

// Widget内で
timer = Timer.periodic(Duration(seconds: 1), (t) => setState(() {}));

マインスイーパのタイマーの表示を1秒ごとに更新するために使いました。

まだ理解不十分なこと

状態やデータ管理のアーキテクチャ

状態管理をsetStateでやっていて、簡単なアプリを作っている間はそれほど気になりませんが、 規模が大きくなったり、サーバサイドでDBを使ったりするともっと考慮が必要になると思います。

状態管理の方法としては、以下の記事で挙げられているものとしても4つあります。

  • setState
  • ScopedModel
  • BLoC
  • Redux

medium.com

状態管理は奥の深いトピックで、まだまだ全然理解が追い付いていないので、個人的に今後の課題です。
まずは公式チュートリアルを読むところからですね。。

データベース

以下の記事で紹介されている通り、sqfliteパッケージを使うとsqliteを使えるようです。

medium.com

また、Firebaseの機能は一通り使えるようなのでそのほうがいいかもです。

アニメーション

自分はあまりアニメーションに凝ったアプリを作るつもりがなかったのでまだ見れていませんが、 今後見ていけたらと思います。

flutter.io

Flutterに関するまとめ系リソース

このへんを漁りまくったら身につきそうです。

*1:ほかにもInheritedWidgetなどもあるようですが、簡単なアプリならこの2つの理解でひとまず十分のようです。