カテゴリー: Android

[Android] ViewModelの書き方

Androidアプリを作る上でViewModelをどれだけうまく書くかがアプリを長く運営していく上で大事になってきます。

昔書いた記事はこちら

ViewModelがAndroidアプリの生命線であり、設計良く保ち、テストコードを書くことでバグを少なく、変更にも対応しやすく変更失敗率も減少し、生産性の向上に繋がります。逆にViewModelが良くないとテストコードも書けず、変更にも対応しにくく変更失敗率が増加し、生産性は下降します。

ViewModelを実装する上でどのように書いたら良いか、海外の記事でうまくまとめられているので1個ずつルールに従っていくと良いViewModelが書けます。2024年6月時点でpart3まであります。

https://proandroiddev.com/mastering-android-viewmodels-essential-dos-and-donts-part-1-%EF%B8%8F-bdf05287bca9

https://proandroiddev.com/mastering-android-viewmodels-essential-dos-and-donts-part-2-%EF%B8%8F-2b49281f0029

https://proandroiddev.com/mastering-android-viewmodels-essential-dos-and-donts-part-3-%EF%B8%8F3%EF%B8%8F%E2%83%A3-1833ce3ddd2b

技術書典16ではmhidakaさんがViewModelの書き方の本を出しているのでこちらもおすすめです。

https://techbooster.booth.pm/items/5748877

内容が少し古いですが釘宮さんの下記の書籍も設計の全体像を把握しやすいのでおすすめです。

https://peaks.cc/books/architecture_with_team


[Android] ProductFlavorのデフォルトを変更する

AndroidStudioのBuildVariantでビルド方法を変更できます。コードをgit cloneしたばかりとかAndroidStudioを再起動したときにBuildVariantがリセットしたりします。そのときに間違ったBuildVariantでビルドしないように普段開発で使うビルド方法をデフォルトにできます。

build.gradle.ktsを下記のように変更し、sync project with gradle filesを実行します。

android {
  buildTypes {
    getByName("release") {
      isDefault = true
    }
  }
}

buildTypesがreleaseの方をデフォルトに変更できました。productFlavorsでも同じことが出来ます。大きいプロジェクトになるとビルド時間が5分を超えるところもあり、意図しないBuildVariantでビルドしてしまうと中止したとしても時間を無駄にしてしまうので、このような細かいところもちゃんとデフォルト設定しておきたいです。


Retrofit2にBOMが追加されました

https://github.com/square/retrofit/releases/tag/2.10.0

https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit-bom/2.10.0

retrofit:2.10.0からBOM機能が追加されました。

retrofit2の関連ライブラリがいくつかありますが、都度バージョンを確認するのも面倒ですし、バージョン間の依存関係も複雑になりがちなのでBOMを使うことがおすすめされています。

利用例

build.gradle.kts

dependencies {
    implementation(platform(“com.squareup.retrofit2:retrofit-bom:2.10.0”))
    implementation(“com.squareup.retrofit2:converter-jackson”)
}

BOMは他にもcomposeやfirebaseにもあるので積極的に使っていきましょう。


[JetpackCompose] NestedScrollの状態でLazyVerticalGridのスクロールリスナーを設定する

LazyVerticalGrid(LazyHorizontalGrid)やLazyColumnでも同様かと思いますがスクロールしたときに他のCompose(View)の表示非表示を切り替えたりしたい場合があります。スクロール状態を取得できるComposeの引数があるかなと思いましたが、少し調べるとModifierでやるようなのでメモしておきます。

val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
            if (available.y >= 0) { /*上にスクロール*/ }
            else { /*下にスクロール*/ } 
            return Offset.Zero
        }
    }
}

LazyVerticalGrid(
    modifier = Modifier.nestedScroll(nestedScrollConnection),
    columns = GridCells.Adaptive(400.dp),
    content = {
        items(listItems) { index ->
            val item = listItems[index]
            // itemを使って表示する内容をここに書く
        }
    }
)
        

if (available.y > 0)にしてしまうとオーバースクロールでも反応してしまうのでif (available.y >= 0)にします。NestedScrollは実装がうまくいかないことも多いので調べながら少しづつやっていこう…。DroidKaigi2023でも発表している人がいたからみんな苦労している感じがする


[Android] シングルモジュールのプロジェクトでDetektによる静的解析をする

巷には多くの静的解析ツールがあります。無料でAndroidのプロジェクトを解析したい場合はDetektがおすすめです。

https://github.com/detekt/detekt

導入も簡単でした。今回はシングルモジュールの場合の導入方法を記載します。appモジュールのbuild.gradle.ktsに下記を追加します。

plugins {
  ....
   id("io.gitlab.arturbosch.detekt").version("1.23.5")
}

detekt {
    // 並列処理
    parallel = true

    // 違反があった場合failさせない
    ignoreFailures = true

    // レポートファイルのバスを相対パスにする
    basePath = rootDir.absolutePath
}

ここで一旦sync project with gradle filesを実行します。すると./gradlew detektが実行できるようになっているのでターミナルで実行します。Detektの結果はモジュール内のbuild/reports/detekt内に出力されます。detekt.html, detekt.md, detekt.sarif, detekt.txt, detekt.xmlがあるのであとは煮るなり焼くなり…


HTTP Toolkitを使って通信内容を書き換える

HTTP/HTTPSで通信するアプリで動作確認等をしたりする場合、環境によってはFakeモジュールで固定の通信結果を返したり、ローカルPCにサーバを立てて通信内容を確認/返却したり、色々な方法があると思いますがHTTP Toolkitを使うとVPNを使って通信内容を書き換えることができ、動作確認等に便利です。GoogleでHTTP Toolkitで検索しても日本語の検索結果があまり出てこないので使い方だけ覚書しておきます。今回はmac版で導入します。

https://httptoolkit.com

Download free nowをクリックします。

自動でダウンロードされない場合はClick hereを押下します。

ダウンロードしたdmgファイルを開きます。

HTTP Toolkit.appをApplicationsフォルダに移動します。(普通にインストールします)

HTTP Toolkitを実行します。

まずはアプリがインストールされている通信内容を確認しましょう。ADBが使われるのでUSBデバッグ可能な状態でAndroid端末をmacにUSB接続します。InterceptタブにあるAndroid Device via ADBをクリックします。

HTTP ToolkitはVPNを利用して通信内容をインターセプトするのでOKを押します。

CA証明書をインストールする必要があるのでそのままOKを押します。

そのままOKを押します。

Connectedと表示されれば成功です。

試しにTest interceptionを押すとViewタブで通信内容を確認できます。

通信内容を書き換える場合は書き換えたいURLの一部をコピーするなり文字列入力できるようにしてMockタブを開きます。

今回はすべてのリクエストを対象にしたいのでAny requestsを選択し、FOR URLS MATCHINGでextensionsと入力します。このようにするとリクエスト先URLにextensionsが含まれる通信に対してインターセプトできます。忘れずに+ボタンを押します。レスポンスの内容を書き換えたいのでPause the response to manually edit itを選択します。リクエストを書き換えたい場合はPause the resquest to manually edit itを選択します。最後に右上のSave changesを押すと設定が適用されます。+ボタンとSave changesボタンは忘れがちなので必ず押してください。

その後、Match:のルールに書いたリクエストが有効になるような操作をAndroid端末で行います。するとViewタブでインターセプト中である画面に切り替わります。

今回はレスポンスを書き換えたいのでRESPONSE BODYの内容を書き換えてResumeボタンを押すと書き換えたレスポンスがAndroidアプリに送られます。(Mockタブでリクエストを書き換える設定にしている場合はREQUEST BODYの内容を書き換えます)ステータスコード401,402とかも発生されるのが難しいですがHTTP Toolkitで簡単に確認できます。

使い方は簡単ですが、いくつかの注意点があります。

  • Mockタブで+ボタンやSave changesボタンを押し忘れることが多い(入力したら自動でインターセプトしてほしい)
  • 無料版ではMockタブの設定内容はアプリ終了と共に削除されるので毎回入力しないといけない
  • アプリ側で通信タイムアウトする前に操作しないといけないのでコピー&ペーストするなり、素早い操作が必要になる
  • Android端末で2重VPNを使うことができないのでリモートワーク等で会社のVPNに繋ぎつつ、HTTP Toolkitと併用することができない。(ホストPC側で会社のVPNに繋いでエミュレータでHTTP Toolkitを使うことは可能です)
  • Android14は2024年1月時点で利用できません。https://httptoolkit.com/blog/android-14-breaks-system-certificate-installation/#enter-android-14

KAPTからKSPに移行したときのメモ

https://developer.android.com/studio/build/migrate-to-ksp?hl=ja

ルートのbuild.gradle.ktsの一番下に下記を追加する

plugins {
    id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
}

“1.9.20-1.0.14″のハイフンの左側はkotlinのバージョンと一致しています。kotlinのバージョンに合わせてどのバージョンが必要かgithubから確認します。

https://github.com/google/ksp/releases

kaptをkspに変更する

id("kotlin-kapt")
↓
id("com.google.devtools.ksp")

kapt("xxxxxx")
↓
ksp("xxxxx")

下記を削除
kapt {
    correctErrorTypes true
    useBuildCache true
}

必要最低限の変更はこれだけですが、プロジェクトによっては関連ライブラリのバージョンアップも必要になるのでそれぞれ確認する。例としてDagger、Room、Hilt、HiltWorker、Moshi、Epoxy等

kaptからkspにするとビルド速度が早くなるって書いてある記事があったけど、あまり変わらないですね…。大規模プロジェクトだと数十秒早くなるのだろうか。


setHasOptionsMenu()、onCreateOptionsMenu()、onOptionsItemSelected()あたりのdeprecated対応でMenuProviderに置き換える

メニュー関連の関数がdeprecatedになったので対応しないといけなくなりました。

https://developer.android.com/reference/androidx/core/view/MenuProvider

変更前

class MyFragment : Fragment() {
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.fragment_my, menu)
        // menuの初期化処理
    }

    override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
        when (menuItem) {
            R.id.menu1 -> {
                // menu1が押されたときの処理
            }
        }
        return super.onOptionsItemSelected(menuItem)
    }
}

変更後

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupMenus()
    }

    private fun setupMenus() {
        (requireActivity() as MenuHost).addMenuProvider(object : MenuProvider {
            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
                menuInflater.inflate(R.menu.fragment_my, menu)
                // menuの初期化処理
            }

            override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                when (menuItem.itemId) {
                    R.id.menu1 -> {
                        // menu1が押されたときの処理
                    }
                }
                return true // 押下したイベントを消費する場合はtrue、それ以外の場合はfalse
            }
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)
    }
}

MenuProviderにはonPrepareMenu()とonMenuClosed()があるのでもう少し細かい制御が必要な場合はこのあたりを使うと良いかも。


[Android] いい感じに文節を区切って表示する

TextViewで文字列を表示するときに日本語の文節毎に改行などがされていないと読みにくい見た目になってしまいます。そういう場合はTextViewの属性にandroid:lineBreakWordStyle=”phrase”を指定するといい感じに文節を区切って表示してくれます。lineBreakWordStyleはAPI LEVEL 33以降ではないと利用できません。

試しに実装してみましたが「タッチスクリーンモバイルデバイス」あたりがおかしいですね。全部カタカナだから文節の判定がしにくかったということだろうか。

表示を分節に区切るのではなくて文字列を分節に区切ってほしいという需要もあるかと思いますがそういうときはBudouXというライブラリを使います。

https://developers-jp.googleblog.com/2023/09/budoux-adobe.html

implementation("com.google.budoux:budoux:0.5.2") // バージョンは適宜調べてください
import com.google.budoux:budoux:

val phraseList = Parser.loadDefaultJapaneseParser().parse("長い文章を文節に区切ってほしい")

phraseListにList<String>型で文節に区切られた文字列が入っています。あとはお好きに加工したりすれば良いかと思います。


AndroidStudioでパッチを作成する

通常の開発ではあまり使わないと思いますが、CI実行時にコードを変更してからビルドしたい、またその際にミスしないようにしたいなどの時にパッチを用意しておくと便利です。まずはパッチにしたい内容にコードを変更します。

import android.util.LogとLog.i(“launch”, “起動したよ”)を追加しました。コードの変更や削除でも良いです。

Shift2回を押下して[Create Patch from Local Changes…]を選択するかAndroidStudio上部の[Git] – [Patch] – [Create Patch from Local Changes…]を選択します。

パッチにしたいファイル、CommitMessageに変更内容を記載、Diffで変更内容を確認し、[Create Patch…]をクリックします。

出力ファイル名を確認して[OK]を押下します。

以上でパッチが作成されました。動作確認のために一旦変更を削除します。(git checkout .を実行する等)

パッチ適用はターミナルで[git apply パッチファイル名]を実行します。今回の場合だと[git apply 起動確認ログ.patch]と入力するとコードが自動で書き換わります。またAndroidStudioからパッチを適用するにはShift2回押下して[Apply Patch…]を選択するかAndroidStudio上部の[Git] – [Patch] – [Apply Patch…]を選択してください。

適用したいパッチのファイルを選択して[Open]を押下します。

OKを押下するとパッチが適用されます。

パッチファイルをGitレポジトリに入れておけばCIからgit applyコマンドでパッチ適用できます。

説明は以上になります。