Flutter tutorialで簡単なアプリを作る

Flutterの環境構築が終わったので、早速Tutorialをやっていきます。
記事はVSCodeのていで書いているので、他のアプリの方は適宜読み替えをどうぞ😊

お題は'Startup Name Generator'。要件としてはこんな感じ。

  • スタートアップ企業の名前が並ぶ
  • ユーザは名前を選択/選択解除して一番いい名前を選べる
  • 非同期的に名前を生成
  • スクロールされるとさらに名前を生成&スクロールは限界なくできる

このパートで学べること

  • iOSAndroidで自然に見えるFlutter appの書き方
  • Flutter appの基本構造
  • パッケージを探して見つけて拡張する方法
  • ホットリロードの使い方
  • ステートを持ったウィジェットの作り方
  • 終わりなく非同期的に読み込まれるリストの作り方

Step1. Flutter appをつくる

これは環境構築の中でやったところ。

$ flutter create hello_flutter

lib/main.dartを主に編集していくよ、とのこと。ふむふむ。

  1. lib/main.dartをすべて消して下記のコードに書き換え
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

(Dartシンタックスハイライトきくんですね。はてなさんすごい…!)

Tipsとして、インデントがくずれた場合は「Alt+Shift+F」で整形できるよ、と紹介されています。至れり尽くせり…!

2. エミュレータを起動してホットリロードを確認 「F5」でエミュレータを起動、「Shift+Cmd+F5」でホットリロードが行えます。
(アプリ実行中は保存でもホットリロードできるとのことなのだけど、うまくいかない…これも後回し案件です📝)

さて、「Hello, World」の文字が表示されました。わーい!😊

Step2. 外部パッケージを使う

名前を生成する為にenglish_wordsという外部ライブラリを導入します!

外部ライブラリはPub siteからDLできるよ、とのこと。RubyでいうRubyGemsみたいな感じでしょうか。

  1. pubspec.yamlenglish_wordsを加える
    これがGemfile的なものっぽい。
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  english_words: ^3.1.0

2. flutter packages getの実行 とあるのですが、保存したら勝手に走った…!!!
あまりに早すぎて(2.3s)本当にちゃんとgetできたの?と心配になりますが、pubspec.lock(Gemfile.lock的なものっぽい)を見るとenglish_wordsの項目が増えているので追加できているようです!

3. packageのimport
lib/main.dartenglish_wordsをimportします。

import 'package:english_words/english_words.dart'

4. English Wordsパッケージを使って文字列を表示する Hello, Worldの代わりに今importしたパッケージを使ってランダムに生成した文字列を表示します。

Widget build(BuildContext context) {
  final wordPair = WordPair.random();
  return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
      appBar: AppBar(
        title: Text('Welcome to Flutter'),
      ),
      body: Center(
        child: Text(wordPair.asPascalCase),
      ),
    ),
  );
}

ホットリロードすると、(保存しなくても画面更新するだけで)ランダムな文字列が表示されるようになりました!

Step3. Statefulなウィジェットを加える

statelessウィジェットは不変で、プロパティを変更できない。すべての値がfinalとなります。
statefulウィジェットウィジェットのライフタイム中変化する可能性のあるstateを保持します。

statefulなウィジェットの実装には最低でも2つのclassが必要になります:

  1. インスタンスを作るStatefulWidgetクラス
  2. Stateクラス

StatefuleWidgetクラスはimmutable(不変)ですが、Stateクラスはウィジェットのライフタイムの間中持続します(?)

RandomWordsというstatefulなウィジェットを加えると、RandomWordsStateというStateクラスが作られます。
すると今つくっているMyAppのstatelessウィジェットの子としてRandomWordsを使えるようになるよ、とのこと。
読むだけじゃよくわからないのでやってみます。

  1. lib/main.dartの最下部に下記を追記
class RandomWordsState extends State<RandomWords> {
  // TODO Add build() method
}

Stateクラスがビルドメソッドを持たなくてIDEに怒られるけど、いったん放置。

2. statefulなRandomWordsウィジェットlib/main.dartに追記
RandomWordsウィジェットはこのRandomWordsのStateクラスを作る以外はほとんど何もしないとのこと。ふむふむ。immutableだからですかね。

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

3. 1で放置していたStateクラスにビルドメソッドを追加

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

4. MyAppクラスからStep2で追加したコードを削除してRandomWords()を呼ぶ

Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
      appBar: AppBar(
        title: Text('Welcome to Flutter'),
      ),
      body: Center(
        child: RandomWords(),
      ),
    ),
  );
}

5. アプリをリロード(ホットリロード)するとStep2と同じ挙動をします!

Step4. ずーっとスクロールできるListViewをつくる

最終段階!
RandomWordsStateクラスを拡張して、ユーザがスクロールすると永遠にリストが表示されるListViewウィジェットを作ります。
ListViewのビルダーファクトリーconstructorを使うとListViewを非同期的に作成できるそうです。

  1. _suggestionsリスト、_biggerFont変数をRandomWordsStateクラスに追加
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);
  //
}

Javaと違ってDart言語はpublicやprivateといった予約語を持たなくて、_をつけるとprivateになるそうです。A Tour of the Dart Language - Dart

2. _buildSuggestions()functionをRandomWordsStateクラスに追加

Widget _buildSuggestions() {
  return ListView.builder(
    padding: const EdgeInsets.all(16.0),
    itemBuilder: /*1*/ (context, i) {
      if (i.isOdd) return Divider(); /*2*/

      final index = i ~/2; /*3*/
      if (index >= _suggestions.length) {
        _suggestions.addAll(generateWordPairs().take(10)); /*4*/
      }
      return _buildRow(_suggestions[index]);
    }
  );
}

急にわけわからなくなった!!😨
ひとつひとつ見ていきましょう🤔

  • /*1*/
    itemBuilderコールバックは単語のペアを呼び出してListTile行に配置します。偶数行にはListTile行を、奇数列にはセパレータを配置します。

  • /*2*/
    /*1*/で触れたセパレータがこれですね。

  • /*3*/
    i ~/ 2Rubyでいうところのi/2 ((i/2).to_i)と同。
    単語のペア数からセパレータ数を引いた数を計算するのに使います。

  • /*4*/
    単語のペアが終端に達したら、さらに10個作ってsuggestions listに加えます。

3. _buildRowfunctionをRandomWordsStateに追加
これはwordPairとstyleを持ったListTileを作るだけ。

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

4. _buildSuggestions()を呼ぶようbuild()を修正
RandomWordsStateクラスで直接english_wordsライブラリを呼ぶのではなく、build()内で_buildSuggestions()を呼ぶようにします。
ふむふむ。ちょっとわかってきました。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Startup Name Generator'),
    ),
    body:_buildSuggestions(),
  );
}

5. MyAppクラスのtitle表示とhome(bodyみたいな感じ?)を修正してRandomWords()を呼び出すよう修正

Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Startup Name Generator',
    home: RandomWords(),
  );
}

6. ホットリロードすると、永遠にスクロールできるStartup Name Generatorの完成です!!!