Pocket

Flutterのおかげでスマホアプリの憂鬱から解放されました。その話をしたいと思います。

最後にネイティブ開発をiOS/Android別々にやったのは2019年なので、ちょっと情報が古い点はあると思います。私が感じた辛みはある程度緩和されているかもしれませんが、ちょっとバッドノウハウ感あるなと。

Flutterは、ワンソースで iOS / Android / Web(PWA) / WindowsAppなどをビルドして公開できるクロスプラットフォーム・フレームワークというもので、Googleが主体となって開発を進めているオープンソースプロジェクトです。

モバイルアプリケーション開発の辛み

UIを作るのに色んな手続きやノウハウが必要

UIの構造・画面遷移・デザインなどが統一的な仕組みではなく、UIのデザインはコードではないが、画面遷移や構造の表現はコードで書くなど、あっちこっちを行ったり来たりするのがだるいと感じていた。IBOutletに代表されるやつです。Anndroidだと、R.id.textみたいなやつ。ButterKnifeって今でも現役なのかな?

SwiftUIは一旦脇に置かせて頂いて、iOSアプリの開発には原則StoryBoardという画面遷移図のようなものを使います。これがまた癖が強い。画面にパーツをペタペタ貼っていくような格好もできますが、そうすると何かのパーツを差し込んだりするタイミングで、GUIでレイアウトを修正しないといけませんでした。

コードでViewを全部初期化しようぜと言っても、レイアウトを組むのにこんな感じのコードをたくさん書かないといけませんでした。最もプリミティブな書き方の例です。

let layoutIcon = "layoutIcon"
let layoutImg = UIImage(named: layoutIcon)
let layoutView = UIImageView(image: layoutImg)
layoutView.backgroundColor = .green
layoutView.layer.cornerRadius = 10
//AutoresizingMaskをAutoLayoutの制約に置き換えるかどうか指定する値(必ずfalse)
layoutView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(layoutView)

//制約を追加 top:50 
//パラメーター説明 item:制約オブジェクト attribute:制約する属性 relatedBy:制約タイプ toItem:制約の相手 attribute:制約相手に使用する属性 multiplier:乗数値 constant:定数値
let topConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 50.0)
//制約は定義した後、activateする必要あり
topConstraint.isActive = true

//制約を追加 left:50
let leftConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 50.0)
leftConstraint.isActive = true

//制約を追加 width:70
let widthConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0)
widthConstraint.isActive = true

//制約を追加 height:70
let heightConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0)
heightConstraint.isActive = true

https://qiita.com/jamestong/items/47ef1ce3cb81c60326ee

iOSにはAutoLayoutという、デバイスのサイズに応じてレスポンシブにUIパーツのサイジングや余白取りがなされる仕組みがありますが、全部コードなんですよね…

今どきのUIでは当たり前になっているDrawerも、私がiOS開発をやっていた時は自前で実装しないといけなかった。多分、2021年の今でもないと思う。UIコンポーネントがあまりないので、Card的なものも自分で作ったり、フォームっぽいやつも自分で…という。ビュー絡みのライブラリすごくいっぱいあった。

Androidはxmlで画面レイアウトを作れますので、その点は楽です。StoryBoardがない分、画面遷移の管理がiOSより原始的でライフサイクルのイベントの多いのでいい感じに画面遷移を管理する必要があり、ライフサイクル起因のバグがわかりにくかった記憶があります。

ライブラリのメンテナンス

単純な話でライブラリの数が倍になります。OSが違いますからね.. 一つのアプリで20近いライブラリがあるのが普通でしょうから、倍の40になります。隔週単位でライブラリのメンテナンスをすることになるでしょうけど。

Androidの場合、今はAndroidXで統一されていると思いますが、SupportLibraryというのがうざかったです。新しいOSバージョンでしか使えないUIパーツ系のクラスを古いOSでも利用できるようするやつなんですが、バージョンがちょいちょい変わってその度にビルドし直した記憶があります。

Flutterの登場でDXが爆上がりした

人生で一番衝撃を受けたOSSが、私にとってはFlutterでした。プログラミングを始めたばかりの頃のワクワクを覚えました。Developer Experience(DX)が全然違ったからです。

宣言的UI

Flutterの大きな特徴が宣言的UIというやつです。すげー簡単に言うと、コンストラクタでUIを構築するため、途中でsetColorみたいな関数を一切呼び出さないスタイルです。これがすごくやりやすかった。

   return Container(
    padding: EdgeInsets.all(20),
    child: Form(
        key: _loginFormKey,
        child: Column(children: <Widget>[
            TextFormField(
                decoration: InputDecoration(labelText: 'ユーザーID'),
                key: const Key(TextFieldKey.USERNAME_KEY),
                onChanged: (v) => data = data.copyWith(userName: v),
                validator: (value) {},
            ),
            TextFormField(
                decoration: InputDecoration(labelText: 'パスワード'),
                key: const Key(TextFieldKey.PASSWORD_KEY),
                onChanged: (v) => data = data.copyWith(password: v),
                validator: (value) {},
                ),
            SizedBox(height: 20),
            ElevatedButton(
                child: const Text('ログイン'),
                onPressed: () async {}
            ),
        ])
    )
);

実際にウチで書いてるFlutterのコードです。全部コンストラクタになっています。このサンプルでは、以下のようなツリー構造になっています。WidgetTreeと言います。

  • Container
    • Column
      • TextFormField
      • TextFormField
      • SizeBox
      • ElevatedButton

StoryBoardやxmlから解放され、コードでビューの構造と振る舞いが一括で表現できます。xibとかxmlなどを使わないでビューのデザインができるのは嬉しい。宣言的UIなのでシンプルに書けます。

最近は、iOSやAndroid自体にも宣言的UIでビューを記述する仕組みがあります。私にとってはできることが同じならワンソースで完結するFlutterを採用します。

レイアウトは基本的に画面サイズに応じてパーツが最大化されます。iPadで横にするとテキストフィールドは最大化されびよーんて長くなります。いやだったら、相対的なサイズを設定したり縦横で別々のレイアウトを組みます。その辺のパーツはFlutter公式でご用意してくれています。

UI開発を促進してくれるのがHot Reloadです。コードを修正されるだけでリロードしてビューの表示の確認ができます。ソースコード修正→コンパイル→ビルド→アプリ起動→対象のビューという流れが、ソースコード修正→コンパイル→対象のビューに短縮されます。秒で終わります。

余談:UI更新はどうやってやるの?

宣言的UIの場合、ビューはイミュータブル(不変)になります。ビューの変更(例えば表示・非表示の切り替え)はリビルドによって表現される仕組みになっています。

ラベルで合計値を表示するような処理の場合、お買い物かごの内容に変化があれば当然合計値も変化します。宣言的UIじゃない場合は、sumLabel.text = "10,000"みたいなコードを書くことになるでしょう。

Flutterはその種のコードが書けません。コード的には、Text(value)になります。valueは合計値の値が入った変数です。合計値は変動するが変数valueの値を出すことは変わらないでしょ、というのが宣言的な考え方です。なので、valueが再代入されてインスタンスが更新されたら、UIを更新するイベントをディスパッチしてリビルドします。

データの更新だけを行いUIを更新する状態管理の手法も色んな方法がありますが、個人的にはRiverpod+StateNotifier+Freezedです。サンプルも多いし、アーキテクチャもシンプルです。

ライブラリのメンテナンスコストの激減

当たり前ですけど、iOS / Androidを各々作ることに比べれば、Flutterはワンソースです。単純計算で半分になります。UIに関する部分だけ抜き出すと、10個前後じゃないでしょうか。Flutter自身の更新が速いのもあって、ライブラリも隔週単位でバージョンアップされることが多いですけど、数が少ないのでキャッチアップもやりやすい。

Flutter自身のバージョンアップの速度はかなり速いです。NullSafety対応レベルのBREAKING CHANGEは早々ないと思いますが。現在は2系ですが、来年あたりは3系になりそうな勢いです。そこでまた、UIに新しい部品ができたり画面遷移の書き方が変わったりするかもしれません。

https://flutter.dev/docs/release/breaking-changes

XCodeがバージョンアップされてSwiftのバージョンが上がって、5.4でコンパイルしてあるFrameworkはだめだから!みたいなエラーが出て再ビルドするのに比べると、まだいいか。SwiftPM以前はそんなの結構あった。

ネイティブアプリ開発の経験は必要か

必須ではないと思います。開発自体はできます。ただ、ビルドシステムはある程度勉強することになるはず。アプリ公開できないから。XCodeのビルドオプションすげーいっぱいあるし、FlutterはライブラリのビルドにCocoaPodsを使ってたりします。Androidだと、Proguardとかね。あの辺ハマると結構沼。

Flutterを学んで、個人的に考えさせられたのがWidgetテスト。これを作ることになって、ちゃんとMockできないと回せない。Widgetテストではネットワーク通信がなされないので、外部リソースを含むビジネスロジックをどういい感じにMock化して差し替えて精度の高いテストを回すか。まだ模索してる。

Flutterで新しい考え方に出会えた

Vue.jsをちょっと前にやってて、あーこうやってクライアントをビルドして状態管理して進めるようになったんだなーと思ったんですけど、Flutterに出会ったときほどの宣言的UIに対するカルチャーショックは受けなかった。

宣言的ではないUI開発をずっとやってきたせいか、Flutterに出会った時は開発手法もモダンでスッキリとしたのに驚いた。コードで全部ビューが作れるんだ、細かい操作に対して標準でUIパーツがすげーたくさんあって助かったのもあるし、ライフサイクルのイベント管理はこうなって、状態管理や画面遷移はこうなるんだみたいな、今までの自分にない考え方に多く出会えた。それが一番の収穫だったように思う。

その上でワンソースでアプリが作れるので、いやもうこれ最高。

We ♥ Flutter !!