Help us understand the problem. What is going on with this article?

Android開発はFlutterでやる方がいい説

More than 1 year has passed since last update.

はじめに

クロスプラットフォームとして語られるFlutterですが、実は、「Android開発だけでもFlutterでやった方がよくね?」 となんとなく思い始めています。
「FlutterってGoogleがAndroid開発を再定義した画期的なものになるんじゃないか」と。

自分は、おっさんなので古い話をしますが、Java開発でEJB2が存在していた頃です。まだ、バージョンが1.1になったばかりのSpring Frameworkを使った案件にたまたま参加したときの衝撃と同じなんです。「何これ? めっちゃわかりやすい。標準のEJBなんて駄目じゃん。」
今ではEJBは廃れ、Springがデファクトスタンダードになっていますよね。

ただ、使ったことがない人に伝えるのは本当に難しく、納得できない人も多いはずです。
自分でもなんでそう思うのかうまく伝えられる気もしないのですが、言語化してみます。
自分は本気で思っていますが、話半分に読んでください。

ライフサイクルが単純

Android開発で誰もが感じるだろう、これじゃない感。そう、理解しがたいライフサイクル。
「メモリ不足して、Activityが再生成される」とか、「画面の向きを変えたら再生成される」とかとか。
さらにFlagmentが絡むとクラッシュしたりと複雑度は増します。

初期の頃はすごく苦労しましたね。(今もですけど。)
MVPにしたり、MVVMにしたり、Daggerを使ったり、RxJavaを使ったりと色々やりますが、結局この部分は複雑なままです。

これ、Flutterでは気にすることないんです。
Flutter独自のライフサイクルは、Androidと比べると極めて単純です。iosと同等な感覚です。
Android Architecture ComponentsのLifeCycleObserverみたいなものを作成するだけです。

例を出すと以下のクラスを作って、

class LifecycleEventHandler extends WidgetsBindingObserver {
  final Future<void> Function() inactiveCallBack;
  final Future<void> Function() pauseCallBack;
  final Future<void> Function() resumeCallBack;

  LifecycleEventHandler({this.inactiveCallBack, this.pauseCallBack, this.resumeCallBack});

  @override
  Future<Null> didChangeAppLifecycleState(AppLifecycleState state) async {
    switch (state) {
      case AppLifecycleState.inactive:
        if (inactiveCallBack != null) {
          await inactiveCallBack();
        }
        break;
      case AppLifecycleState.paused:
        if (pauseCallBack != null) {
          await pauseCallBack();
        }
        break;
      case AppLifecycleState.suspending:
        break;
      case AppLifecycleState.resumed:
        if (resumeCallBack != null) {
          await resumeCallBack();
        }
        break;
    }
  }
}

以下のようにWidgetで呼び出せば、画面ごとのonPause,onResume相当のことを検知できます。
(Widgetとは、AndroidでいうActivityみたいなものと思ってください。全然違うけど。)

class _SampleState extends State<_SampleWidget> {
  LifecycleEventHandler _eventHandler;
  @override
  void initState() {
    super.initState();
    _eventHandler = LifecycleEventHandler(pauseCallBack: () {
      // onPauseの処理
    });
    WidgetsBinding.instance.addObserver(_eventHandler);
  }
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(_eventHandler);
    super.dispose();
  }

また、上記のように initState() と dispose() という単純なライフサイクルをまずは理解していれば良いですし、もっとちゃんとライフサイクルを理解しようとしても簡単に理解できるはずです。

もちろん、開発者オプションの「アクティビティを保持しない」をONにしても普通に動作します。

念の為、ライフサイクル複雑的な記事も示しておきます。
https://academy.realm.io/jp/posts/sf-fabien-davos-modern-android-ditching-activities-fragments/
https://ninjinkun.hatenablog.com/entry/2014/10/16/234611
https://qiita.com/yuya_presto/items/331301cb91bec335ecdf

ホットリロードがめっちゃ早い

AndroidにもInstant Runがありますが全然違います。
爆速とはほど遠く、数分待たされることもあるし、状態が保持されなかったりして、バグなのかが判断できない場合が多々あるので、OFFにしている人も多いでしょう。

FlutterのHot Reloadは真に爆速です。1秒以内です。状態もなくなることもありません。
開発のほとんどの期間をHot Reloadで開発できます。
例外として、initState()メソッドの変更は、restartする必要があります。がこれも早い。Hot Restartと呼んでいるようです。これは数秒です。

Instant RunとHot Reloadの違いは以下がわかりやすいです。
https://stackoverflow.com/questions/52050660/difference-between-androids-instant-run-vs-flutters-hot-reload-and-react-nativ

標準でMaterialDesignをサポート

Androidのデザインは、ほとんどMaterialDesignにするでしょう。

Flutterでは、MaterialDesignのWidgetが用意されています。Android4.4も同様の見た目になります。
Material Colorも標準で用意されています。その中から選択するだけです。
当然、自由に色をつけることも可能です。

Iconもあります。(ちなみに標準ではないですがfont awesomeなIconもあります。)

MaterialDesignをきちんと知っている必要がありますが、何も考えずに余計なことをしなければMaterialDesignになります。

Themeも用意されていて、これだけを変更すれば全ての画面で変更されます。
カスタマイズも可能です。
(Themeに関しての詳しい記事はこちらを参考してください。)

良くも悪くもDart言語

自分、Kotlinが大好きです。Dartよりも圧倒的に好きです。
「FlutterもKotlinで書ければいいのに。」と思っています。

言語の印象は、Java < Dart < C# < Kotlin みたいに感じています。

ただし、これだけは言いたい。Dart言語の文法は、ほとんどJavaです。JavaScriptに近いかもしれませんが、Javaを知っている人の学習コストは限りなくゼロに近いでしょう。

特にAndroid Javaに比べたら圧倒的に良いです。以下の仕様があります。

  • async,await,Promise(Futureクラス)が標準で用意されていて、非同期処理が簡潔にかける(コールバック地獄にならない)
  • StreamというRxの簡易版が標準で用意されていて、変更対象の検知を簡単にできる
  • 静的型付けで、型推論ができるので、Javaのようにいちいち型を記述する必要がない
  • 言語仕様でFactoryパターンが用意されている。(factory constructor)
  • 流れるようなインターフェイスな言語仕様がある。(..)ドットを二回書くとメソッドを実行しつつインスタンスを返すことができる。Kotlinのapplyよりも簡潔に書ける。(Cascade notationという仕様です)
  • 一応、コレクション系にmapやreduceなど関数型なメソッドは用意されている。ただfirstWhereなど若干使いづらい。(filterじゃないのがC#っぽい)
  • 名前付き引数やデフォルト引数ができる
  • Kotlinのようにnullの場合でも(.?)を使えば良いのでnull分岐が減る(safe navigation operatorもしくはNull-aware operatorsと呼ぶらしい。コメント欄に例を記述しています。)

もしJavaしか知らない人でも上記のキーワードを検索すれば、他の言語を学ぶよりも簡単に理解できると思います。
多分、上記の仕様が理解できたら、もうDart技術者です。

Kotlinと比べて、いまいちな点は、文末のセミコロンと拡張関数がないこととNull Safetyではない点です。
文末のセミコロン、これは諦めるしかないでしょう。

Null Safetyに関しては代替手段として、DDDのValueObjectパターンを使ってnullにならないようにしたり、NullObjectを作ったりでしょうか。

例えばこんな感じのValueObjectを作って、

class UserId {
  final String _value;
  String get value => this._value ?? "";

  const UserId(this._value);

  @override
  bool operator ==(other) => other is UserId && value == other.value;

  @override
  int get hashCode => value.hashCode;

  @override
  String toString() => value;

  bool get isEmpty => this == null || this.value.isEmpty;
}

nullが帰ってくる可能性のあるところで使い、プログラムではこの型で使い続ければ良いでしょう。

var userId = UserId(json["user_id"] as String),

拡張関数の代替手段としては、コンポジットにするぐらいでしょうか。全く、代替にはなってないですが。。
(拡張関数なら、EmojiクラスなどのValueObjectを作った方がいいと思う)

class StringEx {
  final String string;

  StringEx(this.string);

  bool isEmoji(){
    // string変数を使ってゴニョゴニョする
  }

Dartを敬遠して、Flutterを使わないのは非常に勿体ないです。
Dartでつまづくことは限りなくゼロですし、もう一度言いますが、Javaで開発するよりも遥かに良いです。

画面もDartで記述

Androidの場合、XMLで画面を作ったりしますが、Flutterでは全てDartです。
これをマイナスだと思う人もいるかもしれませんが、むしろいいんです。
コードで書くことで、型安全にもなるし、シンプルにやりたいことが実現できます。
巨大なWidgetになってもコードなので、簡単に分割リファクタできます。

KotlinのAnkoのメリットと同じなんでしょうか。(いまいち流行ってない?)
おそらく、それ以上にFlutterのはAndroid Studioのおかげで使いやすくなっていると思われます。

例えば、以下の画像の赤枠ですが、コメントを書いているわけではなく、対応するカッコをAndroidStudioが表示してくれているものです。
スクリーンショット 2019-03-27 14.15.39.png

さらにAndroidStudioがAlt+Enterで候補を表示してくれます。ネストが深くても簡単に削除したり、追加したりできるのです。
スクリーンショット 2019-03-27 14.17.53.png

これらの機能のおかげで、XMLで記述していた時と同程度な可読性だと感じます。

普通のAndroid開発の場合、XMLをPreviewで見れます。初期値をプログラムで設定する場合でも、tools:text="サンプル" みたいに書いておけば、ちゃんとしたデザインで見れるあれです。
残念ながら、Flutterではそれはできません。

ただし、それで不便を感じたことはありません。おそらくHot Reloadが早いからなのでしょう。
いちいちXMLで偽のプレビューを見るのと同じスピードで実際の画面が見れるのです。

いざとなればAndroidネイティブだけの画面に遷移できる

これができるので安心なんです。

最初の画面はできないですが、遷移先の全ての画面をKotlin/Javaで開発できます。

こんな感じです。
MainActivityでdartの処理を受け付けます。
この場合は、NextActivityに遷移します。
NextActivity以降は普通のAndroid開発と同じです。

MainActivity.java
class MainActivity() : FlutterActivity() {

    companion object {
        const val CHANNEL = "sample.ko2ic/toPlattformScreen"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)

        MethodChannel(flutterView, CHANNEL).setMethodCallHandler(
            object : MethodCallHandler {
                override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
                    if (call.method.equals("toPlattformScreen")) {
                        startActivity(NextActivity.intent(this@MainActivity))
                    } else {
                        result.notImplemented()
                    }
                }
            })
    }
}

dart側では、button押下などで、_toPlattformScreen()を呼ぶだけです。

  static const MethodChannel channel =
      const MethodChannel('sample.ko2ic/toPlattformScreen');

  _toPlattformScreen() {
    try {
      channel.invokeMethod('toPlattformScreen');
    } on PlatformException catch (e) {
      // TODO
    }
  }

ただし、言いたいことは、一年間業務で開発してきましたが、これをせざるを得ない場合がなかったです
ただ、保険として、これができるのは良いことでしょう。

Androidネイティブな機能のアクセスも簡単

ほとんどPluginで実現できますが、足りない機能を使わないと行けない場合はもちろんあるでしょう。
その場合でも恐る必要はありません。
Android開発をバリバリやっている人にとっては、Flutter上でAndroidネイティブな機能にアクセスすることは簡単です。
上記のMainActivityに書いてあることが基本で、あとは、引数や戻り値などを考えれば良いだけです。

実際に自分も業務で利用するGoogle Ad Manger(旧DFP)のPluginなど複数のPluginを自作しましたが簡単でした。

自作のflutter_google_ad_managerもそうですが、PlatformViewという機能で、Flutterの画面の一部分だけAndroidネイティブで表示もできます。

Pluginが簡単に使える

例えば、文字列を共有する場合です。

Androidの場合はこうです。

    Intent shareIntent = new Intent();
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.putExtra(Intent.EXTRA_TEXT, "ほげ");
    shareIntent.setType("text/plain");
    Intent chooserIntent = Intent.createChooser(shareIntent, null);
    activity.startActivity(chooserIntent);

これ、Android開発になれている人にとっては、当たり前だろうけど、知らない人から見ると「Intentって?」、「ACTION_SENDをやるのか」、「createChooserを使うのか」とか、意外と知らないとできないことですね。
これぐらいだったら、ネットですぐにサンプルを見つけられますが、もっと難しい処理だったら学習コストはそれなりに高いと思います。

ところが、Flutterの場合はこれだけです。

Share.share('ほげ');

当たり前ですけど、ライブラリでWrapされているので簡単に記述できます。

まとめ

Androidエンジニアを育てるのは結構大変だと思うんですね。大変にさせているのはライフサイクルに尽きるのかなと。
それ以外にも覚えることは一杯あります。
Flutterでも覚えることはたくさんあります。
ただし、Androidの学習コストとFlutterの学習コストは、Androidの方が高いと思うんです。

ほとんどのチームで一人ぐらいは、Android開発に精通していると思うんです。
そういう人がOSSには存在しない機能を作っていけば、その他の精通していない人はすごく簡単に実装できるようになります。
(もちろん、いずれは開発している人全員が、Androidに精通してもらうのが理想だけど、初期段階では全員が知ってる必要もないのかなと)

精通していない人でもAndroid開発はもちろんできますが、ライフサイクルの複雑さのため徹底したレビューが必要になるし、思わぬクラッシュが出やすいです。

Flutter自体の学習コストですが、普通に作る分には低いと感じます。
画面レイアウトの仕方を覚える必要がありますが、Androidでのレイアウトの仕方と学習コストは変わらないか、それよりも低いかもしれません。

中級ぐらいでわかってないと困ることは、以下の記事に書いてあることとプラスアルファ(例えば、Builderは冪等であるべきとか)ぐらいで大丈夫なんじゃないかな。これらを読むと難しく感じるかもしれないけど、逆に言うとこれぐらいなんです。Flutterの難しいところ。

ちゃんと伝わってるかは自信はないですが、なんとなく、「Android開発だけでもFlutterでやる方がいい」と思った理由を書けたんじゃないかな。

ko2ic
最近はflutterです。
uzabase
企業活動の意思決定を支える情報インフラの提供
https://www.uzabase.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away