Jetpack Compose Samples でも使われている「Version catalog update plugin」で libs.versions.toml を書き出してみる

一度は見たことがあるでしょう「Jetpack Compose Samples」。

Jetpack Compose Samples

👉 android/compose-samples: Official Jetpack Compose samples. hatena-bookmark

こんな記述があります。

👉 Jetchat/build.gradle.kts#L39-L42 hatena-bookmark

何でしょうか、調べてみましょう。

 

■ Version catalog update plugin

このプラグインは、libs.versions.toml の作成や更新をしてくれるようです。


👉 littlerobots/version-catalog-update-plugin: Gradle plugin for updating a project version catalog hatena-bookmark

一度、書き出して、どのようになるか確かめてみます。

 

■ 作成


./gradlew versionCatalogUpdate --create

作成されました。


[versions]
androidx-appcompat = "1.5.1"
androidx-arch-core = "2.1.0"
androidx-core = "1.8.0"
androidx-emoji2 = "1.2.0"
androidx-vectordrawable = "1.1.0"
org-jetbrains-kotlin = "1.7.20"
org-jetbrains-kotlinx = "1.6.1"

[libraries]
androidx-activity = "androidx.activity:activity:1.5.1"
androidx-annotation = "androidx.annotation:annotation:1.3.0"
androidx-annotation-annotation-experimental = "androidx.annotation:annotation-experimental:1.1.0"
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-appcompat-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" }
androidx-arch-core-core-common = { module = "androidx.arch.core:core-common", version.ref = "androidx-arch-core" }
androidx-arch-core-core-runtime = { module = "androidx.arch.core:core-runtime", version.ref = "androidx-arch-core" }
androidx-cardview = "androidx.cardview:cardview:1.0.0"
androidx-collection = "androidx.collection:collection:1.1.0"
androidx-concurrent-concurrent-futures = "androidx.concurrent:concurrent-futures:1.0.0"
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
androidx-constraintlayout-constraintlayout-core = "androidx.constraintlayout:constraintlayout-core:1.0.4"
androidx-constraintlayout-constraintlayout-solver = "androidx.constraintlayout:constraintlayout-solver:2.0.1"
androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
androidx-core-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-cursoradapter = "androidx.cursoradapter:cursoradapter:1.0.0"
androidx-customview = "androidx.customview:customview:1.1.0"
androidx-documentfile = "androidx.documentfile:documentfile:1.0.0"
androidx-drawerlayout = "androidx.drawerlayout:drawerlayout:1.1.1"
androidx-dynamicanimation = "androidx.dynamicanimation:dynamicanimation:1.0.0"
androidx-emoji2 = { module = "androidx.emoji2:emoji2", version.ref = "androidx-emoji2" }
androidx-emoji2-emoji2-views-helper = { module = "androidx.emoji2:emoji2-views-helper", version.ref = "androidx-emoji2" }
androidx-fragment = "androidx.fragment:fragment:1.3.6"
androidx-interpolator = "androidx.interpolator:interpolator:1.0.0"
androidx-legacy-legacy-support-core-utils = "androidx.legacy:legacy-support-core-utils:1.0.0"
androidx-lifecycle-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.5.1"
androidx-lifecycle-lifecycle-livedata = "androidx.lifecycle:lifecycle-livedata:2.0.0"
androidx-lifecycle-lifecycle-livedata-core = "androidx.lifecycle:lifecycle-livedata-core:2.5.1"
androidx-lifecycle-lifecycle-process = "androidx.lifecycle:lifecycle-process:2.4.1"
androidx-lifecycle-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime:2.5.1"
androidx-lifecycle-lifecycle-viewmodel = "androidx.lifecycle:lifecycle-viewmodel:2.5.1"
androidx-lifecycle-lifecycle-viewmodel-savedstate = "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1"
androidx-loader = "androidx.loader:loader:1.0.0"
androidx-localbroadcastmanager = "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0"
androidx-print = "androidx.print:print:1.0.0"
androidx-recyclerview = "androidx.recyclerview:recyclerview:1.1.0"
androidx-resourceinspection-resourceinspection-annotation = "androidx.resourceinspection:resourceinspection-annotation:1.0.1"
androidx-savedstate = "androidx.savedstate:savedstate:1.2.0"
androidx-startup-startup-runtime = "androidx.startup:startup-runtime:1.1.1"
androidx-test-espresso-espresso-core = "androidx.test.espresso:espresso-core:3.4.0"
androidx-test-ext-junit = "androidx.test.ext:junit:1.1.3"
androidx-tracing = "androidx.tracing:tracing:1.0.0"
androidx-transition = "androidx.transition:transition:1.2.0"
androidx-vectordrawable = { module = "androidx.vectordrawable:vectordrawable", version.ref = "androidx-vectordrawable" }
androidx-vectordrawable-vectordrawable-animated = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "androidx-vectordrawable" }
androidx-versionedparcelable = "androidx.versionedparcelable:versionedparcelable:1.1.1"
androidx-viewpager = "androidx.viewpager:viewpager:1.0.0"
androidx-viewpager2 = "androidx.viewpager2:viewpager2:1.0.0"
com-google-android-material = "com.google.android.material:material:1.7.0"
junit = "junit:junit:4.13.2"
org-apache-logging-log4j-log4j-core = "org.apache.logging.log4j:log4j-core:2.17.1"
org-jacoco-org-jacoco-ant = "org.jacoco:org.jacoco.ant:0.8.7"
org-jetbrains-annotations = "org.jetbrains:annotations:13.0"
org-jetbrains-kotlin-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlinx-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "org-jetbrains-kotlinx" }

