Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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 型のインスタンスを生成してプロパティに保持しておくこともできます (こちらのほうが望ましいかもしれません) 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away