うまとま君の技術めも

2015年新卒入社した社畜の勉強内容などなど

Flutterで始めるアプリ開発

Flutterで始めるアプリ開発

Flutterを使ったiOS/Android/Webアプリ開発への入門に必要な情報を
分かりやすく紹介するウェブサイトを立ち上げました。

よかったら利用してみて下さい。

www.flutter-study.dev

ウェブサイト概要

ウェブサイトの対象者

  • アプリを作ってみたいけど、どうやって作るか分からない方 😕
  • iOS/Androidアプリ開発が難しすぎて挫折してしまった方 😨
  • Flutterを使って簡単にアプリを作れるようになりたい方 😆
  • などなど

ウェブサイトのゴール

FlutterとFirebaseを組み合わせた、
少し複雑なiOS/Android/Webアプリ開発ができる状態に持っていくことを目指します 😀

ウェブサイトで紹介する内容

  • Flutterとは
  • Flutterを使ってみる
  • Widgetを使ってみる
  • Todoアプリを作ってみる
  • Firebaseとの連携
  • 少し複雑なアプリ作成
  • 自分だけのWebアプリ公開

更新情報

まだまだ、作成できてないページがあったり、分かりづらい部分も多々あります。。。

新しく追加したページや更新した部分は随時Twitterで共有していくつもりなので、
よかったらこちらもチェックしてみて下さい。

twitter.com

Flutter for Web

Flutter for Web

Flutter for Web

  • Android/iOSに加えてFlutterの対応プラットフォームにWebが入っている
  • Dartで書かれたソースコードJavaScriptへと変換し動作させる
  • 2020年4月時点では beta channel で使用可能(本番利用は非推奨)

使い方

仕組み

  • DartからJavaScriptへの変換処理はDart自体で提供されている仕組みを利用
  • DOM・CanvasCSSを使い各ブラウザで動作する描画処理をFlutter側で提供している

Dart Web

  • webdevを使いDartからJavaScriptへの変換処理を行う
    • 内部では、dartdevc と daert2js の2つのコンパイラが使われている
    • dartdevc
      • the Dart dev compiler
      • 開発用、差分ビルド等が使える
    • dart2js
  • HTMLから変換されたJavaScriptを呼び出すことで処理を実行する

DartでWebアプリケーション作成

$ dart --version
Dart VM version: 2.7.2 (Mon Mar 23 22:11:27 2020 +0100) on "macos_x64"

// DartからJavaScriptへの変換処理を行うためのツール
$ pub global activate webdev

// プロジェクト作成
$ mkdir dartwebapp
$ cd dartwebapp
$ vi pubspec.yaml
name: dartwebapp
description: Dart web application
environment:
  sdk: '>=2.7.0 <3.0.0'
dev_dependencies:
  # dependencies for webdev
  build_runner: ^1.8.1
  build_test: ^0.10.12+1
  build_web_compilers: ^2.9.0

$ vi web/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>dartwebapp</title>
    <!-- JavaScriptに変換されたmain.dartを読み込む -->
    <script defer src="main.dart.js"></script>
</head>
<body>
    <div id="hello"></div>
</body>
</html>

$ vi web/main.dart
import 'dart:html';

void main() {
  querySelector('#hello').text = 'Hello World!!';
}

// Webアプリケーション起動(開発用)
$ pub global run webdev serve
...
[INFO] Serving `web` on http://127.0.0.1:8080
...

// ビルド(本番用)
$ pub global run webdev build --release

Flutterでの描画処理

