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

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

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 を網羅せずにビルドすると以下のような警告が表示されます。

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

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

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

これからの対応

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

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

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

参考


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

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

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

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

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

ぼくは昔はAndroidのコードを書いていたという人を何人か知っていますし、iOSがメインだけど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について知っておいてほしいことがあります。

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

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

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

原因

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

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

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

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

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

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

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

対応方法

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

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

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

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

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

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

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

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

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


DialogFragment の内部でシンプルな AlertDialog を使っていて、さらにその AlertDialog のボタンから画面遷移の処理をしているときの注意点についてです。

簡単な例

簡単な実装例です。
画面のボタンを押したらダイアログを表示して、そのダイアログのボタンが押されたら、ダイアログを閉じて次の画面に遷移するのを想定してます。

まずは


Photo by Lalith T on Unsplash

versionCode について、ぼくがこれまで見てきたプロジェクトで結構失敗してるケースを見かけてるので、改めて versionCode をどう扱えばよいのかをまとめます。

アンチパターン

往々にして間違いやすいパターンがあり、それは versionName と同じ値を使おうとするパターンです。

例えば以下のような感じです。

versionName を元にメジャー、マイナー、パッチにそれぞれ2桁ずつ割り当てて、それを数値化したものです。

この場合の大きな問題としてはPlay Consoleアップするときに問題になる可能性があります。

例えば、テストトラックで配布したい場合です。特に修正がなく、そのままプロダクションリリースするときは問題はないですが、修正してテストトラックで再配布したいときに versionCode を上げる必要があります。

このとき、 versionCodeversionName と同じような扱いにしてるため、 versionName をどうするかが問題なります。まだプロダクションリリースをしていないにも関わらず、 versionName の値が変わることになります。

ベストプラクティス

ベストプラクティスは非常にシンプルです。

最初のリリースでは versionCode1 を設定して、Play Consoleにアップするたびに versionCode をインクリメントするだけです。

例えば、 1.2.0 が8回目にPlay Consoleにアップするときは以下のようになります。

ドキュメントにも以下のように書いてあります。

Typically, you would release the first version of your app with versionCode set to 1, then monotonically increase the value with each release, regardless of whether the release constitutes a major or minor release.

これだけです!

versionNameを関連付けたい場合

どうしても versionCodeversionName を関連付けたい場合は、以下のように、 メジャー、マイナー、パッチに加えて、ビルド番号みたいな値を追加してください。

計算でやるには以下のようにすると良いでしょう。

versionCodeversionName を関連付けたいときってどういうときかを考えたときに、例えばHTTP ヘッダーに数値として含めたいとか、そういうときじゃないかなと思います。

このときに別に versionCode を使う必要はまったくなく、新しく BuildConfig の値を設定することも検討しましょう。

最後に

versionCode の運用は最初にミスると後々変更することが難しくなってくるものです。しっかりと versionCode が何に使われてるのかを理解しましょう。

参考

https://developer.android.com/studio/publish/versioning


Photo by Andrey Metelev on Unsplash

Coroutines asyncは例外のハンドリングが結構ややこしくハマりどころだと思います。ハンドリングの方法を間違えるとクラッシュを引き起こすこともあります。

asyncを使う際の例外ハンドリングをまとめていきます。

SupervisorJobやsupervisorScopeなどの細かい解説はしてませんので、最後に載せてる参考リンク等で確認してください。

scope.async

CoroutineScopeから直接asyncを使う場合です。

asyncではawaitしたタイミングで例外が発生するため、awaitの処理をtry catchで囲んでいます。この場合は普通にハンドリングできます。

次に複数のasyncを使った場合です。CoroutineScopeのJobによって挙動が変わります。

この例の場合は、通常のJobを使ってるのでそのスコープから起動したasyncで例外が発生すると、他のasyncの処理が自動でキャンセルされます。

次に SupervisorJob を使った例です。

この場合は、どれかのasyncがキャンセルしても、他のasyncはキャンセルされません。

scope.launch内でのasync

CoroutineScopeからlaunchにて新たにCoroutineを起動して、その中でasyncを使う場合です。

この場合はtry catchをしていたとしてもクラッシュします。Coroutineは例外が起きた場合に親に伝播します。この例だとasyncで起きた例外が一番上のCoroutineScopeに伝わりクラッシュすることになります。

