はじめに

最近,僕の周りで話題のFlutterを触ってみました。
Write Your First Flutter Appの内容をそのままやっていきます。

前提

  • Dart, Flutterはインストール済
  • 開発環境は作成済(IntelliJを使用)
  • iOSで実行を確認

Step1: Hello World

とりあえず,Flutterでアプリ作成します。

$ flutter create first_app

このコマンドでFlutterのアプリが作成されます。
変更するのは,基本lib/main.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: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          child: new Text('Hello World'),
        ),
      ),
    );
  }
}

よくわからないけど,とりあえずRun。

mainメソッドの=>は1行で関数を書くときに使える記法。MyAppインスタンスを渡してアプリを実行してるっぽいです。
たぶん,Javaとか読める人はなんとなくやってることわかる気がする。。
全体のViewをStatelessWidgetが司っていて,Viewをbuildしてます。MaterialAppがtitle, home, bodyプロパティを持っていてそれぞれに任意のview widgetを渡すことで画面を描画しているようです。

Dart,始めてでもなかなか読みやすいですね。

Step2: 外部のパッケージを使う

パッケージを導入してみます。FlutterのパッケージはFlutter Packagesで探せます。ここでは,english_wordsを導入しています。
Flutterではパッケージをpubspec.yamlファイルで管理します。

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.0
  english_words: ^3.1.0

一番下にenglish_wordsを追加。
追加するとIntelliJの上の方にPackages getというのがでるのでそれを押せばpackageが導入できます。
lib/main.dartを以下のように書き換えます。

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          // child: new Text('Hello World'),
          child: new Text(wordPair.asPascalCase),
        ),
      ),
    );
  }
}

これで,bodyに表示される文字列が変わるリロードする度に変わります。
非常に簡単ですね。

Step3: Stateful Widgetを追加する

Stateless Widgetはその名の通り,動きがないウィジェットです。
Stateful Widgetのライフタイムの中で変化する状態を保持します。Stateful Widgetは少なくとも2つのクラスを使用します。

  1. Stateクラス
  2. StatefulWidgetクラス

StatefulWidgetクラスはStateクラスのインスタンスを生成するクラスで,それ自体では変化することはないですが,StateクラスはWidgetのライフタイムを保持しています。
ここでは,RandomWordsStateクラスを生成するRandomWordsというStatefulクラスを追加してみます。RandomWordsStateクラスは,Widgetに表示されたお気に入りワードのペアを保持します。

まず,stateful RandomWordsウィジェットをmain.dartに追加します。
MyAppクラスの外に記述することに注意!

class RandomWords extends StatefulWidget {
  @override
  State<StatefulWidget> createState();
}

次に,RandomWordsStateクラスを追加します。全てのアプリのコードは,RandomWordsウィジェットの状態を保持するこのクラスに属します。このクラスが具体的にすることは,

  • ユーザのスクロールによって生成されるワードのペアを保持する
  • ユーザがハートアイコンをトグルすることでリストに追加/削除されるお気に入りワードのペアを保持する

の2つです。

そして,このクラスを追加すると,buildメソッドがないよと怒られるので,buildメソッドを追加します。このbuildメソッドでは,ワードペアの生成を行います。
MyAppクラスからRandomWordsStateクラスへワード生成のコードを移しておきます。buildメソッドは以下のようになります。

class RandomWords extends StatefulWidget {
  @override
  createState() => new RandomWordsState();
}

MyAppの方のワード生成のコードは削除しておきます。

class RandomWordsState extends State<RandomWords> {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return (new Text(wordPair.asPascalCase));
  }
}

ここまでで一旦実行すると,Step2と同じ用にリロードする度に中央のワードが変更されるという動作を確認できると思います。

Step4: 無限Scroll Viewを作成する

このステップでは,RandomWordsStateクラスを拡張して,ワードのリストを生成して表示します。
ListViewのbuilderファクトリーコンストラクタは,要求に応じてリストビューを作成します。

まず,提案されたワードを保持する_suggestionsリストをRandomWordsStateクラスに追加します。
ちなみに,変数の頭のアンダースコア(_)は,Dartにおいてプライバシー変数であることを意味します。
更に,biggerFont変数もフォントサイズを大きくするために定義しておきます。

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return (new Text(wordPair.asPascalCase));
  }
}

次に,_buildSuggestions()関数を追加します。このメソッドは提案されたワードを生成するリストビューを作成する関数となります。
ListViewクラスはビルダープロパティitemBuilder(ファクトリビルダーとコールバック関数)
BuildContextとイテレータ i がこの関数に渡されるパラメータとなります。イテレータは0から始まり,ワードを提案する度に関数が呼ばれてインクリメントされます。

  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // itemBuilderのコールバックはワードが提案される度に呼ばれ,ListTitleの行にそれを配置する
      itemBuilder: (context, i) {
        if (i.isOdd) return new Divider();
        final index = i ~/ 2;
        if (index >= _suggestions.length) {
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }
}

このままでは動きません。なぜなら,_buildSuggestions関数は_buildRowをワードペア毎に読んでいるからです。この関数は,リストタイトルに次のステップで注目する行生成のための関数で,新しいペアを表示する役割を担います。

_buildRow関数は,以下のように実装されます。

class RandomWordsState extends State<RandomWords> {
  ...

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase, style: _biggerFont,
      ),
    );
  }
}

そして,RandomWordsStateを以下のように変更します。

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }
  ...

}

最後に,MyAppクラスを変更します。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new RandomWords(),
    );
  }
}

これで,ユーザがスクロールすると自動でワードを生成して表示するリストビューが完成です。