DomCanvasとBitmapCanvasの使われ方

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          leading: FlutterLogo(),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}
<flt-scene flt-layer-state="updated" style="position: absolute;">
    <flt-transform
        flt-layer-state="updated"
        style="position: absolute; transform-origin: 0px 0px 0px;">
        <flt-offset
            flt-layer-state="retained"
            style="position: absolute; transform-origin: 0px 0px 0px; transform: translate(0px, 0px);">
            <flt-offset
                flt-layer-state="retained"
                style="position: absolute; transform-origin: 0px 0px 0px; transform: translate(0px, 0px);">
                <flt-clip
                    flt-layer-state="retained"
                    clip-type="physical-shape"
                    style="position: absolute; overflow: hidden; background-color: rgb(250, 250, 250); box-shadow: none; left: 0px; top: 0px; width: 552px; height: 815px; border-radius: 0px;">
                    <flt-clip-interior style="position: absolute; left: 0px; top: 0px;">
                        <flt-picture flt-layer-state="retained" style="position: absolute; transform: translate(0px, 0px);">

                            <!-- HTML&CSSのみで描画できる場合 → HTML&CSSを使う (DomCanvas) -->
                            <flt-dom-canvas style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;">
                                <!-- Scaffold.body -->
                                <p style="font-size: 14px; font-weight: normal; font-family: Roboto, Arial, sans-serif; color: rgba(0, 0, 0, 0.867); position: absolute; white-space: pre-wrap; overflow-wrap: break-word; overflow: hidden; height: 16px; width: 72px; transform-origin: 0px 0px 0px; transform: matrix(1, 0, 0, 1, 240, 427.5);">Hello World</p>
                            </flt-dom-canvas>
                        </flt-picture>
                        <flt-clip
                            flt-layer-state="retained"
                            clip-type="physical-shape"
                            style="position: absolute; overflow: hidden; background-color: rgb(33, 150, 243); box-shadow: rgba(0, 0, 0, 0.4) 1.33333px 2.66667px 5.52px 0px; left: 0px; top: 0px; width: 552px; height: 56px; border-radius: 0px;">
                            <flt-clip-interior style="position: absolute; left: 0px; top: 0px;">
                                <flt-picture
                                    flt-layer-state="retained"
                                    style="position: absolute; transform: translate(0px, 0px);">

                                    <!-- HTML&CSSのみで描画できない場合 → Canvasを使う (BitmapCanvas) -->
                                    <flt-canvas style="position: absolute; transform: translate(6px, 1px);">
                                        <!-- Scaffold.appBar -->
                                        <canvas
                                            width="44"
                                            height="54"
                                            style="position: absolute; width: 44px; height: 54px; z-index: -1;"></canvas>
                                    </flt-canvas>
                                </flt-picture>
                            </flt-clip-interior>
                        </flt-clip>
                        <flt-picture
                            flt-layer-state="retained"
                            style="position: absolute; transform: translate(0px, 0px);"></flt-picture>
                    </flt-clip-interior>
                </flt-clip>
            </flt-offset>
        </flt-offset>
    </flt-transform>
</flt-scene>

Nativeコードを呼び出す(JavaScriptを呼び出す)

参考資料

Flutter Widget of the Week まとめ 2

Flutter Widget of the Week

Flutter Widget of the Week で紹介されたWidgetを実際に使ってみたいと思います。

YouTube

FittedBox

LayoutBuilder

AbsorbPointer

Transform

BackdropFilter

Align

Positioned

AnimatedBuilder

Dismissible

SizedBox

ValueListenableBuilder

Draggable

AnimatedList

Flexible

MediaQuery

Spacer

InheritedWidget

AnimatedIcon

AspectRatio

LimitedBox

Placeholder

Flutter - InheritedWidgetを使ってProviderを実装してみる

Provider

  • ProviderとはInheritedWidgetをラッパーし使いやすくした、状態管理をするためのライブラリである。 github.com

InheritedWidget

InheritedWidgetを使ってProviderを実装してみる

  • InheritedWidgetを使い、Provider.of()Provider.value()を実装してみた

感想

  • provider を使うのであれば、InheritedWidgetの理解を深めておくと良さそう
  • StatefulWidgetvalueをもたせていない場合にうまく動作しなかったが原因はよくわかっていない
  • provider v3 では StatefulWidgetを使った実装だが、v4 から使われなくなっている
    • 軽く実装を見てみたが、どのようにしてChangeNotifierの変更を検知し再ビルドしているのか分からなかった

Flutter Widget of the Week まとめ 1

Flutter Widget of the Week

Flutter Widget of the Week で紹介されたWidgetを実際に使ってみたいと思います。

Flutter Widget of the Week - YouTube

SafeArea

Expanded

Wrap

AnimatedContainer

Opacity

FutureBuilder

FadeTransition

FloatingActionButton

PageView

Table

SliverAppBar

SliverList & SliverGrid

FadeInImage

StreamBuilder

InheritedModel

ClipRRect

Hero

CustomPaint

Tooltip

Flutter関連情報まとめ

YouTube

ブログ

イベント

プロトタイピング

Dart

開発ツール

まとめ

Flutter入門

Flutterとは

Flutterとは

  • Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
    • 単一のコードベースでいい感じのネイティブアプリが作れるもの
    • Google
    • iOS, Android, (Web, Desktop) をサポート

特徴

  • Fast development
    • ホットリロードを始めとした、より高速に開発するための様々なツールが提供されている
  • Expressive, beautiful UIs
    • Material Design 等に沿ったUIパーツを使うことで、いい感じのUIが簡単に作れる
  • Native Performance
    • Dartという言語を使い、各プラットフォームに応じたネイティブコードを生成できる

なぜFlutterを使うのか?

  • より高速に開発するため
  • より魅力的なUI/UXを提供するため

アーキテクチャ・コンセプト

  • 独自エンジンを各プラットフォームで動かせるようにしている
    • エンジン内部にはUI描画処理も含まれている >> Skia
  • ウィジェットのツリーでUIを表現
    • Reactのような宣言的UIを採用している

Flutter for Web

iOS/AndroidだけでなくWebアプリの開発もサポートされている。
(2020年5月時点ではβ版)

umatomakun.hatenablog.com

Flutterでアプリを開発するには

Flutterを使ったアプリ開発の入門サイトがあるので、
こちらを利用してみましょう。

www.flutter-study.dev

Androidアプリを起動してみる

Flutter導入

プロジェクト作成

