
こんにちは、モバイルアプリエンジニアのnekoです。
今回は STORES ADVENT CALENDAR 10日目の記事として、先日行ったKotlinを始めとしたAndroidアプリのライブラリバージョンアップの話を書きたいと思います。
概要
まず、概要として、主なバージョン変更箇所は下記のとおりです。
その他、依存するライブラリ群もアップデートを行いました。
| 対象 | 変更前バージョン | 変更後バージョン |
|---|---|---|
| Kotlin | 1.7.20 | 1.9.20 |
| Gradle | 7.2.2 | 8.1.2 |
| Coroutines | 1.6.1 | 1.7.3 |
| Compose | 1.3.1 | 1.5.0 |
| targetSdkVersion | 33 | 34 |
| JVM | 11 | 17 |
主な変更点
名前空間の指定
AndroidManifest.xmlのpackage属性として名前空間を設定していましたが、build.gradleのnamespaceとして設定するように変更しました。
公式ドキュメントによると、AGP7.3以降でマニフェストファイルのpackageを直接設定することが非推奨になったようです。
【変更前】
<?xml version="1.0" encoding="utf-8"?> <manifest package="com.example.application" />
【変更後】
android {
namespace = "com.example.application"
}
uses-featureの追加
今回対象としたアプリでは、uses-permissionとしてCALL_PHONEとCAMERAを指定していましたが、それぞれに対応するuses-featureの設定がされていなかったため、これを追加しました。
uses-featureとは、ハードウェアおよびソフトウェアの機能要件を満たさない端末をフィルタするものです。
requiredをtrueにすると、その機能要件を満たしていない端末ではGoolge Playにアプリが表示されなくなります。
公式サイトに下記のような注釈がありますが、今回の対応の中でも、不要なフィルタリングを防ぐため、requiredはfalseにしました。
注: Google Play によるアプリの不要なフィルタリングを防ぐため、アプリの動作に必要ないすべてのカメラ機能に android:required="false" を追加してください。この要素を追加しないと、その機能は必要であると見なされ、その機能をサポートしていないデバイスはアプリにアクセスできなくなります。
<uses-feature android:name="android.hardware.telephony" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CAMERA" />
collectAsStateWithLifecycleへの変更
androidx.lifecycle:lifecycleの2.6.0以降でcollectAsStateWithLifecycleが使えるようになりました。
今回の対応の中では、UIレイヤーでcollectAsStateを使っていた箇所は、すべてcollectAsStateWithLifecycleに置き換えました。
collectAsStateはライフサイクルを考慮せずに使うと、例えばアプリがバックグラウンドにある状態でStateFlowが更新されてもrecompositionされてしまいます。
これを防ぐため、flowWithLifecycleを利用して、Lifecycle.State.STARTEDからLifecycle.State.STOPPEDの範囲外でStateFlowが変更されてもrecompositionされないようにする工夫が必要でした。
collectAsStateWithLifecycleはライフサイクルを考慮したcollectAsStateで、上記のことをデフォルトで行ってくれます。
※下記の記事も非常に参考になりました。
【変更前】
val params = viewModel.params.collectAsState() val showIndicator = viewModel.showIndicator.collectAsState()
【変更後】
val params = viewModel.params.collectAsStateWithLifecycle() val showIndicator = viewModel.showIndicator.collectAsStateWithLifecycle()
SwipeRefreshの置き換え
今まではAccompanistのSwipeRefreshを利用していましたが、これがdeprecatedになったため、ComposeのpullRefreshに置き換えました。
ただし、ComposeのpullRefreshはレイアウトを持っていないため、そのまますんなりと置き換えはできず、下記のようにBoxレイアウトで配置するなどの工夫が必要でした。
【変更前】
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = onRefresh
) {
...
}
【変更後】
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh)
Scaffold(topBar = {
....
}
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
) {
...
PullRefreshIndicator(
refreshing = refreshing,
state = pullRefreshState,
modifier = Modifier
.align(Alignment.TopCenter)
)
}
}
pagerの置き換え
SwipeRefreshの他に、pagerもAccompanistの機能を使っていましたが、こちらもdeprecatedになりました。
SwipeRefreshと同様にComposeのpagerを使うように変更しました。
こちらは引数の扱いが異なるものの、SwipeRefreshからpullRefreshへの置き換えに比べるとスムーズに移行完了しました。
そしてこのアプリでは、これを持ってAccompanistがお役御免となりました。
【変更前】
val pagerState = rememberPagerState()
【変更後】
val pagerState = rememberPagerState { pageCount }
まとめ
今回対象となったアプリはしばらくライブラリ更新作業が止まっており、このタイミングで一気にバージョンアップする必要がありました。
結果として思ったよりも差分が大きく、なかなか苦労しました。
実際のファイル差分は下記のとおりでした。

年末を前に片付けられて良かったものの、来年からはもう少し日頃からこまめなメンテナンスを心がけようと思いました。
来年こそは計画的に!