こんにちは、モバイルアプリエンジニアの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 }
まとめ
今回対象となったアプリはしばらくライブラリ更新作業が止まっており、このタイミングで一気にバージョンアップする必要がありました。
結果として思ったよりも差分が大きく、なかなか苦労しました。
実際のファイル差分は下記のとおりでした。
年末を前に片付けられて良かったものの、来年からはもう少し日頃からこまめなメンテナンスを心がけようと思いました。
来年こそは計画的に!