入門!アトミックデザインに行ってきました

今日は@nrsさんのAtomicDesign入門に行ってきました!
振り返りと理解の確認のためにまとめを書かせて頂きます。

登壇資料はこちら。

speakerdeck.com

AtomicDesignとは

モジュール分割を基本としたデザインシステム(デザインの指針・デザインの共通言語)のこと。
モジュールは粒度によって下記の5つのステージに分けられる。

  • Atoms 原子: 最小のコンポーネント。ex. Logo, TextBox
  • Molecules 分子: ふたつ以上のAtomsを組み合わせたコンポーネント
    ex. HeaderLink(Logo + HeaderTitle), SearchBox(TextBox + PrimaryButton)
  • Organisms 生物: AtomsとMoleculesを組み合わせたコンポーネント
    ex. Header(HeaderLink + SearchBox)
  • Templates ページの基礎となるコンテンツ構造、ワイヤーフレームのこと。
    ex.Header + BreadCrampList + Contents
  • Pages テンプレートにデータを流し込んだもの

※ Templatesから「原子どこ行った??」状態なのは、Atom〜Organismsまではエンジニア/デザイナー向け、Templates以上は上流のお歴々と話をする為の道具となるからじゃないでしょうか、とのこと。
Atomとは、「UIとしての機能性を破壊しない最小要素」。
もしButtonは矩形とラベルにわけると、ラベルは押せないのでButtonとしての機能を失う
だから、Buttonが最小要素(Atom)。

なるほどわかりやすい!

けどやっぱり、そうシンプルな話でもない。
AtomicDesignは5階層に分けることでモジュールの乱立を防ぐことができることが重要なのだけど、いつでもAtom -> Molecules -> Organismsと分けられるわけではない。

たとえば、こんなページがあったとすると

Atoms: TableCell, PagingButton
Molecules: TableRow(TableCellの集まり), PagingMenu(PagingButtonの集まり)

Atoms: TabButton, TabContent
Molecules: TabMenu(TabButtonの集まり)

Organismはこうなる

Organisms: Table(TableRow + PagingMenu), Tab(TabMenu + TabContent)

でも、Organism同士であるTableとTabを足しても、これはTemplateとは言えない。
(HeaderとかFooterとかSideMenuとか足りない)
…おや?🤔

AtomicDesignは使えないのか?

わけではない。
AtomicDesignはデザインシステムである。

AtomicDesignの目的

  • ❌AtomicDesignに沿って作ること
  • ⭕デザイナーとエンジニアの協業のための共通基盤づくり
    -> 共通認識のもとで会話ができるようになること!

コンポーネント指向

すべてはコンポーネントであるという考え方

Webだと、image, textbox, buttonetc, ぜんぶ組み合わせで作れてしまう。
ゲームやネイティブアプリだと、全てコンポーネントを作る(必要がある)。

AtomicDesignとは粒度にルールを加えたコンポーネント指向のこと。

たとえばよくあるこんなケース

デザイナー💁「モーダルに『閉じる』ボタンをつけたい!」
コンポーネント化していないと
エンジニア「いくつあるのかわからないモーダル、全部修正してテストするのか😇」
コンポーネント化していると
エンジニア「Modalコンポーネントにつければいけるぜ😋」

AtomicDesignのメリット: 変更に強くなる

AtomicDesign実践入門

どこからつくる?🤔

  1. サンプルページを作る
    デザイナー💁「こんな感じにしよっか」
  2. Atomの切り出しの検討
  3. 組み合わせてMoleculesを作る
  4. 組みあわせてOrganismsを作る
    2〜4の工程をデザイナーとエンジニアを一緒にやれたらベスト!!
    (おすすめはホワイトボードだけど、Web上でやったりとかでもOK)
  5. 4まで共通認識が出揃ったら各デザイナー、エンジニアが実装
    ※ プロパティやイベントを持った最小要素(Atoms)を仮実装->Molecules->Organismsと進める
    (下位要素があれば上位要素もそれで作れるから)
    ※ 同じレイヤーの作業は別の人と重複することがあるので、作業分担は最初に要素を洗い出すことが大事
  6. Template
    Templateのコード化は必須ではないが、もしするなら全体のTemplateとコンテンツのTemplateで分けるのがおすすめ
  7. Organismsの粒度
    さっきの例のように、Atom -> Molecules -> Organismの三段階では足りなくなるパターンがある。
    → OrganismはOrganismをコンポジションする
    つまり、Organism + Organism = Organismもアリ

