STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

年末に向けた大掃除 〜Androidアプリのライブラリバージョンアップ〜

こんにちは、モバイルアプリエンジニアの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.xmlpackage属性として名前空間を設定していましたが、build.gradlenamespaceとして設定するように変更しました。

公式ドキュメントによると、AGP7.3以降でマニフェストファイルのpackageを直接設定することが非推奨になったようです。

developer.android.com

【変更前】

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.application" />

【変更後】

android {
    namespace = "com.example.application"
}

uses-featureの追加

今回対象としたアプリでは、uses-permissionとしてCALL_PHONECAMERAを指定していましたが、それぞれに対応するuses-featureの設定がされていなかったため、これを追加しました。

uses-featureとは、ハードウェアおよびソフトウェアの機能要件を満たさない端末をフィルタするものです。

requiredtrueにすると、その機能要件を満たしていない端末ではGoolge Playにアプリが表示されなくなります。

developer.android.com

公式サイトに下記のような注釈がありますが、今回の対応の中でも、不要なフィルタリングを防ぐため、requiredfalseにしました。

注: 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に置き換えました。

developer.android.com

collectAsStateはライフサイクルを考慮せずに使うと、例えばアプリがバックグラウンドにある状態でStateFlowが更新されてもrecompositionされてしまいます。

これを防ぐため、flowWithLifecycleを利用して、Lifecycle.State.STARTEDからLifecycle.State.STOPPEDの範囲外でStateFlowが変更されてもrecompositionされないようにする工夫が必要でした。

collectAsStateWithLifecycleはライフサイクルを考慮したcollectAsStateで、上記のことをデフォルトで行ってくれます。

※下記の記事も非常に参考になりました。

medium.com

【変更前】

val params = viewModel.params.collectAsState()
val showIndicator = viewModel.showIndicator.collectAsState()

【変更後】

val params = viewModel.params.collectAsStateWithLifecycle()
val showIndicator = viewModel.showIndicator.collectAsStateWithLifecycle()

SwipeRefreshの置き換え

今まではAccompanistSwipeRefreshを利用していましたが、これがdeprecatedになったため、ComposepullRefreshに置き換えました。

ただし、ComposepullRefreshはレイアウトを持っていないため、そのまますんなりと置き換えはできず、下記のように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の他に、pagerAccompanistの機能を使っていましたが、こちらもdeprecatedになりました。

SwipeRefreshと同様にComposepagerを使うように変更しました。

こちらは引数の扱いが異なるものの、SwipeRefreshからpullRefreshへの置き換えに比べるとスムーズに移行完了しました。

そしてこのアプリでは、これを持ってAccompanistがお役御免となりました。

【変更前】

val pagerState = rememberPagerState()

【変更後】

val pagerState = rememberPagerState { pageCount }

まとめ

今回対象となったアプリはしばらくライブラリ更新作業が止まっており、このタイミングで一気にバージョンアップする必要がありました。

結果として思ったよりも差分が大きく、なかなか苦労しました。

実際のファイル差分は下記のとおりでした。

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

来年こそは計画的に!