Step5: インタラクティブなアプリを作成する

次に,各行にお気に入りボタンを追加します。そのためには,ユーザがタップできるようにしないといけませんね。

まず,RandomWordsStateに_savedセットを追加します。

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _saved = new Set<WordPair>();

  final _biggerFont = const TextStyle(fontSize: 18.0);
 ...

}

次に,_buildRow関数にお気に入りに追加されているかを確認するためのalreadySavedを追加し,ハート型のアイコンをリストに表示します。

class RandomWordsState extends State<RandomWords> {
  ...

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return new ListTile(
      title: new Text(
        pair.asPascalCase, style: _biggerFont,
      ),
      trailing: new Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
    );
}

ここまででリロードすると,リストの行の右端にハート型のアイコンが表示されているはずです。

最後に,タップできるように_buildRow関数を変更します。
buildRow関数の完成形は以下。

Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return new ListTile(
      title: new Text(
        pair.asPascalCase, style: _biggerFont,
      ),
      trailing: new Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
           if (alreadySaved) {
             _saved.remove(pair);
           } else {
             _saved.add(pair);
           }
        });
      },
    );
  }

Step6: 新しいScreenに誘導する

前のステップでお気に入りボタンを追加したので,このステップではお気に入りリストの画面を追加して,画面遷移をやってみます。

Flutterでは,Navigatorが画面遷移はアプリのルートを含むstackを管理します。

  • Navigatorのスタックにルートがプッシュされると,そのルートに画面が更新される
  • NavigatorのSタックからポップされると,前のルートに戻る

では,実際に追加して遷移処理してみます。
まず,RandomWordsStateのbuildメソッドでAppBarにリストアイコンを追加します。ユーザがこれをクリックすると,お気に入りが追加要素をもった新しいルートがNavigatorにプッシュされ,アイコンを表示します。

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[
          new IconButton(icon: new Icond(Icons.list), onPressed: _pushSaved)
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

次に,RandomWordsStateクラスに_pushSaved()関数を追加します。

class RandomWordsState extends State<RandomWords> {
  ...
  void _pushSaved() {}
}

ここでアプリをホットリロードしてみると,AppBarにリストアイコンが追加されていることがわかります。

せっかくアイコンを追加したので,タップしたときの処理を追加します。
ユーザがAppBarのリストアイコンをタップした際に,ルートを生成してNavigatorスタックにプッシュします。このアクションで,新しいルートを表示して画面を変更します。

新しいページの内容は,無名関数MaterialPageRouteのbuilderプロパティでビルドされます。

下のように,NavigatorのスタックにルートをプッシュするNavigator.pushを追加し,MaterialPageRouteとそのビルダーを追加します。ここでは,ListTileの行を生成するコードを追加しておきます。
ListTileのdivideTiles()メソッドが各ListTileの間に水平のスペースを入れてくれます。
devided変数が最後の行を保持していて,これは簡易関数toList()によってリストに変換されます。

  void _pushSaved() {
    Navigator.of(context).push(
      new MaterialPageRoute(
          builder: (context) {
            final tiles = _saved.map(
                (pair) {
                  return new ListTile(
                    title: new Text(
                      pair.asPascalCase,
                      style: _biggerFont,
                    ),
                  );
                }
            );
            final divided = ListTile.divideTiles(
                tiles: tiles,
                context: context
            ).toList();
          })
    );
  }

ビルダープロパティはScaffoldを返します。Scaffoldは新しいルートのためのAppBar,"SavedSuggestions"を持っています。新しいルートのbodyはListTiles行を含むListViewで構成されていて,各行はdividerによって分割されています。

void _pushSaved() {
    Navigator.of(context).push(
      new MaterialPageRoute(
          builder: (context) {
            final tiles = _saved.map(
                (pair) {
                  return new ListTile(
                    title: new Text(
                      pair.asPascalCase,
                      style: _biggerFont,
                    ),
                  );
                }
            );
            final divided = ListTile.divideTiles(
                tiles: tiles,
                context: context
            ).toList();

            return new Scaffold(
                appBar: new AppBar(
                  title: new Text('Saved Suggestions'),
                ),
                body: new ListView(children: divided)
            );
          }
      )
    );
  }

ホットリロードし,適当にお気に入りを追加して画面遷移すると,新しい画面が表示されるはずです。ちなみに,return new Scaffold以下を書かないと,次の画面が表示されず真っ黒な画面が表示されます。

ちなみに,前の画面に戻るためにわざわざNavigator.popを実装する必要はありません。勝手に新しい画面のAppBarに自動で"戻る"ボタンが追加されているはずです。

Step7: テーマを使ってUIを変更する

最後のステップですが,アプリのテーマで遊んでみます。

アプリのテーマはThemeDataクラスをいじることで簡単に変更することができます。今はデフォルトテーマを使っているので,白をベースとした色に変更してみます。

MyAppを次のように変更します。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      theme: new ThemeData(
        primaryColor: Colors.amber
      ),
      home: new RandomWords(),
    );
  }
}

ホットリロードすると色がかわることがわかります。

MaterialライブラリのColorsクラスはたくさんの色の定数を提供していますので,いろんな色に変更して遊んでみると良いと思います。ホットリロードしながら変更すると簡単に確認しながら変更できます。

終わりに

ここまでやった感想としては,Flutterは非常によく作られてるなと思いました。
dartも始めてでも直感的でわかりやすいし,学習コストは低そうという印象。
本気でいろいろやり始めると壁があるのかもしれませんが,個人的にはやってみる価値はあるかなという感じです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.