AtomicDesignは5階層であることに意味があるけれど、忠実にすることが難しさにつながるならその本質を見極めて取捨選択するべき

AtomicDesignは守ることを目的とした不変の法律ではなく、開発に関わる全ての人達の指標
目的はデザイナーとエンジニアの協業のための共通基盤づくり
指標があるとお互いがお互いをリスペクトして仕事しやすくなる。

まとめ

  • AtomicDesignとは
    粒度にルールを加えたコンポーネント指向のこと
  • AtomicDesignのメリット
    変更に強くなる、保守性があがること
  • AtomicDesignの目的
    デザイナーとエンジニアが共通認識のもとで会話・協業ができるようになること

デザイナー💁「これに従ってデザインするね」
エンジニア😍「やりやすい!」

こんな会話が生まれるためのデザインシステム、それがAtomicDesign。


AtomicDesignとは、を学ばせて頂くと同時に、終始「よりよくするには?」を根底に感じる1時間でした。
ありがとうございました!!

参考: 原典AtomicDesign

Flutterでgoogleにログインする

毎日Flutter。
今日はFirebaseで認証をします。

Pubはこれ(firebase_auth)ですね。

google_sign_inを追加する

まずはgoogle_sign_inを追加します。
pubspec.yamlに下記追記して、flutter packages get

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  google_sign_in: ^4.0.1+1 # new!
  firebase_storage:

main.dartimportを追記します。

import 'package:google_sign_in/google_sign_in.dart';

firebase_authを追加する

build.gradleclasspathを追記するのは前回やったので省略。
pubspec.yamlmain.dartにそれぞれ下記追記します。pubspec変更後はflutter packages get

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  google_sign_in: ^4.0.1+1
  firebase_auth: # new!
  firebase_storage:
import 'package:firebase_auth/firebase_auth.dart';

ログイン画面の追加

main.dartに下記を追記し、MyAppクラスからMyGoogleLoginPage()を呼び出すように変更します。

class MyGoogleLoginPage extends StatefulWidget {
  @override
  _MyGoogleLoginPageState createState() => _MyGoogleLoginPageState();
}

class _MyGoogleLoginPageState extends State<MyGoogleLoginPage>{
  final GoogleSignIn _googleSignIn = GoogleSignIn();
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Future<FirebaseUser> _handleSignIn() async {
    final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;

    final AuthCredential credential = GoogleAuthProvider.getCredential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );

    final FirebaseUser user = await _auth.signInWithCredential(credential);
    print("signed in " + user.displayName);
    return user;
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('none'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            SizedBox(height: 24.0),
            FlatButton(
              child: Text('SignIn'),
              onPressed: () => _handleSignIn()
                .then((FirebaseUser user) => print(user))
                .catchError((e) => print(e))
            ),
          ],
        ),
      ),
    );
  }
}

googleにログインができるようになりました!

FlutterでFirebaseのStorageを使う〜後編〜

昨日(FlutterでFirebaseのStorageを使う〜前編〜)の続きです。

AndroidXに対応する

さて、昨日、Firebaseを使うようbuild.gradleに設定を加えたところ、大量のエラーが出てビルドができなくなりました😨
エラーメッセージ中に「ここ見てね!」とリンクが記述されているので、素直に見に行きます。
ざっくりいうと、android.supportというライブラリが非推奨になって、AndroidXってライブラリに変更されたよ!とのこと。

migrateの方法なども記載されているのですが、自動でAndroidX化してくれる方法を載せてくれている人がいたのでその方法で対応します。

# android/gradle.propertiesに下記を追加
android.useAndroidX = true
android.enableJetifier = true