awaitでtry catchしてる場合は、実はここも呼び出されますが、結局はasyncから親に伝播されてるので、意味がない感じになってしまいます。

最初に説明したscope.asyncの場合が問題ない理由としては、ルートのCoroutineになってるので伝搬する親がいないためです。

これを対応するにはいくつか方法があります。まずはよく使われる supervisorScope で対応する方法です。

supervisorScope で囲むことで、asyncで起きた例外が親に伝播されることがなくなり、クラッシュを防ぐことができます。この方法がよく紹介されてる気がします。

もう一つよくある対応としては CoroutineExceptionHandler を使う方法があります。


Photo by Kind and Curious on Unsplash

Fragmentの使い方が誤っていないかをチェックする FragmentStrictMode について簡単に紹介します。

Fragment 1.4.0-alpha02で導入されています。
(今回は1.4.0-alpha03で検証しています。

android.os.StrictMode のFragment版という感じになります。

誤りを検出してLog出力する

簡単な例として、 <fragment> タグを使用してる場合にログ出力する実装です。

FragmentStrictMode.Policy を作って、それを setDefaultPolicy でセットしている感じになります。
実装はApplicationクラスで設定しておくと良いともいます。

Policyについてはいくつかありますが、どういったのがあるかは後ほど紹介ます。

もし、レイアウトxmlで <fragment> を使ってる箇所がある場合は、以下のようなログが出力されるようになります。


Photo by La-Rel Easter on Unsplash

Fragment 1.4.0-alpha01 よりMultiple back stacksの対応が導入されました。この対応で新しいAPIが追加されてるので、使い方と振る舞いについて見ていきます。

おそらく、基本的に直接これらのAPIを使うことはなく Navigation Component を通して使うことが多いと思います。

注意

まだリリース直後のalpha01で試しているため、今後変更がある可能性が高いです。

新しいAPI

saveBackStackrestoreBackStack のAPIが新たに追加されていて、これらを使ってMultiple back stacksを実現していきます。

こちらのAPIは addToBackStack で指定した名前を使用します。挙動としては popBackStack と同じような動作になりますが、あとから復元できるようにBack stackを保存します。

saveBackStack で保存したBack stackをあとから復元してFragmentを再表示します。

ドキュメントではBack stackを保存という記述になっていますが、個人的にはdetach/attachに近いのかなって思っています。

使用方法

使用例をいくつか見ながら説明していきます。

まずは単純に2つのFragment使用します。

で、そのあとに、 saveBackStack で Back stackを保存します。

この結果を図にすると以下のようになります。


Photo by Med Badr Chemmaoui on Unsplash

状態管理する方法は色々ありますが、UiStateという状態を一つのclassでまとめて管理する方法を紹介します。
(UiStateの他にはUiModel, ViewStateなど色んな呼び方を見かけます)

UiStateでも大きく2つやり方があるかなと思っています。
sealed class を使ったやり方と data class を使ったやり方です。

sealed class を使った管理

sealed class を使って画面が取り得る状態を定義して、必要なデータは各継承クラスのプロパティに持たせる方法です。

以下のコメントに書いてる通り、各状態を sealed class を継承した data classobject で表現しています。

ViewModelではこれを StateFlowLiveData を使って状態ごとに更新していきます。

Activity/Fragment ではこの変更を受け取れるように実装していきます。

このやり方はAndroid Developerのドキュメントでもやっていたりします。

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow

ぼく個人としてはこのやり方はいくつか問題点があり、あまりやりたくはない方法になります。

上の例で示したとおり、画面全体の状態を表しているので、ちょっと複雑な画面などすぐに破綻します。
例えば、画面にお気に入りボタンがあったりした場合などは、 sealed class の継承するものがどんどん増えていきます。

画面の更新についても、画面全体の状態と表すのでUIを更新する際に冗長になり、またミスが増え要因になるように思えます。
以下に例を示しますが、画面全体の状態ごとにすべてのUIの状態を設定する必要があります。

また、DataBindingをシンプルに使用することができません。

data class を使った状態管理

次に単純な data class を使って管理する方法です。

sealed class のように画面全体の表現するのではなく、UIを表現するのに必要なものをプロパティとして定義するイメージです。

ViewModelは以下のように StateFlowLiveData を使いつつ copy メソッドで変更したいプロパティのみを更新していきます。

Kenji Abe

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store