Skip to content

Instantly share code, notes, and snippets.

@najeira
Last active September 6, 2018 13:39
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save najeira/4ea8c4ca93570dfb1468fae5c8d6c616 to your computer and use it in GitHub Desktop.
Save najeira/4ea8c4ca93570dfb1468fae5c8d6c616 to your computer and use it in GitHub Desktop.
Flutter codelab

Flutterコードラボ

Flutterを使って、簡単なアプリを開発してみましょう!

準備

インストール

Flutterのインストールは、公式ドキュメントに従って実施してください。

Android / iOS の開発環境は、今回使いたい方どちらか一方の準備でOKです。

開発環境

公式プラグインが以下の環境向けに用意されています。

  • Android Studio
  • Visual Studio Code
  • IntelliJ IDEA

上記以外でも、お好きなエディタ+コマンドだけでも開発可能です。

※本ドキュメントは、IntelliJ IDEA もしくは Android Studio に Flutter プラグインを入れた環境を対象に記述されています(一部対応するコマンドを併記)。なお、他の環境についても、手順に大きな違いはありません。

確認

インストールが完了後 flutter doctor コマンドを実行すると、初回に関連ファイルのダウンロード&セットアップが実行されます。

その後、以下のように表示されます。

myhome: flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v0.5.4, on Mac OS X 10.12.6 16G1314, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 27.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
[✓] Android Studio (version 3.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2018.1)
[✓] VS Code (version 1.24.0)
[✓] Connected devices (3 available)

Flutter と Android toolchain か iOS toolchain の使う方がチェックマークになっていればOKです。

今日の流れ

  • プロジェクト作成
  • ホットリロード
  • リストビュー
  • HTTP通信
  • 状態を持ってみる
  • アニメーション

行けるところまででOK!

プロジェクト作成

まずはプロジェクトの雛形を作成します。

Android Studio: File > New Flutter Project から進み、アプリ名(myapp)を入力します。コマンドの場合は flutter create myapp と実行します(myappはアプリ名)。

myappというディレクトリが作成され、その下にファイルやディレクトリが作成されます。今回のコードラボで主に使うのは lib/main.dartpubspec.yaml です。

詳細や、他の環境は以下を参照: https://flutter.io/get-started/test-drive/#androidstudio

実行

アプリの雛形を作成した時点で、最低限の実行できるコードが生成されています。まずは実行してみましょう。

ツールバー(多くの場合は右上に表示されています)から、実行対象を選択します。対象がない場合は「Open Android Emulator」「Open iOS Simulator」から起動します。Androidのエミュレータがセットアップされていない場合は、AVDを作成してください。

img

AVD作成について: https://developer.android.com/studio/run/managing-avds

エミュレータ起動後、それを選んだ状態で、右にある再生ボタンを押すと、起動が開始されます。

初回の起動は時間が少しかかります。

画面の中央に数字があり、右下のボタンを押すとカウントアップするデモが表示されたでしょうか?

アプリが起動したら、起動したままで、次のステップへ進みます。

※エディタとエミュレータが同時に見えるように横並びにしておくと、開発中のホットリロードが分かりやすいと思います

開発

ステップ1

アプリを作成したディレクトリにある lib の下に、Dartのファイルが作成されています。lib/main.dart を開いてみましょう。

以下のようになっています(コメントを省略して表示しています)。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}

main 関数は、起動時に最初に呼ばれるエントリ・ポイントです。そこで runApp 関数に Widget を渡してアプリを開始しています。

MyApp は状態を持たない StatelessWidgetbuild メソッドで Widget を return して UI を定義します。

MyHomePage は状態のある StatefulWidget で、その状態である _MyHomePageState を生成します。_MyHomePageStateState を継承し、状態を持ち、なおかつ build メソッドで Widget を return して UI を定義しています(このように Widget と State が分かれています)。

このように Flutter では、状態のない StatelessWidget と、状態のある StatefulWidget を使いわけながら build メソッドで UI の定義(Widget ツリー)を return することで UI を作っていきます。

Textの変更

下の方にある Text widget の中身の文字を変えてみましょう。

'You have pushed the button this many times' となっている箇所を、別の文字列にしてみましょう(例: 'こんにちはFlutter!')。

変更したら「ホットリロード」してみましょう。

