Edited at

viewmodel-ktx 2.1.0で導入されるviewModelScopeを使えばCoroutineScopeを実装しなくてよくなる

androidx.lifecycle:lifecycle-viewmodel-ktx の 2.1.0 (2019年1月1日時点でまだ alpha01 ですが) から、ViewModelviewModelScope という CoroutineScope 型の拡張プロパティが導入されます。これを使うと、ViewModel において CoroutineScope を実装1しなくても、ViewModel.onCleared() のタイミングでキャンセルされる CoroutineScope が利用できるようになります。

// 2.1.0 より前

class MyViewModel : ViewModel(), CoroutineScope {
private val job = Job()
override val coroutineContext = Dispatchers.Main + job

override fun onCleared() {
super.onCleared()
job.cancel()
}

fun doSomething() {
launch {
// ...
}
}
}

// 2.1.0 以降

class MyViewModel : ViewModel() {
fun doSomething() {
viewModelScope.launch {
// ...
}
}
}


仕組み

viewModelScope の内容は Job() + Dispatchers.Main です。 viewModelScope プロパティへの初回アクセス時にインスタンスが作成され、ViewModel のタグとして保持されます (ソースコード)。


lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModel.kt

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(Job() + Dispatchers.Main))
}

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}


タグは、任意の型のオブジェクトを String 型のキーに紐づけて ViewModel に登録するための仕組みで、lifecycle-viewmodel の 2.1.0 で ViewModel に導入されます。タグが Closeable を実装していると ViewModel.onCleared() の直前にタグの close() が呼ばれるようなっており、viewModelScope は、それを利用して、ViewModel のクリア時に CoroutineContext をキャンセルしています。以下に、ViewModel の関連する実装を抜粋します。


lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModel.java

public abstract class ViewModel {

@Nullable
private final ConcurrentHashMap<String, Object> mBagOfTags = new ConcurrentHashMap<>();

// 中略...

final void clear() {
mCleared = true;
for (Object value: mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
onCleared();
}

// 中略...

private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}


タグを使えば、後処理が必要となるような ViewModel の共通処理をBaseクラスを作ったりせずに実装できるので、他の目的でも便利に使えそうです。


2019年1月3日追記

便利に使えそう、と思ったのですが、 タグを操作するメソッド getTag()setTagIfAbsent() のアクセス修飾子が publicprotected ではなく、default (package-private) でした... androidx.lifecycle パッケージにコードを書けば無理やりアクセスできるかもしれませんが、タグは、アプリから使われることを想定していない機能のようです 😢





  1. ViewModelCoroutineScope を用意する方法として、ViewModelCoroutineScope を実装する以外にも、GoogleのKotlin CoroutinesのCodelab にあるように CoroutineScope 型のインスタンスを生成してプロパティに保持しておくこともできます (こちらのほうが望ましいかもしれません)