月: 2022年3月

InAppUpdatesを実装し、GooglePlayConsoleの内部アプリ共有(internal app-sharing)機能を使ってテストする

アプリのアップデートを早めに浸透させる為にinAppUpdatesを使ってみましょう。

https://developer.android.com/guide/playcore/in-app-updates

inAppUpdateはFlexibleとImmediateの2つがあります。Flexibleはアップデートの進捗を表示したり細かい制御が可能です。Immediateはただアップデートするだけと考え、概ね差し支えないと思います。ユーザがアップデートのキャンセルが可能な為、どちらの場合も「強制アップデート」はできないです。とりあえずInAppUpdatesをすぐに導入したいだけならImmediateを使うと簡単なので、今回はImmediateで実装します。

InAppUpdatesを実行するActivityが属するモジュールのbuild.gradle.ktsに下記を記述し、SyncProjectWithGradleFilesを実行します。今回は下記のバージョンを使っていますが、バージョン上がる毎に機能が増えたりしているので、該当バージョンと実装方法は確認しましょう。またktxを使う場合と使わない場合でコードが少し異なるので、そこも注意です。

implementation("com.google.android.play:core:1.10.3")
implementation("com.google.android.play:core-ktx:1.8.1")

ActivityにinAppUpdatesを導入する最小のコードは下記になるかと思います。

class MainActivity : AppCompatActivity() {
    companion object {
        const val IN_APP_UPDATE_REQUEST_CODE = 32
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        checkInAppUpdates()
    }
    private fun checkInAppUpdates() {
        val manager = AppUpdateManagerFactory.create(this)
        manager.appUpdateInfo.addOnSuccessListener { info ->
            if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isImmediateUpdateAllowed)
                manager.startUpdateFlowForResult(
                        info, AppUpdateType.IMMEDIATE, this, IN_APP_UPDATE_REQUEST_CODE
                )
        }
    }
}

これをテストする方法はいくつかあると思うのですが、GooglePlayConsoleの内部アプリ共有機能を使ってテストします。「内部テスト」という似た名前の項目があるので間違えずに「内部アプリ共有」を選択し、テスターの管理を済ませておきます。

テスト手順を順番に書くと

1.リリース予定のVersionCodeでBuildVariantをアプリリリース時と同じ署名でaabファイルを作成する(署名は同じでも既にストアに上がっているアプリとは別のアプリとして扱われるので本番のアプリには影響ありません)

2. 1で作成したaabファイルを内部アプリ共有にアップロードする(https://play.google.com/console/internal-app-sharing)
3. 2でアップロードしたアプリを端末にインストールする(画面右下のコピーボタンからURLリンクの共有をすることでインストール可能です

4. VersionCodeをリリース予定のVersionCodeより大きい数値にし、BuildVariantをアプリリリース時と同じ署名でaabファイルを作成する

5. 4で作成したaabファイルを内部アプリ共有にアップロードする

6. 5でアップロードしたaabファイルのリンクを3でインストールした端末と同じ端末に共有しリンクを開く。ただしここではアプリをインストールしないこと。この操作によりVersionCodeが大きいアプリがPlayStoreにアップロードされていることをアプリが認識できるようになります。

7. 3でインストールしたアプリを起動する

8. inAppUpdatesのUIが表示される

この手順でInAppUpdatesを内部アプリ共有で動作確認することが可能です。


[Android]Roomで引数Booleanによって条件分岐させてSQL結果のソート順を切り替える

前回の続き

@Query("SELECT * FROM table_name WHERE package_name LIKE '%' || :query || '%' OR column_name LIKE '%' || :query || '%' ORDER BY CASE WHEN :isDescending = 0 THEN column_name END ASC, CASE WHEN :isDescending = 1 THEN column_name END DESC")
suspend fun search(query: String, isDescending: Boolean): List<Entity>

isDescending==trueの場合はcolumn_nameの降順、isDescending==falseの場合はcolumn_nameの昇順になります。

参考:https://stackoverflow.com/questions/55297165/room-dao-order-by-asc-or-desc-variable


[Android]RoomでLIKEを使って曖昧検索するときのSQL

RoomでSQLを指定して曖昧検索をする時は次のように書くようです。

@Query("SELECT * FROM table_name WHERE name LIKE '%' || :query || '%'")
fun loadEntities(query: String): List<Entity>

これ何記法ってい言うんだろう…。SQLite固有の記法でも一般的なSQLの記法でもない気がするのですが…。Roomの記法なんですかね。

参考:https://stackoverflow.com/questions/44184769/android-room-select-query-with-like


Espressoでテストコードが書けないUIライブラリにMatcherを用意してテストコードを書けるようにする

オープンソースのUIライブラリを使っているとFragmentのユニットテストやGUIテストを書けない場合があります。今回は例として下記のUIライブラリのテストコードを書けるようにします。

https://github.com/bitvale/Switcher

このライブラリはマテリアルデザインのSwitchを置き換える目的で使っていたのですが、UIテストがうまく動きませんでした。動かなかったテストコードは以下になります。

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import java.util.*

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SwitcherFragmentSpec {
    @Before
    fun setUp() {
        // 必要な初期化処理
    }

    private fun launchFragment() {
        launchFragmentInContainer<SwitcherFragment>(themeResId = R.style.Theme_MaterialComponents_Light)
    }

    @Test
    fun onResume_shouldShowParameters() {
        launchFragment()
        // SwitcherXがチェックされているか
        onView(withId(R.id.switcherx)).check(matches(isChecked()))
        // SwitcherXがチェックされていないか
        onView(withId(R.id.switcherx)).check(matches(isNotChecked()))
    }

    @After
    fun tearDown() {
        // 必要な終了処理
    }
}

一見するとこのコードは動きそうですが、SwitcherXがチェックされていてもmatches(isChecked())で失敗するか、SwitcherXのチェックがされていなくてもmatches(isNotChecked())で失敗します。

これはそもそもライブラリがCheckBoxやSwitchを継承しているのではなく、Viewを継承しているために発生しています。

https://github.com/bitvale/Switcher/blob/568ea7a4153b3d2e530ee64d7e5fb71c3ea1189d/library/src/main/java/com/bitvale/switcher/Switcher.kt#L30

つまりmatches(isChecked())やmatches(isNotChecked())はCheckboxやSwitchを継承したViewでのみ動作するのですが、SwitcherXはViewを継承しているので正しく動作しないです。こういうときはどうするかというとEspresso用にMatcherを用意することで正しくテストコードを動作させることができます。

import android.view.View
import androidx.test.espresso.matcher.BoundedMatcher
import com.bitvale.switcher.Switcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.core.Is.`is`

object SwitcherMatcher {
    fun isChecked(checked: Boolean): IsCheckedMatcher =
            IsCheckedMatcher(`is`(checked))

    class IsCheckedMatcher(private val matcher: Matcher<Boolean>) :
            BoundedMatcher<View, Switcher>(Switcher::class.java) {
        override fun describeTo(description: Description?) {
            description?.appendText("is checked: ")
        }

        override fun matchesSafely(view: Switcher?): Boolean =
                matcher.matches(view?.isChecked)
    }
}

このようにBoundedMatcherを継承したIsCheckedMatcherクラスを作成し、view?.isCheckedでチェックされているかのマッチャーを作成しています。

テストコードで使用する場合は下記のようにします。

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import java.util.*

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SwitcherFragmentSpec {
    @Before
    fun setUp() {
        // 必要な初期化処理
    }
    
    private fun launchFragment() {
        launchFragmentInContainer<SwitcherFragment>(themeResId = R.style.Theme_MaterialComponents_Light)
    }

    @Test
    fun onResume_shouldShowParameters() {
        launchFragment()
        // SwitcherXがチェックされているか
        onView(withId(R.id.switcherx)).check(matches(SwitcherMatcher.isChecked(true)))
        // SwitcherXがチェックされていないか
        onView(withId(R.id.switcherx)).check(matches(SwitcherMatcher.isChecked(false)))
    }

    @After
    fun tearDown() {
        // 必要な終了処理
    }
}

これでUIのテストができるようになります。UIライブラリにゲッターメソッドがあれば独自のMatcherを作れるので他のUIライブラリでも同じようにMatcherを作成しテストコードが書けると思います。