アプリがエミュレータで実行中であれば、再生ボタンの右の方にある稲妻のボタンが有効になっています。このボタンを押すと「ホットリロード」が実行されます。ボタンを押してからエミュレータを見ると Flutter の「ホットリロード」によって、内容が変わっていることが確認できます。

コマンドラインの場合は r キーでホットリロードされます。

エミュレータを見ると、文字が変わっていることが確認できると思います(数字が維持されたまま!)。

カウント処理

テキスト(定数・変数)だけでなく、ロジックも変更&ホットリロードできます。

_incrementCounter というメソッド内の、カウンタをインクリメントしている処理を、デクリメントに変更してみましょう。

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

ホットリロードし、ボタンを押してみてください。数値が減少するように動作が変わりましたね。

ステップ2

続いて、リストビューの実装をしてみます。

_MyHomePageState の中身をごっそり入れ替えます。build 内を以下のように ListView クラスを使った実装に置き換えてみましょう。

ListViewchildren は Widget の List を受け取ります。Widget であれば、どのような Widget を入れてもかまいません。ここの実装で使っている ListTile は「よくあるリストの行」を実装した便利な Widget です。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'App DOJO',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'リストビュー'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new ListView(
        children: <Widget>[
          new ListTile(title: new Text('A')),
          new ListTile(title: new Text('B')),
          new ListTile(title: new Text('C')),
          new ListTile(title: new Text('D')),
          new ListTile(title: new Text('E')),
          new ListTile(title: new Text('F')),
          new ListTile(title: new Text('G')),
          new ListTile(title: new Text('H')),
          new ListTile(title: new Text('I')),
          new ListTile(title: new Text('J')),
          new ListTile(title: new Text('K')),
          new ListTile(title: new Text('L')),
          new ListTile(title: new Text('M')),
          new ListTile(title: new Text('N')),
          new ListTile(title: new Text('O')),
          new ListTile(title: new Text('P')),
          new ListTile(title: new Text('Q')),
          new ListTile(title: new Text('R')),
          new ListTile(title: new Text('S')),
          new ListTile(title: new Text('T')),
          new ListTile(title: new Text('U')),
          new ListTile(title: new Text('V')),
          new ListTile(title: new Text('W')),
          new ListTile(title: new Text('X')),
          new ListTile(title: new Text('Y')),
          new ListTile(title: new Text('Z')),
        ],
      ),
    );
  }
}

img

ホットリロード後、リストが表示されたでしょうか?

※もしホットリロードでうまくいかない場合や何らかのエラーが表示された場合は「ホットリスタート」を行ってください。プラグインの場合はツールバーの「再生ボタン」を、コマンドの場合は Shift+R でホットリスタートできます。

ステップ3

HTTP 通信を使ってみましょう。

Dart の http パッケージを使うために pubspec.yaml というファイルに依存ライブラリとして追加します。

myapp ディレクトリにある pubspec.yaml というファイルを開き dependencieshttp を追加します。

dependencies:
  flutter:
    sdk: flutter
  http: any

any とあるのは、バージョンは「何でもよい」という指定です。

pubspec.yaml を更新したあとは、依存関係を更新するための処理が必要です。

まずアプリを停止してください。ツールバーの停止ボタン、コマンドは Q キーで停止できます。 (パッケージの追加の場合は「ホットリロード」が効かないため、アプリを停止しています)

IDE + プラグインで開発している場合、pubspec.yaml を開いてみて、エディタ画面上部に表示される「Packages get」を押してください。 コマンドの場合は myapp ディレクトリで flutter packages get と実行してください。

この処理が完了すると依存関係に http パッケージが追加されています。

import

import に http を追加します。

import 'package:flutter/material.dart';

// 以下の行を追加
import 'package:http/http.dart' as http;

Tips: as http

as http とあるのは import したパッケージのプレフィックスをつけるためです。

プレフィックスをつけない場合は、そのパッケージが公開している名前を直接使うことになります。ここでは http パッケージのものを使っていることが分かりやすいようにプレフィックスをつけます。

プレフィックスなしだと get 関数が何なのか分かりにくい:
import 'package:http/http.dart';

var resp = get('https://example.com')
プレフィックスありのほうが分かりやすい:
import 'package:http/http.dart' as http;

var resp = http.get('https://example.com')

http.get

さて http パッケージを使ってみましょう。以下のコードでは _fetch メソッド内で HTTP 通信によってデータを取得してきて、そのデータをそのまま画面に表示しています。_MyHomePageStateの中を以下のように実装してみましょう。

class _MyHomePageState extends State<MyHomePage> {
  String _data = '';

  @override
  void initState() {
    super.initState();
    _fetch();
  }
  
  void _fetch() async {
    var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
    setState(() {
      _data = resp.body;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Text(_data),
      ),
    );
  }
}

img

initStateState が作られたときに1度だけ呼び出されるメソッドで、通常はここで初期処理を行います。

HTTP 通信で得られたデータをインスタンス変数 _data に代入しています。これを setState というメソッドに渡した無名関数の中で実行しています。setState は Flutter の State が提供しているもので、Flutter に「状態が変わった」ことを通知します。その結果、Flutter は最新の状態で該当の Widget を描画します。状態を変更した場合は、setState を使うことで再描画させましょう。

また _fetch メソッドには async キーワードがついています。これは非同期処理を行うメソッドを示しています。async メソッド内では await キーワードによって、非同期処理の結果を待つことができます。http.get 関数の戻り値は Future という「いずれ結果が得られる」非同期処理における結果オブジェクトで、この実装ではキーワード await によって Future が完了するのを待って、結果を取り出しています。

async / await を使わない場合は以下のよう then メソッドを使ってコールバック方式で書くことができます。

void _fetch() {
  var future = http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
  
  future.then((http.Response resp) {
    setState(() {
      _data = resp.body;
    });
  });
}

ステップ4

HTTP で取得したデータは JSON 形式になっています。これを変換して Dart で扱いやすい形式にしましょう。

import

Dart 標準の convert パッケージを import します。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

convert

json.decode 関数によって、データを List<dynamic> に変換します。今回のデータでは、リスト内には文字列しかないので、さらに List<String> に変換しています。また、そのリストを ListView を使ってリスト表示しています。

class _MyHomePageState extends State<MyHomePage> {
  List<String> fruits = <String>[];

  @override
  void initState() {
    super.initState();
    _fetch();
  }
  
  void _fetch() async {
    var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
    setState(() {
      List<dynamic> list = json.decode(resp.body);
      fruits = list.map<String>((dynamic elem) {
        return elem as String;
      }).toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new ListView(
        children: fruits.map<Widget>((String fruit) {
          return new ListTile(title: new Text(fruit));
        }).toList(),
      ),
    );
  }
}

img

ステップ5

リストの各行に「お気に入り」ボタンを付けてみましょう。

お気に入りかどうかは、インスタンス変数 favorites で管理します。

ListTiletrailing プロパティは、リスト行の後ろ(右)側の Widget です。ここに IconButton を使って、タップできるボタンを配置します。

IconButtononPressed で「お気に入り」の状態を変更する処理を書きます。

class _MyHomePageState extends State<MyHomePage> {
  List<String> fruits = <String>[];
  
  // お気に入りかどうかの状態を管理するMap
  Map<String, bool> favorites = <String, bool>{};

  @override
  void initState() {
    super.initState();
    _fetch();
  }
  
  void _fetch() async {
    var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
    List<dynamic> list = json.decode(resp.body);
    setState(() {
      fruits = list.map<String>((dynamic elem) {
        return elem as String;
      }).toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new ListView(
        children: fruits.map<Widget>((String fruit) {
          // お気に入りかどうか
          bool fav = favorites[fruit] ?? false;
          
          return new ListTile(
            title: new Text(fruit),
            
            // お気に入りボタン
            trailing: new IconButton(
              // アイコンはお気に入りかどうかで見た目を変える
              icon: new Icon(
                fav ? Icons.favorite : Icons.favorite_border,
                color: fav ? Colors.red : Colors.grey,
              ),
              
              // タップされたらお気に入りの状態を変える
              onPressed: () {
                bool fav = favorites[fruit] ?? false;
                setState(() {
                  favorites[fruit] = !fav;
                });
              },
            ),
          );
        }).toList(),
      ),
    );
  }
}

img

Map の要素へのアクセス favorites[fruit] で、要素がない場合は null を返します。?? は、左辺が null の場合に右辺を返す演算子です。このため Map に要素がない場合は false (お気に入りではない)となるようにしています。

ステップ6

お気に入りボタンにアニメーションをつけてみましょう。

FlutterではアニメーションもWidgetです。

アニメーションするお気に入りボタンのWidget FavIcon を以下のように実装します(ファイルの末尾に追加しましょう)。

class FavIcon extends StatefulWidget {
  FavIcon({Key key, this.fav}) : super(key: key);
  