無事ビルドが通るようになりました!

firebase_storageをインポート

Pubでそれっぽいものを探すと、firebase_storageが出てきます。これだ!
image_pickerと違ってExampleは書かれていないので、手探りでやります💪

まずはpubspec.yamlfirebase_storageを追加。

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  firebase_storage: # new!

main.dartでインポートします。

import 'package:firebase_storage/firebase_storage.dart';

Flutter製チャットアプリを支える技術を参考にuploadImageメソッドを書きます。
(格好悪いけど、直接getImageから呼び出す)

class _MyHomePageState extends State<MyHomePage> {
  Future getImage() async {
    // (略)
    uploadImage(image);
  }
  Future<String> uploadImage(File file) async {
    int timestamp = DateTime.now().millisecondsSinceEpoch;
    String subDirectoryName = 'images';
    final StorageReference ref = FirebaseStorage()
        .ref()
        .child(subDirectoryName)
        .child('${timestamp}');
    final StorageUploadTask uploadTask = ref.putFile(
        file,
        StorageMetadata(
          contentType: "image/jpeg",
        ));
    StorageTaskSnapshot snapshot = await uploadTask.onComplete;
    if (snapshot.error == null) {
      return await snapshot.ref.getDownloadURL();
    } else {
      return 'Something goes wrong';
    }
  }
  // (略)
}

※ 実行の際にMissingPluginException(No implementation found for ...)というエラーがでる場合は、flutter clean & flutter packages get & flutter runでうまくいく場合があります。

さて、実行すると、firebase-auth is not linkedno auth token for requestといったメッセージが表示されますが、画像のアップロードに関してのエラーはないみたい!

わくわくしながらFirebaseのStorageを見てみると、タイムスタンプの名前のついた画像がアップロードされました!🎉

FlutterでFirebaseのStorageを使う〜前編〜

先日Flutterアプリにカメラボタンをつけましたが、写真を撮ったその後の処理がなかったので、今日は撮った写真をFirebaseにつないでアップロードしてみます。

ちなみにFirebaseとは、Googleが開発・提供しているmobile Backend as as Service(mBaaS)のこと。
通常Webアプリケーションを作る際にはサーバを立てたりAWSのようなサービスを利用してバックエンドの処理を行いますが、それをサーバレスで実現する仕組みのこと(だと思う)。
認証、画像のストレージ、DB、プッシュ通知やSMSメッセージの送信など、様々なサービスが提供されています。

今回はこの画像ストレージに画像を送信します。

Firebaseにログイン、プロジェクトの作成

Firebaseにログインします。持っていない場合はgoogleアカウントを使って作成します。
「コンソール」をクリックすると、「Firebaseへようこそ」メッセージが表示されます。
「+ プロジェクトを追加」カードをクリック。
プロジェクト名を求められるので適当に入力、プロジェクトIDと地域を必要に応じて変更します。

アプリにFirebaseを追加

今作成したプロジェクトを選択します。
iOSAndroid、Webが選択できる。ひとまずAndroidを選択します。
パッケージ名が求められるので、android/app/build.gradleにあるapplicationIdを入力して「アプリを登録」ボタンを押下。
google-services.jsonをダウンロードボタンが表示されるのでクリックしてダウンロード。android/appディレクトリに配置します。 android/build.gradleandroid/app/build.gradleにそれぞれ表示されているコードをコピーします。

google-services.jsonにはAPIキーが記載されているので、そのままgit pushしないよう注意。
やってしまいGitGurdianからワーニングがきたうっかりさんはわたしです。

Storageの設定変更

左ペインの「Storage」をクリック、「ルール」タブをクリックします。
今回認証は行わないので、誰でもread&writeできるようにルールを変更します。

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

というところで、続きはまた明日やっていきます。

Dart言語の3つのコンストラクタ

少しずつFlutterに慣れてきたところで、Dart言語も少しお勉強します。
今日はコンストラクタについて。

Dartには三種類のコンストラクタがあるそうです。

  1. 生成的コンストラク
  2. ファクトリ
  3. 定数コンストラク