[plugins]
com-android-application = "com.android.application:7.3.1"
com-android-library = "com.android.library:7.3.1"
com-github-ben-manes-versions = "com.github.ben-manes.versions:0.41.0"
nl-littlerobots-version-catalog-update = "nl.littlerobots.version-catalog-update:0.7.0"
org-jetbrains-kotlin-android = "org.jetbrains.kotlin.android:1.7.20"

フォーマットが統一され、ソートされたきれいなTOMLファイルです。

キーは、kebab-case で重複処理も上手に行ってくれてるように見えます。

👉 【Gradle Version Catalog】libs.versions.toml キー名の形式 camelCase vs kebab-case hatena-bookmark

build.gradle 側は更新しないようなので新規作成は安全ですが、既存の場合は、


./gradlew versionCatalogUpdate --interactive

とすると、別のファイル名 libs.versions.updates.toml で書き出してくれるようです。

Version catalog update plugin

実行後は gradle ディレクトリを Reload すると作成されてることが確認できます。


[plugins]
# From version 7.3.1 --> 8.0.0-alpha06
android-application = "com.android.application:8.0.0-alpha06"

まずは、参考にまで使ってみました。簡単で便利です。

無駄のない最小限の記述なので、最初の書き出しには使うと良いと思います。

👌 もっと、シンプルでいいような気がするので以下の方法でもやってみました。


👉 「⚠ This project uses Gradle Version Catalogs: this tool may not behave as expected.」→ 今現在、Gradle Version Catalog には gradle-versions-plugin が必須では? hatena-bookmark


Android ViewModel が不要である理由

👉 You Don't Need Android ViewModel, Here is Why hatena-bookmark

ViewModel is one of the most popular building blocks in Android applications, but I don’t use it in my projects. To some Android developers, especially to ones who started their career in the “ViewModel era”, this might sound crazy. Well, I’m not crazy, I promise (though, that’s something a crazy person would probably say). I’m just pragmatic and understand Android architecture well enough to know what’s better for me.

ViewModel は Android アプリケーションで最も使われている構成要素の1つですが、私のプロジェクトでは使用していません。Android開発者、特に「ViewModel時代」にキャリアをスタートさせた開発者にとっては、これは不思議に聞こえるかもしれません。しかし、私は決して狂ってはいません。私はただ現実的で、Android のアーキテクチャを十分に理解しているので、何が自分にとって良いことなのかを知っています。

So, in this post, I’ll explain why you don’t really need ViewModel and show you the alternatives that I use. To organize our discussion here, in the following sections I’ll discuss the main features of ViewModel framework one-by-one. Let’s go.

そこで、この投稿では、ViewModel が本当に必要ない理由を説明し、私が使用している代替を紹介します。ここでの議論を整理するために、以下のセクションでは ViewModel フレームワークの主な機能をひとつずつ説明していきます。それでは参りましょう。

 

■ 関心の分離

Back in the dark ages of Android development (b.v.m – before ViewModel), we would write all our code right inside Activities. That of course, led to projects littered with huge God Classes having many thousands of lines of code inside them. Maintaining Android applications had been hell and many developers committed suicide, so something had to be done about it. Okay, okay, that last part about suicides wasn’t true. In fact, the first part about God Classes wasn’t entirely correct either, but let’s give devil its due.

Android開発の暗黒時代(b.v.m - ViewModel以前)には、すべてのコードを Activity 内に記述していました。当然、その結果、何千行ものコードを含む巨大な神クラスがプロジェクトに散乱することになりました。Androidアプリケーションのメンテナンスは地獄でした。

So, one of the main reasons to use ViewModel is to separate so-called UI logic, which is responsible for “drawing on the screen”, from other application’s concerns:

ViewModel を使う大きな理由のひとつは、「画面に描画する」ことを担う UI ロジックを切り離すことにあります。

Separation of Concerns

In my opinion, this kind of separation is one of the few best practices that you should always use in your projects. However, you don’t need ViewModel framework for that. Any other MVx pattern (MVC, MVP, etc.) will achieve the same objective, without the additional ViewModel-specific complexity. The earliest discussion of MVC in Android that I’m aware of dates back to 2010, and, by 2014, when I joined, MVP was kind of “standard” among experienced developers. So, in terms of separation of concerns, ViewModel is just one of many available solutions.

このような分離は、あなたのプロジェクトで常に使用すべき数少ないベストプラクティスの一つです。しかし、そのために ViewModel フレームワークが必要なわけではありません。他のどの MVx パターン (MVC, MVP など) でも、ViewModel 固有の複雑さを追加することなく、同じ目的を達成することができます。私が知る限り、Android における MVC の最も古い議論は 2010 年にさかのぼり、私が入社した 2014 年には、経験豊富な開発者の間で MVP はある種の「標準」になっていました。つまり、関心の分離という点では、ViewModel は数多くあるソリューションの1つに過ぎないのです。

When it comes to separation of UI logic, I personally use what I call MVC architectural pattern. It’s better than ViewModel-based MVVM because it incorporates the most important architectural insight in Android world – that Activities and Fragments shouldn’t contain UI logic at all.

UI ロジックを分離する場合、私は MVC を使用しています。これは ViewModel ベースの MVVM よりも優れていて、Android の世界で最も重要なアーキテクチャの洞察、つまり Activity と Fragment にはUIロジックを一切含めるべきではないということを取り入れているからです。

 

■ 設定変更時の状態の保持

Now let’s discuss the most important ViewModel’s feature: ViewModel component survives configuration changes.

ここで、ViewModel の最も重要な機能について説明します。ViewModel コンポーネントは設定変更時にも状態を保持します。

To remind you, when something about device’s configuration changes (e.g. user rotates the device), the system will destroy and then re-create all Activities and Fragments. This means that all the objects referenced from those Activities and Fragments exclusively will be “lost”. For example: computed state, data fetched from the network, user input, etc. Obviously, starting from scratch every time a config change occurs isn’t a very good idea, right? Enter ViewModel!

デバイスの構成が変更された場合(ユーザーがデバイスを回転させた場合など)、システムはすべての Activity と Fragment を破棄し、再作成することを忘れないようにしてください。これは、それらの Activity や Fragment から排他的に参照されるすべてのオブジェクトが「失われる」ことを意味します。例えば、計算された状態、ネットワークから取得されたデータ、ユーザー入力などです。明らかに、設定変更のたびにゼロから始めるのはあまり良いアイデアではありませんよね?そこで、ViewModel の登場です!

ViewModel has its own lifecycle, which is “longer” than the lifecycles of individual Activities and Fragments. Due to its longer lifecycle, ViewModel will stick around for as long as any instance of the same “logical” Activity or Fragment is “in scope”. Therefore, when the respective component is re-created, ViewModel will automatically reattach to the new instance. It’s only when the last instance of the same logical Activity or Fragment is destroyed and won’t be re-created in the future (e.g. user navigated back from a screen) that the respective ViewModel will be cleared.

ViewModel には独自のライフサイクルがあり、それは個々の Activity や Fragment のライフサイクルよりも「長い」ものです。その長いライフサイクルにより、ViewModel は同じ「論理的」な Activity や Fragment のインスタンスが「スコープ内」にある限り、ずっと存在し続けることになります。そのため、それぞれのコンポーネントが再作成されると、ViewModel は自動的に新しいインスタンスに再アタッチされます。同じ論理的な Activity や Fragment の最後のインスタンスが破壊され、今後再作成されることがない場合(例えば、ユーザーが画面からナビゲートして戻った場合)にのみ、それぞれの ViewModel はクリアされます。

Schematically, you can visualize the difference between ViewModel and any other “controller” implementation in this manner:

図式的には、ViewModel と他の「コントローラ」実装の違いをこのように視覚化することができます。

(Activity/Fragment lifecycle vs ViewModel lifecycle)

So, it’s ViewModel’s longer lifecycle that allows it to retain the data during configuration changes. However, way too many developers believe that if you don’t use ViewModel, you’ll necessarily be starting from a clean state on each config change. That’s incorrect.

つまり、ViewModel のライフサイクルが長いからこそ、設定変更時にもデータを保持することができるのです。しかし、あまりにも多くの開発者が、ViewModel を使用しない場合、設定変更のたびに必ずクリーンな状態からスタートすることになると考えています。それは間違っています。

First and foremost, pretty much all standard Android Views can retain their state automatically. The only precondition to that is to assign unique IDs to these views in XML. If you create your custom Views, you can achieve the same behavior by implementing onSaveInstanceState and onRestoreInstanceState methods. In fact, you’ll probably want to rely on this mechanism even if you do use ViewModel (e.g. it would be extremely dirty to store input fields state in ViewModel). So, no benefit for ViewModel in the context of UI state.

まず第一に、ほとんどすべての標準的な Android View は、その状態を自動的に保持することができます。そのための唯一の前提条件は、XML でこれらの View に一意のIDを割り当てることです。カスタムView を作成する場合、onSaveInstanceStateとonRestoreInstanceState メソッドを実装することで、同じ動作を実現することができます。実際、ViewModel を使用する場合でも、このメカニズムに依存したいと思うでしょう (例えば、入力フィールドの状態を ViewModel に保存するのは非常に汚いことです)。つまり、UI の状態という文脈では ViewModel の利点はない。

Then there is the state your app fetches from the server. That’s the most common reason developers give when asked why they use ViewModel: “we don’t want to re-execute network request(s) on each config change”. This might sound like a no-brainer, but I’m going to challenge this idea.

そして、アプリケーションがサーバから取得する状態もあります。これは、開発者が ViewModel を使用する理由を尋ねられた際に最もよく挙げる理由です。「設定を変更するたびにネットワークリクエストを再実行したくないから」です。これは当然のことのように聞こえるかもしれませんが、私はこの考え方に反論してみようと思います。

My very first question would be “how often do your users experience config changes?”. The answer, in absolute majority of cases, is “pretty much never”. This is a rare experience for most users (myself included), so it’s not important enough to justify any kind of performance optimization. In other words: you can go ahead and re-fetch all the required data from the server.

最初の質問は「ユーザーはどれくらいの頻度で設定変更を経験するのか」です。答えは、絶対多数のケースで「ほとんどない」です。これは(私を含む)ほとんどのユーザーにとって稀な経験であり、パフォーマンスの最適化を正当化するほど重要ではありません。言い換えれば、サーバーから必要なデータをすべて再取得すればいいのです。

Sure, there are some exceptions, like video streaming and navigation apps. However, in most of these exceptional cases, you wouldn’t store the state inside ViewModels anyway because you wouldn’t want to lose it if the user navigates back from a screen.

もちろん、ビデオストリーミングやナビゲーションアプリのような例外もあります。しかし、これらの例外的なケースのほとんどは、ユーザーが画面をナビゲートして戻ってきたときに状態を失いたくないので、いずれにしても ViewModel 内に状態を保存しないでしょう。

So, in absolute majority of cases, when it comes to preserving network data over config changes, ViewModel is a preliminary optimization. And, like all preliminary optimizations, it can get worse. See, the most puzzling argument in favor of ViewModel is “I want to keep network request alive if config change happens right when it’s executing”. It leaves me speechless because it’s like starting with a rare use case that probably doesn’t need optimization to begin with, and then narrowing it down to something which is much rarer still. My answer is: just re-fetch the damn data!

そのため、設定変更に伴うネットワークデータの保持に関しては、ほとんどの場合、ViewModel は予備的な最適化であると言えます。そして、すべての予備的な最適化と同様に、それは悪化する可能性があります。ViewModel を支持する最も不可解な主張は、「実行中に設定変更があった場合でも、ネットワーク要求を維持したい」というものです。そもそも最適化が必要ないような稀なユースケースから始めて、さらにもっと稀なものに絞り込むようなものですから、言葉を失いますね。私の答えは、「データを再取得すればいい!」です。

Said all that, there are few valid use cases when you’d legitimately want to optimize your code. In addition, some application might have a computed state that needs to be preserved as well. Surely, now you’d need ViewModel, right? Nope. From keeping the data globally (in Application scope), to using a Service, to retaining data through onSaveInstanceState – you’ve got many other options at your disposal. Not to mention that you could always implement a “retained controller”, which is exactly what ViewModel is, using retained headless Fragments or onRetainCustomNonConfigurationInstance method (Google deprecated both approaches to leave no competition for ViewModel, but they still work).

とはいえ、コードを最適化したいと思うような有効なユースケースはほとんどない。さらに、アプリケーションによっては、計算された状態も保存する必要がある場合があります。確かに、今なら ViewModel が必要でしょう?いいえ、そうではありません。グローバルにデータを保持する (アプリケーション スコープ)、サービスを使用する、onSaveInstanceState によってデータを保持するなど、他にも自由に使えるオプションがたくさんあります。言うまでもなく、リテインされたヘッドレスフラグメントや onRetainCustomNonConfigurationInstance メソッドを使って、まさに ViewModel のような「リテインされたコントローラ」を実装することができます(Google は ViewModel の競合を排除するために両方のアプローチを廃止しましたが、これらはまだ動作します)。

Lastly, let’s discuss real performance optimizations of config changes. The truth is that the most aggressive optimization strategy in this context renders ViewModel completely obsolete. I’m referring to the manual handling of config changes using android:configChanges option in activity tag inside AndroidManifest.xml. Most developers either don’t know about this option, or think that it’s intended to avoid dealing with config changes at all. But what this option actually does, is allowing you to tell Android not to kill your Activities (and, subsequently, Fragments) when configuration changes occur. After all, if you want top performance, sparing the need to re-create all these heavyweight components sounds like the only reasonable thing to do. Sure, this path requires more work and experience, but that’s always the case with real performance optimizations. So, if you really in need of performance, that’s what you should use. And when you use this option, there is zero benefit in using ViewModel as opposed to any other MVx implementation (because everything is retained anyway).

最後に、設定変更の実際のパフォーマンス最適化について説明します。実は、この文脈で最も積極的な最適化戦略は、ViewModel を完全に陳腐化させます。AndroidManifest.xml 内の activity タグで android:configChanges オプションを使用して、設定変更を手動で処理することを指しているのです。ほとんどの開発者はこのオプションについて知らないか、あるいは、設定変更の処理をまったく行わないようにするためのものだと考えています。しかし、このオプションが実際に行うことは、設定変更が発生したときにアクティビティ(と、それに続くフラグメント)を停止しないように、Androidに指示することです。結局のところ、最高のパフォーマンスを求めるなら、これらの重いコンポーネントをすべて再作成する必要性を免れることは、唯一の合理的なことのように聞こえます。もちろん、この方法はより多くの作業と経験を必要としますが、それは本当のパフォーマンスの最適化では常にそうなのです。ですから、本当にパフォーマンスが必要な場合は、この方法を使うべきでしょう。また、このオプションを使用する場合、他の MVx 実装と比較して ViewModel を使用する利点は全くありません (なぜなら、どのみち全てが保持されるからです)。

All in all, the bottom line is that you probably don’t need to optimize your config changes, but, even if you do, you probably don’t need ViewModel.

結局のところ、設定変更を最適化する必要はないだろうが、仮に必要だとしても ViewModel は必要ないだろう、ということです。

 

■ 論理的な画面から離れると自動クリアされる

Another common benefit attributed to ViewModel is that its onCleared method is called when the logical scope of the enclosing Activity/Fragment disappears. As far as I know, there is no alternative to this callback among Android APIs, so it’s a unique feature.

ViewModel のもう一つの特徴として、Activity/Fragment の論理スコープから外れたときに onCleared メソッドが呼び出されることが挙げられます。私の知る限り、このコールバックに代わるものは Android API には存在しないので、ユニークな機能だと思います。

However, in practice, I’ve never needed this callback. Therefore, while I’m sure there are some “clever” use cases for it, it’s just yet another “niche” API that should be reserved for special circumstances, rather than being touted as a “best practice”.

しかし、実際には、このコールバックが必要になったことは一度もありません。したがって「賢い」使用法があることは確かだが、「ベスト・プラクティス」として宣伝されるよりは、特別な状況のために確保されるべき、また別の「ニッチ」なAPIに過ぎないのである。

 

■ 自動管理されたコルーチンのスコープ

Lastly, many developers seem to find additional benefits in viewModelScope property, which is an extension for ViewModel class. The argument goes along the lines of “since this CoroutineScope is cleared in onCleared callback, it must be superior to other approaches”.

最後に、多くの開発者は ViewModel クラスの拡張である viewModelScope プロパティにさらなる利点を見出しているようです。このCoroutineScope は onCleared コールバックでクリアされるので、他のアプローチより優れているに違いない、というのがその主張です。

Well, let’s acknowledge the ugly truth about Kotlin Coroutines: there is no single “best” time to cancel your Coroutine Scopes. In fact, the need to take care of cancellation is, by itself, the biggest problem of Coroutines. For example, if your application involves drawings on Canvas and you’d want to save them when the user leaves the respective screen, using viewModelScope can cause serious bugs and lead to user data loss. Many developers don’t understand that. Ooops.

しかし、Kotlin の coroutine に関する醜い真実は、coroutine のスコープをキャンセルする唯一のベストなタイミングがないことです。実際、キャンセルの必要性自体が、コルーチンの最大の問題点です。例えば、Canvas 上に描画するアプリケーションで、ユーザが画面を離れた時に保存したい場合、ViewModelScope を使用すると、深刻なバグを引き起こし、ユーザデータの損失につながる可能性があります。多くの開発者はそのことを理解していないのです。

In my opinion, non-cancellable GlobalScope would be much better default Coroutine Scope to use inside ViewModels. So, the “clever” management of viewModelScope is actually a foot-gun (time-bomb), not a benefit.

個人的には、キャンセルできない GlobalScope の方が、ViewModel 内部で使うデフォルトの Coroutine スコープとしてはずっと良いのではないかと思います。というわけで、viewModelScopeの「賢い」管理は、実はメリットではなく、足かせ(時限爆弾)なのです。

 

■ まとめ

My criticism of ViewModel isn’t new. I was among the first to point out the problems with this API when it had been released and correctly predicted its unfortunate fate. Since then, I had worked on quite a bit of apps in various business domains (finance, medicine, social, productivity) and I didn’t need ViewModel even once.

私が ViewModel を批判するのは新しいことではありません。私はこの API がリリースされた時、その問題点をいち早く指摘し、その不幸な運命を正確に予測しました。それ以来、私は様々なビジネス領域 (金融、医療、社会、生産性) のアプリケーションに携わってきましたが、ViewModel を必要としたことは一度もありませんでした。

In this article I summarized the mindsets and the technical alternatives that you can use to avoid ViewModel in your projects. In my opinion, avoiding this component is a major benefit for long-term maintainability because ViewModel brings too much complexity into projects and have too many associated pitfalls. There was another official API that dealt with optimization of config changes – Loaders. We all know how this ended (new Android developers might not even know what Loaders are), and, sooner or later, ViewModel will share Loaders’ fate.

この記事では、プロジェクトで ViewModel を使用しないための考え方と技術的な代替案についてまとめました。私見では、ViewModel はプロジェクトに複雑さをもたらし、多くの落とし穴があるため、このコンポーネントを避けることは長期的な保守性において大きな利点となります。もう一つ、設定変更の最適化を扱う公式APIがありました。それが Loader です。私達はこの API がどのように終わったかを知っていますし(新しい Android 開発者は Loader が何であるかさえ知らないかもしれません)、遅かれ早かれ ViewModel は Loader と運命を共にすることになるでしょう。

It’s not a coincidence that, on the question of ViewModel’s utility, my opinion is echoed by Jake Wharton’s sentiment. This doesn’t happen very often, so it’s another sign of how amazingly useless and harmful ViewModels are.

ViewModel の実用性の問題に関して、私の意見が Jake Wharton 氏の意見と同じであることは偶然ではありません。このようなことはあまりないので、これも ViewModel がいかに素晴らしく無駄で有害なものであるかを示していると言えるでしょう。

👉 ViewModel を捨てて マルチプラットフォーム に備える hatena-bookmark


ViewModel を捨てて マルチプラットフォーム に備える

AAC ViewModel

どの Compose バージョンでもシームレスに動作するマルチプラットフォーム ViewModel のようなものをお考えでしょうか?

AAC ViewModel は Android 用の雑なもので、悪いパターンを増殖させる理由はないと思います。データレイヤーが適切に設計されていれば、AAC ViewModel を使う必要がないことに気づくはずです。

しかし、おそらく一番良いのは、アプリが本物のデータ層(キャッシュ、ネットワーク層など)を持つことです。 ViewModel はデータレイヤーを参照するかもしれませんが、ViewModel 自身はすべてのインタラクションを直接処理するべきではなく、プラットフォームに依存しないデータレイヤーがそれを行うべきです。

@JimSproch
Senior software engineer at Google. Progenitor of Jetpack Compose (May 2017). Now working on giving Compose its next-generation super-power.

👉 Jim Sproch(@JimSproch)さん / Twitter hatena-bookmark

クラッジ kludge
その場しのぎに間に合わせで採る安易な方法。またそうした問題回避。特にコンピューターのプログラミングやシステム構築で,とりあえず動くが不調和な組み合わせを持ったその場しのぎのもの。

AAC ViewModel の件、全く同感です。AAC ViewModel を不要にするような形で Compose が考案されたことは喜ばしいことです。
しかし、Compose の成功のためには、マルチプラットフォームのアーキテクチャパターンをいくつか考え出すことが本当に重要だと思います。

印刷して額に入れよう。

あとは、独占しているコンフィグ不要のしくみを取り除いて、人々に返すだけです。
👉 android:configChanges - Android デベロッパー  |  Android Developers hatena-bookmark

「なぜ?」と思われる方のために、もう少し詳しく説明します。

AndroidのViewModelは不要、その理由は?
ViewModel は Android アプリケーションで最も人気のある構成要素の一つですが、私は自分のプロジェクトでは使っていません。Android 開発者の中には、特に「ViewModel 時代」にキャリアをスタートさせた人にとっては、これはクレイジーに聞こえるかもしれません。

最初の導入時、ViewModel は素晴らしかったのですが、今は Navigation Component があり、それを異なる場所に配置しなければならないので、少し混乱しています。しかし、赤ん坊を風呂の水と一緒に捨てるわけにはいかないと思います。

私達は、これらのライブラリはどちらも使用しません。

throw the baby out with the bath water

意味・対訳
大事なものを無用なものといっしょに捨てる

では、AAC ViewModel の優れた代替手段は何ですか?

必要ない、というのが元のツイートのポイントです。ドメインに特化したデータレイヤーが欲しいなら、画面に特化したプレゼンターとレンダーレイヤーを用意すれば良いです。Compose UI やクラシックビューでやればいい。AAC ViewModelは常に奇妙なボルトオンのソリューションでした。

いくつかのプロジェクトをKMMに移行したいのですが、AAC ViewModel を捨てれば、おそらく作業はよりシンプルになると思います。

👉 Android ViewModel が不要である理由 hatena-bookmark
👉 【MVVM】 Kotlin Flow で使える5つの利用パターン hatena-bookmark