// プロジェクト作成 w/ Integration tests
$ flutter create --org com.umatoma --with-driver-test --no-pub flutter_training_app

// そのままだと依存解決に失敗するので調整
$ vim pubspec.yaml
- test: 1.6.2
+ test: ^1.9.4
$ flutter pub get

アプリ起動

// 利用可能なエミュレータ一覧を確認
$ flutter emulators
3 available emulators:

Pixel_3a_API_28     • Pixel 3a API 28 • Google • android
TEST                • TEST            •        • android
apple_ios_simulator • iOS Simulator   • Apple  • ios
...

$ flutter emulators --launch Pixel_3a_API_28
$ flutter run

テスト実行

// Unit tests 実行
//   ファイル名が *_test.dart となっているものをテストファイルと認識する  
$ flutter test test/
  00:06 +1: All tests passed!

// Integration tests 実行
//   test_driver ディレクトリに target と同期したファイル名のテストが実行される
//   e.g. main.dart >> test_driver/main_test.dart
$ flutter drive --target=./lib/main.dart
Using device AOSP on IA Emulator.
Starting application: ./lib/main.dart
Installing build/app/outputs/apk/app.apk...                         4.3s
Running Gradle task 'assembleDebug'...
Running Gradle task 'assembleDebug'... Done                        13.2s
✓ Built build/app/outputs/apk/debug/app-debug.apk.
...
00:00 +0: end-to-end test (setUpAll)

...
00:01 +0: end-to-end test tap on the floating action button; verify counter

00:01 +1: end-to-end test (tearDownAll)

00:01 +1: All tests passed!

Stopping application instance.

リリース用ビルド作成

// keystore 作成
$ keytool -genkey -v -keystore ./key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
// 署名用の設定を追加
$ vim android/app/build.gradle
// apk 作成
$ flutter build apk
You are building a fat APK that includes binaries for android-arm, android-arm64, android-x64.
...
Removed unused resources: Binary resource data reduced from 37KB to 32KB: Removed 14%
Running Gradle task 'assembleRelease'...
Running Gradle task 'assembleRelease'... Done                      14.9s
✓ Built build/app/outputs/apk/release/app-release.apk (15.6MB).

Webアプリを起動してみる

Webアプリ機能有効化

// 現時点ではbeta版らしい
$ flutter channel beta
$ flutter config --enable-web

// 作成済みのプロジェクトにWebアプリのコードを追加する場合
$ flutter create .

Recreating project ....
  web/index.html (created)
  web/manifest.json (created)
  web/icons/Icon-192.png (created)
  web/icons/Icon-512.png (created)
Wrote 7 files.
...

アプリ起動

$ flutter run -d chrome

リリース用ビルド作成

$ flutter build web

状態管理はどうやるのか

State

  • Widgetは常にImmutableである
  • 状態管理はStateで管理し、変更に応じてWidgetを再生成する
    • setState() をCallすることで、状態が変更されたことを通知する
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        onPressed: () {
          setState(() {
            _count++;
          });
        },
        child: Text('Increment: $_count'),
      ),
    );
  }
}

Provider

  • provider パッケージを使うことで、複数Widgetを跨いだ状態の管理が簡単に行える
    • Provider/Consumer が Pub/Sub の関係
    • 任意のWidgetに状態を共有できる
void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CountModel(),
    child: MaterialApp(
      home: Column(
        children: <Widget>[
          CounterText(),
          CounterButton(),
        ],
      ),
    ),
  ));
}

class CountModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Consumer を通して Provider に渡したモデルにアクセスできる
    return Consumer<CountModel>(
      builder: (context, countModel, child) => Text('Count: ${countModel.count}'),
    );
  }
}

class CounterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // データを参照する必要がない場合は Provider.of で listen:false でもOK
    final countModel = Provider.of<CountModel>(context, listen: false);
    return RaisedButton(
      onPressed: () => countModel.increment(),
      child: Text('Increment'),
    );
  }
}

ProviderはInheritedWidgetを使いやすくしたものである。
Providerの仕組みに関してはこちら↓で紹介

umatomakun.hatenablog.com

Widget

FlutterではUIを構築するための様々なWidgetが提供されている。
様々なWidgetを使ってみたい場合はこちら↓から

umatomakun.hatenablog.com umatomakun.hatenablog.com

感想

  • 軽く使ってみた感じ、非常に簡単に環境構築〜アプリ作成まで行えるのが良い
  • エディタを含め開発環境周りもそれなりに整っており、全体としての完成度が非常に高い印象
  • Reactに影響されてそうな部分が多く、Reactユーザーにとっては参入障壁が低いはず
  • テスト周りもデフォルトで Unit・Widget・Integration テストを行える環境が整備されていて良い
  • バックエンドにDartを採用できれば、アプリ〜バックエンドまで少人数かつ1チームで全てを網羅することで出来る可能性も
    • これができれば、規模が拡大してもFeatureチーム単位で組織を分割しやすくなる?
  • ただし、各プラットフォームに関する知識をもっていないと、ハマった時に大変そう...

参考情報