DartPadというブラウザ上で実行できるREPLがあるので、入力しながら読むと理解がはかどりました。
なお、今日の参考ページはこちら -> Dartのコンストラクタについて - DevelopersIO

クラスの構成

整理すると難しくないのですが、慣れるまで違和感があったので一度整理します。

class Apple {
  static final String nickname = 'Ann'; // staticをつけるとクラス変数になる
  String color;               // staticをつけないとインスタンス変数。クラス変数と同名は指定できない
  Person() {                  // class名()でコンストラクタを記述
    this.color = 'Red';       // this.変数名(メソッド名も同様)でメンバにアクセス
  }
}

変数の宣言にはvar, const, finalも使えるし、String, intといった型も指定できます。
ただどちらかは指定しないとコンパイルエラーになるみたい。ふむ。

生成的コンストラクタ(Generative Constructors)

いわゆるふつうのコンストラクタ。

class Person {
  String name;
  Person() {
    this.name = 'Bob';
  }
}

void main() {
  Person foo = new Person();
  print(foo.name); // => 'Bob'
}

もちろん引数も取れます。

class Person {
  String name;
  Person(String name) {
    this.name = name;
  }
}

void main() {
  Person foo = new Person('Alice');
  print(foo.name); // => 'Alice'
}

Automatic field initialization

冗長な感じがするので、代入だけなら簡略して記述できます。

class Person {
  String name;
  Person(this.name);
}

void main() {
  Person foo = new Person('Tom');
  print(foo.name); // => 'Tom'
}

複数の引数でもOK。

class Person {
  String name;
  int age;
  
  Person(this.name, this.age);
}

void main() {
  Person foo = new Person('Tommy', 20);
  print(foo.name); // => 'Tommy'
  print(foo.age);  // => 20
}

Named Constructors

コンストラクタのオーバーロードはできないそうです。
代わりに、コンストラクタに任意の名前を付けて通常のコンストラクタから呼び出す方法を取るみたい。

class Person {
  String name;
  String full_name;
  
  Person(){
    this.name = 'Tommy';
  }
  Person.full_name(){
    this.full_name = '${this.name} Smith';
  }
}

void main() {
  Person foo = new Person();
  print(foo.name);      // => 'Tommy'
  print(foo.full_name); // => null
  
  Person bar = new Person.full_name();
  bar.name = 'Jacob';
  print(bar.name);      // => 'Jacob'
  print(bar.full_name); // => 'null Smith'
}

うん…?コンパイルは通るけど、こういう使い方じゃない気がする🤔

Redirecting Constructors

オーバーライドしたい時はこれで書くのがよさそう。

class Person {
  String name;
  String full_name;

  Person() : this.name();
  Person.name() {
    this.name = 'William';
    this.full_name = '${this.name} Smith';
  }
}

void main(){
  Person foo = new Person();
  print(foo.name); // => 'William'
  print(foo.full_name); // => 'William Smith'
}

参考ページにはリダイレクト元(左辺)のコンストラクタのボディで処理もできる、とあるのだけど、実行すると「Redirecting constructors can't have a body.」と言われる。
仕様がかわったのかな…?

Initializer Lists

Named Constructorsで、コロンに続けてフィールドの初期化処理を記述できる。
複数フィールドの初期化時はカンマ区切りでリストアップできる。

class Person {
  String name;
  String full_name;
  
  Person() : this.full_name();
  Person.full_name() : this.name = 'Ethan',
                       this.full_name = 'Ethan Smith';
  // field initializerの中からthisにアクセスはできない
  // これはコンパイルエラー
  // Person.full_name() : this.name = 'Ethan',
  //                      this.full_name = '${this.name} Smith';
}

void main() {
  Person foo = new Person();
  print(foo.name);      // => 'Ethan'
  print(foo.full_name); // => 'Ethan Smith'
}

親のコンストラクタを呼び出す時はリダイレクトコンストラクタを使う。

class Person {
  String name;
  String address;

  Person();
  Person.name_address(name, address){
    this.name = name;
    this.address = address;
  }
}
 
class Customer extends Person {
  Customer() : super.name_address('Emily', 'Tokyo');
// コンストラクタのボディで親クラスのコンストラクタは呼び出せない
//   Customer(String name, String address){
//     super.name_address('Emily', 'Tokyo');
//   }
}

main() {
  Customer foo = new Customer();
  print(foo.name);    // 'Emily'
  print(foo.address); // 'Tokyo'
}

ファクトリ(factory)

factoryコンストラクタ。
初めて聞いたのですが、インスタンスを生成しないコンストラクタなので、自分でインスタンスを生成します。
singletonパターンの実装なんかに使うみたいです(あまりよくわかってない)。

class Person {
  String name;
  static var _instance; // 返すインスタンスはクラス変数にする
  
  factory Person(String name){
    if (_instance == null) {
      _instance = new Person._internal(name);
    }
    return _instance;
  }

  Person._internal(this.name);
}

void main(){
  Person foo = new Person('Alan');
  Person bar = new Person('Elen');
  print(foo.name);   // => 'Alan'
  print(bar.name);   // => 'Alan'
  print(foo == bar); // => true、同じインスタンスを返している
}

定数コンストラクタ(Constant Constructors)

コンパイル時に定数オブジェクトをインスタンス化したい場合に使うそう。
グローバルスコープに定数オブジェクトを定義したい時、みたいな感じ…?🤔
定数オブジェクトのフィールドはfinal、コンストラクタはconstで定義、インスタンスの作成もconstで行います。

ちなみにconstfinalはどちらも定数を宣言するキーワードっぽいのだけど、以下のような違いがあるそうです。

class Person {
  final name;
  const Person(this.name);
}

final foo = const Person('Colin');

void main(){
  final Person bar = new Person('Mike');
  print(foo.name); // => Colin
  print(bar.name); // => Mike
}

以上、コンストラクタのまとめでした!

Flutterアプリにimage_pickerを使ってカメラ呼び出しボタンをつける

心はRubyistなのですが、毎日Flutterを触っているので今日も今日とてFlutterネタ。
Flutterといえば今日小田急線のロゴを見かけて、「Flutterのアイコンと似てる!!」とテンションがあがったのですが、帰って確認してみたら共通点は三角形ということと水色ということだけでした。
そんな大雑把な記憶を留める為に今日も小ネタを記してゆきます!

Flutterアプリにカメラ呼び出しボタンをつける

image_pickerというパッケージを使います。
手順としてはこんな感じ。

  1. image_pickerパッケージの読み込み
  2. image読み込みメソッド記述
  3. カメラ呼び出しアイコンを記述

順番にやっていきます。

image_pickerパッケージの読み込み

pubspec.yamlに追記します。

dependencies:
  flutter:
    sdk: flutter
  image_picker: # => Added

バージョン指定はなくても構いません。
書いたらflutter packages getでインストール。

なお、インストールしてるのにうまく反映されないな?てときはキャッシュが悪さしてることが往々にしてあるようなので、flutter clean & flutter runを行うと解決する場合があるようです。

image読み込みメソッドを記述

exampleがすごく充実してるのでその通りやるだけ。
main.dartimportを記述します。

import 'package:image_picker/image_picker.dart';
import 'dart:io';

RandomWordsStateクラスにgetImageメソッドを追記。
(前回の続きに追記しているのでRandomWordsクラスですが、example通りだと_MyHomePageStateクラス)

class RandomWordsState extends State<RandomWords> {
  // (略)
  File _image;

  Future getImage() async {
    var image = await ImagePicker.pickImage(source: ImageSource.camera);

    setState(() {
      _image = image;
    });
  }
  // (略)
}

カメラ呼び出しアイコンを記述

getImageを呼び出す為のアイコンを追記します。

class RandomWordsState extends State<RandomWords> {
  // (略)
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      body:_buildSuggestions(),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add_a_photo),
        onPressed: getImage,
      ),
    );
  }
  // (略)
}

これで、ボタンを押すとカメラアプリが呼び出されるようになりました!

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の完成です!!!