  final bool fav;
  
  @override
  State<StatefulWidget> createState() {
    return new FavIconState();
  }
}

class FavIconState extends State<FavIcon>
  with SingleTickerProviderStateMixin {
  
  AnimationController controller;
  
  @override
  void initState() {
    super.initState();
    controller = new AnimationController(
      value: 1.0,
      vsync: this,
      duration: const Duration(milliseconds: 700),
    );
  }
  
  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget(FavIcon oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.fav != widget.fav) {
      controller.forward(from: 0.0);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return new ScaleTransition(
      scale: new CurvedAnimation(
        parent: controller,
        curve: Curves.bounceOut,
      ),
      child: new Icon(
        widget.fav ? Icons.favorite : Icons.favorite_border,
        color: widget.fav ? Colors.red : Colors.grey,
      ),
    );
  }
}

また、これを使うリストビュー側の実装として _MyHomePageStatebuild メソッドは以下のようになります。IconButton の中身を変えて FavIcon を使うようにしています。

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new ListView(
        children: fruits.map<Widget>((String fruit) {
          bool fav = favorites[fruit] ?? false;
          
          return new ListTile(
            title: new Text(fruit),
            
            trailing: new IconButton(
              // FavIconにお気に入りの状態を渡してWidgetを作る
              icon: new FavIcon(fav: fav),
              
              onPressed: () {
                bool fav = favorites[fruit] ?? false;
                setState(() {
                  favorites[fruit] = !fav;
                });
              },
            ),
          );
        }).toList(),
      ),
    );
  }

FavIconState を見てみましょう。AnimationController がアニメーションにおける値の変化を管理してくれます。これをインスタンス変数に持っています。例によって initState で初期化をしています。また dispose で後処理をしています。

また FavIconStatewith SingleTickerProviderStateMixin となっています。これは、アニメーションのための Tikcer を提供する機能を State へ "Mixin" して持たせるためです。

didUpdateWidget はこの State に関連する Widget (ここでは FavIcon)の設定が変更されたときに呼び出されます。つまり変数 bool fav が変更された場合です。このときに AnimationController を値 0.0 から forward させているので、お気に入り状態が変わったときにアニメーションが開始します。

build 内ではアニメーション対象である IconScaleTransition でラップしています。CurvedAnimation はアニメーションの変化を定義するクラスで、これに AnimationController を渡しているため、そのコントローラーの変化によって最終的に Icon のサイズが変化するようになっています。

さて、ボタンを押したときに、大きさが変化するアニメーションが確認できたでしょうか?

完了!

このコードラボは以上です。

Flutterのいくつかの要素は確認できたと思います。

  • プロジェクト作成
  • ホットリロード
  • リストビュー
  • HTTP通信
  • 状態を持ってみる
  • アニメーション

お疲れ様でした!

[
"あけび",
"アサイー",
"アセロラ",
"アボカド",
"あんず",
"いちご",
"いちじく",
"いよかん",
"梅",
"オリーブ",
"オレンジ",
"柿",
"かぼす",
"ガラナ",
"かりん",
"キウイ",
"金柑",
"グァバ",
"クランベリー",
"グレープフルーツ",
"ココナッツ",
"さくらんぼ",
"ざくろ",
"シークワーサー",
"スイカ",
"すだち",
"すもも",
"デコポン",
"ドリアン",
"梨",
"なつめ",
"ネクタリン",
"パイナップル",
"ハスカップ",
"はっさく",
"バナナ",
"パパイヤ",
"びわ",
"ぶどう",
"ブルーベリー",
"プルーン",
"マスカット",
"マンゴー",
"みかん",
"メロン",
"桃",
"ゆず",
"ライチ",
"ライム",
"ラズベリー",
"りんご",
"レモン"
]
@najeira
Copy link
Author

najeira commented Jul 6, 2018

更新用メモ:

  • インストールまわりを充実させる
    • Android Studio / Xcode のエミュレータまわりなど
  • ホットリロードとホットリスタートの使い分け
    • buildだけでよいのか、initStateも再実行したいのか
  • 各種スクショを増やす?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment