Kenji Abe

Photo by Lisa Luminaire on Unsplash

Androidの公式ドキュメントにある設計ガイドが更新され、実装について以前よりだいぶ明確に指針が記載されています。

その中で、ViewModelからのイベントで画面にメッセージを表示するような消費型のイベントについての記載があります。

https://developer.android.com/jetpack/guide/ui-layer/events#consuming-trigger-updates

イベントも状態として表現しており、メッセージ等を表示後にその状態を更新するような感じの処理になります。

個人的にずっと悩んでた、SingleLiveEventは推奨されないけどどう実装するのが良いのだろう…っていう悩みが解決しました。

ただ、ガイドでは簡略されてるため実際にこのパターンで実装するうえでの悩みと実際にどう実装すれば良いかを考えてみました。

実装の悩みどころ

設計ガイドの実装では Snackbar でメッセージを表示するようになっていますが、現実ではそれ以外にも処理したいことがあります。

例えば、何か処理が終わって成功したときに画面遷移をしたい場合にどう実装するのが良いのか。

メッセージを表示する手段も、 Snackbar だけじゃなく AlertDialog や Toast もあります。

この複数の種類があるイベント処理についてどう実装するのが良いのかが悩みどころです。

これをどう解決するかを考えてみました。

UIの処理パターンで状態を分ける

例えば、以下のような感じで、UIでの処理パターンを状態としてUI Stateで別々で管理する方法です。

更にViewModel側でこれらのイベントを消費するメソッドを用意してあげます。実装例は省略してますが、それぞれ処理済みのイベントListを除外してUI Stateを更新する感じです。

ViewModelからイベントを発行するには以下のような感じです。

UI側はそれぞれで対応する処理をする感じです。以下の例はComposeを想定していますが、Viewシステムで似たような感じになります。

実装としてはこれでも問題なく実現できるかなぁとは思いますが、だいぶ冗長な感じがしています。UIの処理パターンが増えるたびに管理する状態も増えていきます。

また、ViewModelからイベントを発行する際にUIの処理を強く意識する必要があります。

イベントを1つの状態で管理する

ぼくが考えたもう一つの実装方法として、イベントを1つの状態として管理する方法です。

まず、ViewModelから発生するイベントを sealed interface/class で定義し、UI Stateで管理します。この例では成功とエラーの2種類を定義しています。

次にイベント処理後に消費するためのメソッドを追加します。単純にイベントListから消費したいイベントを除外したListを作成してUI Stateを更新する感じですね。

Photo by Alexander Schimmeck on Unsplash

AndroidX Macrobenchmarkでアプリの起動時間を計測する方法について書いていきます。

基本はドキュメントを見れば大丈夫だとは思いますが、少し分かりにくいものもあるので、丁寧に手順を説明しようと思います。

https://developer.android.com/studio/profile/macrobenchmark-intro

環境

  • Android Studio Bumblebee Beta 4

ベンチマーク用のモジュール作成

まずはベンチマーク用のモジュールを作成します。 作成するには通常のマルチモジュールと同じです。1点注意としてはminSdkは23以上にする必要があります。

Photo by Kelly Sikkema on Unsplash

Elevationを指定することで、ある程度自動で影をつけることが出来ます。ただし、これには条件があり、うまく影がつけれないパターンもあります。

例えば、以下のようなレイアウトで背景に角丸の長方形を設定したものは自動で影がついてくれます。

Photo by Aleksi Tappura on Unsplash

Gradle 7.0から導入された Version catalog を使ったライブラリ管理方法についてまとめておきます。

また、Gradle 7.2ではいくつか改善がされてるので、今回はGradle 7.2を使っていきます。

Gradleで使用するライブラリの依存関係を定義するときに、これまでもいろいろな方法がありました。例えば、extra propertiesを定義したり、buildSrcで定義したり、など。

これらの方法とは別に Version catalog という新しい方法を使うことができるようになりました。

Version catalogの定義

Version Catalogを使ってどのようにライブラリを管理するかを見ていきます。例として、androidxのviewmodelとlivedataを使う場合です。

settings.gradle に以下のように記述します。

次に使いたい箇所の build.gradle に以下のように書きます。

settings.gradle に定義したものを build.gradle で使うだけです。

少しややこしいのが、定義は lifecycle-viewmodel のようにハイフン区切りのものが、使用するときは、 lib.lifecycle.viewmodel のようにドット区切りに変わることです。

もう少し便利な使い方を見ていきます。

バージョンを定義する

先程の例では、同じバージョンを使用していましたが、こういう場合はバージョンを別に定義することで一箇所で管理することができます。

version というのでバージョンを定義できるので、それを versionRef として参照している感じです。

ライブラリをまとめて管理

よくあるケースとして、複数のライブラリを一緒に使用したい場合があります。こういった場合も Version catalog では管理することができます。

bundle を使って、複数のライブラリをまとめることができます。

これを build.gradle で以下のように使用することができ、複数のライブラリを一つのライブラリかのような依存定義で書けるようになります。

libs.bundles のあとに自分で定義した名前を書くことで使用できます。

この例だと一行で viewmodel と livedata が同時に依存関係に追加されます。

Pluginの管理

最後にPluginの管理についてです。Version catalog でPluginを管理するには行かようにします。

Target SDK 31(Android 12)から PendingIntent のmutability(可変性)を指定する必要があります。

https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability

指定するときに FLAG_IMMUTABLEFLAG_MUTABLE のどちらかの値を使用することになります。

この2つのフラグの違いについて解説します。

コード例

まずは動作確認用として、Activityから別のActivityにPendingIntentを渡して、そのPendingIntentを実行する例です。

まずはPendingIntentを作って、それを別のActivityに渡すコードです。

次に、起動されたActivity側でPendingIntentを受け取り、PendingIntentに関連付けられてるIntentを実行するコードです。

PendingIntent.send で関連付けられてるIntentを起動することができます。

これを実行すると新しくActivityが表示される感じになります。

これをベースに考えていきます。

FLAG_IMMUTABLE と FLAG_MUTABLE

先程のコードだと、FLAG_IMMUTABLEFLAG_MUTABLE では結果に違いがありません。

PendingIntent.send では関連付けられてるIntentに対してパラメータを追加することが出来ます。以下のように新しくIntentを作り、それを PendingIntent.send に渡してあげます。(Intent.fillIn と同じことが行われています)

これにより、起動される側(今回の例ではMainActivity)で追加されたパラメータが取得できるようになります。

FLAG_IMMUTABLEFLAG_MUTABLEPendingIntent.send に渡された追加のIntentを無視するかどうかのフラグになります。

FLAG_IMMUTABLE

FLAG_IMMUTABLE を指定すると、パラメータを追加をしたとしても、元々のIntentは変更はされず、起動された側でも取得できずにnullになります。

ほとんどケースで FLAG_IMMUTABLE を指定することになると思います。

FLAG_MUTABLE

FLAG_MUTABLE を指定すると、変更が可能となり、先程のパラメータを追加したときに、起動された側で取得することが出来ます。

あまり自分で実装することは無いですが、通知のDirect reply actionなどのいくつかケースでは FLAG_MUTABLE を指定する必要があります。詳しくはドキュメントを確認してみてください。

Androidのバージョンによる違い

このフラグは Target SDK 31の場合は必須となっているため、実行時にエラーになってしまいます。そのためどちらかを必ず指定する必要があります。

Target SDK 31未満のデフォルトは、FLAG_MUTABLE と同じ挙動となっています。

参考

Kotlin 1.5.30からOpt-inにて Exhaustive when statements が試せるようになったので、これについて解説します。

現状

これを書いてる最新バージョンであるKotlin 1.5.30までの sealed class/interface と Boolean の when ですが、以下のようにすべてを網羅しなくてもビルド時に警告などは表示されません。(IDE上での警告は表示されてました)

ただし、以下のように when から戻り値を受け取るような場合にはコンパイルエラーになります。

以前から要望として、戻り値を受け取らない場合などでも網羅チェックをしてほしいというのがありました。

exhaustive when statement

Kotlin 1.5.30からOpt-inにてexhaustive when statementを有効にすることができます。

有効にするには build.gradle に以下のように記述します。

この状態でsealed class/interface と Boolean を網羅せずにビルドすると以下のような警告が表示されます。

Non exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7, add 'Error' branch or 'else' branch instead

Kotlin 1.6からはデフォルトでこのような警告が表示されるようになります。

更に build.gradleprogressiveMode = true に追加することで警告ではなくエラーにすることも出来ます。

これで網羅せずにビルドすると以下のようなエラーになります。

'when' expression must be exhaustive, add necessary 'Error' branch or 'else' branch instead

これからの対応

Kotlin 1.6では when で網羅されていない sealed class/interface と Boolean では警告が表示されるようになります。

Kotlin 1.7では警告がエラーになる予定です。

Opt-inして早めに網羅されてないものがないかをチェックしておくと、後々安心かもしれません。

参考

最近よくAndroidエンジニアが足りない、採用難しいって話をよく聞くので、ぼくが感じてることを書きたいと思います。

すべての状況を理解してるわけでもなく、ぼく自身が感じてることなので、そこはご了承ください。また、Androidエンジニアを特別に優遇しろというわけではないので誤解しないようにしてもらえると。

ぼくが言いたいことは、すべてこのツイートにまとめられています。

Androidエンジニアは本当に少ないのか?

ぼくはAndroidエンジニアは少ないとは思っていないです。ただメインでAndroidエンジニアをやっている人は少ないとは思います。

ぼくは昔はAndroidのコードを書いていたという人を何人か知っていますし、iOSがメインだけどAndroidもやってるっていう人も知っています。

なので、Android開発の経験者としてみると、そこそこの人数がいると考えています。

何かしらの理由がありメインではやらない、やりたくないっていう人もいるかもしれません。

Androidエンジニアのツラミ

採用うんぬんの前にAndroidエンジニアとして感じる普段のツライ点をあげます。

Androidのデザイン

昔から言われてることですが、デザインの問題があります。いつもデザイナーさんから渡されるデザインがiOSしかないことが普通になっています。

工数などの問題があり、改善することが難しい状況です。

ただ、会社によってはしっかり対応してるところも知っています。

iOSのデザインしかないことは、Androidエンジニアからするとやはり悲しい気持ちになります。(ぼくは慣れきってしまって感情は無になってしまいましたが…)

逆にMaterial Designを理解してくれてるデザイナーさんがいるというだけでかなりテンションがあがると思います。

リソース、スケジュール問題

Android開発をメインでやっている人は少ないため、どうしてもリソース不足になります。

ただ、それでもなぜかiOSのリリースとスケジュールを合わせなきゃいけなかったりすることもあり、だいぶ頑張らなければならない状況に陥ります。

時間がないため新しい技術にチャレンジする時間がない人もいるかと思います。

なんとかやってしまう人もいれば、挫折してしまう人もいるでしょう。頑張ったとしても報酬があがらないこともあります。

この状況で続ける人がいるのかどうか…

誰も使ってない

日本におけるAndroid/iOSのシェアはだいぶ偏りがあります。

こちらの政府の資料 [ PDF ] によると、iOSが約66%でAndroidが約34%です。

そういった流れから同じプロジェクトのなかでAndroidを使ってる人がいなく、プロジェクト内の会話が常にiOSだったりないでしょうか?

こういった状況でモチベーションを維持することはできますか?

Androidエンジニアの楽しさ

ツライとこばかりあげてもアレなので、個人的に楽しいところも。

開発環境

Android開発はiOS開発とは異なり、Windows/Mac/Linuxで開発環境を作ることができます。

また、Android Studioという優秀なIDEのおかげでストレスなくコードを書くことができます。

新しい技術

Googleが常に色々と新しいライブラリなどを出してくれるおかげで、ぼくは飽きずにAndroid開発を続けられています。

言語もJavaからKotlinが主流になりました。Kotlinも書いてて非常に楽しい言語です。(Javaも好きですよ)

今はJetpack Composeなどのまったく別のUIツールがリリースされるなどして、本当にワクワクするものばかりです。

ただ、これはデメリットも大きく、初学者がつまりやすいという問題があります。これはなんとかしていきたいなぁと常々思っています。

コミュニティ

ぼくはAndroidエンジニアのコミュニティが本当に好きで、みんな良い人ばかりです。

日本では @mhidaka さんが中心になって色々やってくれていて、いつも感謝しています。

初学者もウェルカムな空気なので、ぜひ色んな人とか関わってもらえると良いかなと思います。

他にももっとあると思います。みんな楽しいところを共有していきましょう!

大事なこと

だらだらと書きましたが、採用の前に、自社のAndroidエンジニアと向き合うことが大事だとぼくは思います。

本当にAndroidエンジニアにとって良い環境だと自信を持って言えますか?

もし良い環境であれば、自信を持ってどういう環境かをアピールすると良いと思います。

ただ来てくれっていう言うだけでは誰も来てくれないでしょう。

最後に

ぼくはAndroidエンジニアの方の相談にのることが時々あります。そこでツラミを多く聞くことがあります。ツライと言いながらも、Android開発が好きで続けてる人も多くいます。

Android開発が好きなAndroidエンジニアがもっと活躍できるような世界になることを願いつつ、ぼくも微力ですが協力していきたいと思います。

また、これからAndroid開発を始める人も何かぼくにできることがあれば協力していきたいと思っています。

Navigation Componentでは、通常のFragmentと同様にDialogFragmentも扱えるようになっています。しかし、問題点もあるので、その解説をしたいと思います。

以下のような感じで <dialog> を使うだけです。

問題が発生するケース

DialogFragmentを単純に表示するだけでは、特に問題は起きません。

よくあるケースとして、DialogFragmentの結果によって、画面を閉じたり、次の画面に遷移したいケースがあると思います。そのときに問題が起こる可能性があります。

以下は setFragmentResultListener を使った例になります。共有の ViewModel や、単純な findNavController().currentBackStackEntry を使った場合も同様です。

このとき、次の画面に遷移しようとDestinationが見つからずにクラッシュします。そして popBackStack は何も起きません。

知っておいてほしいこと

解説の前に知っておいてほしいことが2つあります。

DialogFragmentの呼び出し元のLifecycleについて

DialogFragmentを呼び出したときの、呼び出し元のLifecycleについて知っておいてほしいことがあります。

呼び出し元FragmentのLifecycleは、DialogFragmentを表示したとしても、状態は変わりません。変わらずに、 ON_RESUME の状態のままになります。

NavigationのDialogの表示方法について

もう一つ知っておいてほしいことがあり、Navigation ComponentがDialogFragmentを表示するときに使用してる FragmentManger についてです。

Navigation Componentは <dialog> を使ってDialogFragmentを表示する際は、呼び出し元Fragmentの parentFragmentManager を使って呼び出しています。

原因

まず、 setFragmentResultListener ですが、ここが呼ばれるタイミングとしては、 Lifecycleが ON_START 以降になります。

また同様に、単純なfindNavController().currentBackStackEntry についても同様です。

以下は setFragmentResultListener の例で説明しています。

通常のFragmentの場合

通常のFragmentの遷移の場合は、呼び出し元は一度 ON_DESTROY まで以降するため、遷移先のFragmentで setFragmentResult が実行されても遷移元の setFragmentResultListener はすぐに呼び出されません。遷移元Fragmentへ戻ってきてから呼び出されます。

DialogFragmentの場合

DialogFragmentの場合ですが、前述しましたが、呼び出し元は ON_RESUME の状態になります。そのため、DialogFragmentで setFragmentResult が実行されると、すぐに呼び出し元の setFragmentResultListener が実行されることになります。
そのため、DialogFragmentがまだ表示されてる状態で、setFragmentResultListener が呼び出されていることになります。

上記の挙動から、DialogFragmentからの戻り値で次の画面への遷移や、画面を閉じようとしたときは、FragmentMangerとしては呼び出したDialogFragmentで行われてることになります。

Navigation Componentでは、DialogFragmentからのDestinationを探して見つからないという状態になります。

対応方法

findNavController().currentBackStackEntryの場合

この場合は公式ドキュメントに書いてあります。

https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations

詳細についてはドキュメントのほうを確認してください。
対応してる概要としては NavBackStackEntry のLifecycleを監視して対応する感じになります。だいぶ長めコードですが。

setFragmentResultListener、共有ViewModelの場合

setFragmentResultListener や共有 ViewModelを使いたい場合の対応方法は現時点ではありません。(もし、知っていたらコメントもらえると嬉しいです)

ただし、Navigation Componentを使わなければ、 setFragmentResultListener でも問題ありませんが、少し注意があります。

まず、DialogFragmentを表示するときは、 childFragmentManager を使うようにします。ドキュメントにもそう記載されています。

SampleDialogFragment().show(childFragmentManager, TAG)

また、setFragmentResultListenerchildFragmentManager に対して行う必要があります。

これで問題は起きなくなります。

補足

childFragmentManagerAlertDialog を使う場合、少し注意があるので、以前書いた記事を参考にしてみてください。

Kenji Abe

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO