コルティンでのキャンセルと例外|常駐タスクの詳細な説明

このシリーズの2番目の記事「Coroutinesでのキャンセルと例外|キャンセル操作の詳細な説明」では、タスクが不要になったときに、正しく終了することが非常に重要であることを学びました。Androidでは、Jetpackが提供する2つのCoroutineScope(viewModelScopeとlifecycleScope)を使用できます。これらは、Activity、Fragment、およびLifecycleが完了したときに実行中のタスクを終了できます。独自のCoroutineScopeを作成する場合は、それをタスクにバインドし、必要に応じてキャンセルすることを忘れないでください。

  • viewModelScope

    https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope

  • lifecycleScope

    https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope

 

ただし、場合によっては、ユーザーが現在のインターフェイスを離れても、操作を完了させたいことがあります。したがって、データベースへのデータの書き込みやサーバーへの特定のタイプの要求の送信など、タスクがキャンセルされることは望ましくありません。

 

このような状況を実現するためのモードを紹介します。

CoroutineまたはWorkManager?

コルーチンは、アプリケーションのアクティビティ中に実行されます。アプリケーションプロセスの外部でアクティブにできる操作(リモートサーバーへのログの送信など)を実行する必要がある場合は、AndroidプラットフォームでWorkManagerを使用することをお勧めします。WorkManagerは、将来のある時点で実行されることが予想される重要な操作のための拡張ライブラリです。

  • WorkManager

    https://developer.android.google.cn/topic/libraries/architecture/workmanager

 

現在のプロセスで有効な操作にはコルーチンを使用し、ユーザーがアプリケーションを閉じたときに操作をキャンセルできることを確認してください(たとえば、キャッシュするネットワーク要求を作成します)。では、このタイプの操作を実装するためのベストプラクティスは何ですか?

コルティンのベストプラクティス

この記事で紹介するパターンは、他のコルーチンのベストプラクティスに基づいて実装されているため、この機会に次のことを確認できます。

1.スケジューラーをクラスに挿入します

coroutineを作成するとき、またはwithContextを呼び出すときに、スケジューラーをハードコーディングしないでください。

 

利点:テストの容易さ。ユニットテストや機器テストを行う際に簡単に交換できます。

 

2. Coroutineは、ViewModelまたはPresenterレイヤーで作成する必要があります

操作がUIのみに関連している場合は、UIレイヤーで実行できます。このベストプラクティスがプロジェクトで実行可能でないと思われる場合は、最初のベストプラクティスに従わなかった可能性があります(スケジューラなしでViewModelをテストするのはより困難になります。この場合、公開します。サスペンド機能により、テストが実行可能になります)。

 

利点:UIレイヤーはできるだけ簡潔にする必要があり、ビジネスロジックを直接トリガーしないでください。代わりに、応答性をViewModelまたはPresenterレイヤーの実装に転送する必要があります。Androidでは、UIレイヤーをテストするにはインストルメンテーションテストが必要であり、インストルメンテーションテストを実行するにはエミュレーターが必要です。

 

3. ViewModelまたはPresenterの下のレベルで、サスペンド機能とフローを公開する必要があります

coroutineを作成する必要がある場合は、coroutineScopeまたはsupervisorScopeを使用してください。また、コルーチンを他のスコープに制限したい場合は、読み続けてください。この記事では次にそれについて説明します。

 

利点:呼び出し元(通常はViewModelレイヤー)は、タスクとライフサイクルの実行時にこれらのレベルを制御でき、必要に応じてこれらのタスクをキャンセルすることもできます。

  • coroutineScope

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html

  • スーパーバイザースコープ

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html

キャンセルしてはならないコルティンでの操作

アプリケーションにViewModelとリポジトリがあるとすると、それらに関連するロジックは次のようになります。

class MyViewModel(private val repo: Repository) : ViewModel() {
  fun callRepo() {
    viewModelScope.launch {
      repo.doWork()
    }
  }
}


class Repository(private val ioDispatcher: CoroutineDispatcher) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
     veryImportantOperation() // 它不应当被取消
    }
  }
}

viewModelScopeはいつでもキャンセルされる可能性があるため、viewModelScopeを使用してveryImportantOperation()を制御することは望ましくありません。この操作をviewModelScopeよりも長く実行する必要があります。この目標をどのように達成できますか?

 

Applicationクラスで独自のスコープを作成し、それによって開始されたコルーチンでこれらの操作を呼び出す必要がありますこのスコープは、それを必要とするクラスに注入する必要があります。

 

GlobalScopeなど、この記事の後半で説明する他のソリューションと比較すると、独自のCoroutineScopeを作成する利点は、独自のアイデアに従って構成できることです。CoroutineExceptionHandlerが必要な場合でも、独自のスレッドプールをスケジューラーとして使用する場合でも、これらの一般的な構成は、CoroutineScopeのCoroutineContextに配置できます。

  • CoroutineExceptionHandler

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/

それをapplicationScopeと呼ぶことができます。applicationScopeには、Coroutineの障害がレベル間で伝播されないように、SupervisorJob()が含まれている必要があります(このシリーズの3番目の記事:Coroutineのキャンセルと例外|例外処理の詳細な説明を参照):

class MyApplication : Application() {
  // 不需要取消这个作用域,因为它会随着进程结束而结束
   val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}

アプリケーションプロセスの存続期間中はアクティブのままにしておきたいので、applicationScopeをキャンセルする必要はなく、SupervisorJob参照を保持する必要もありません。コルーチンに必要なライフタイムが呼び出し元のスコープのライフタイムよりも長い場合は、applicationScopeを使用してコルーチンを実行できます。

 

アプリケーションCoroutineScopeによって作成されたcoroutineからキャンセルしてはならない操作を呼び出します

 

新しいリポジトリインスタンスを作成するときはいつでも、上記で作成したapplicationScopeを渡しますテストについては、以下の「テスト」セクションを参照してください。

どのcoroutineコンストラクターを使用する必要がありますか?

veryImportantOperationの動作に基づいて新しいcoroutineを開始するには、launchまたはasyncを使用する必要があります。

  • 結果を返す必要がある場合は、asyncを使用し、awaitを呼び出して結果が完了するのを待ちます。

  • そうでない場合は、launchを使用してjoinを呼び出し、完了するのを待ちます。このシリーズの第3部で説明されいるように、起動ブロック内で例外を手動で処理する必要があることに注意してください

起動を使用してcoroutineを開始する方法は次のとおりです。

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      externalScope.launch {
        //如果这里会抛出异常,那么要将其包裹进 try/catch 中;
        //或者依赖 externalScope 的 CoroutineScope 中的 CoroutineExceptionHandler 
        veryImportantOperation()
      }.join()
    }
  }
}

または非同期を使用します。

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork(): Any { // 在结果中使用特定类型
    withContext(ioDispatcher) {
      doSomeOtherWork()
      return externalScope.async {
        // 异常会在调用 await 时暴露,它们会在调用了 doWork 的协程中传播。
        // 注意,如果正在调用的上下文被取消,那么异常将会被忽略。
        veryImportantOperation()
    }.await()
    }
  }
}

いずれの場合も、上記のViewModelのコードを変更する必要はありません。ViewModelScopeが破棄されても、externalScopeを使用するタスクは引き続き実行されます。他のサスペンド関数と同様に、doWork()はveryImportantOperation()が完了した後にのみ返されます。

 

もっと簡単な解決策はありますか?

一部のユースケースで使用できる別のソリューション(おそらく誰もが最初に考えるソリューション)は、次のようにwithContextを使用してveryImportantOperationをexternalScopeのコンテキストにカプセル化することです。

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      withContext(externalScope.coroutineContext) {
        veryImportantOperation()
      }
    }
  }
}

ただし、この方法には次の注意事項があり、使用する際には注意が必要です。

  • veryImportantOperationの実行開始時にdoWork()を呼び出すコルーチンが終了した場合、veryImportantOperationの終了後に終了するのではなく、次の終了ノードまで実行を継続します。

  • withContextでコンテキストを使用すると例外が再スローされるため、CoroutineExceptionHandlerは期待どおりに機能しません。

テスト

スケジューラとCoroutineScopを同時に注入する必要があるかもしれないので、これらのシナリオでは何を注入する必要がありますか?

テスト時に何を注入するか

???? ドキュメンテーション: 

  • TestCoroutineDispatcher

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html

  • MainCoroutineRule

    https://github.com/android/plaid/blob/master/test_shared/src/main/java/io/plaidapp/test/shared/MainCoroutineRule.kt

  • TestCoroutineScope

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/

  • AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()

    https://github.com/google/iosched/blob/adssched/mobile/src/androidTest/java/com/google/samples/apps/iosched/tests/di/TestCoroutinesModule.kt#L36

代替案

実際、この動作を実現するためにコロチンを使用できる方法は他にもいくつかあります。ただし、これらのソリューションは、すべての条件下で整然と実装できるわけではありません。いくつかの代替案、それらが適用可能かどうか、およびそれらをいつ使用するかを見てみましょう。

 

GlobalScope

GlobalScopeを使用すべきでない理由は次のとおりです。

  • ハードコードされた値を書くように私たちを誘導しますGlobalScopeを直接使用すると、ハードコードされたスケジューラーを作成する傾向がありますが、これは不適切な方法です。

  • テストを非常に難しくします。コードは制御されていないスコープで実行されるため、コードから開始されたタスクを管理することはできません。

  • applicationScopeで行ったように、すべてのcoroutineに共通のCoroutineContext組み込みスコープを提供することはできません代わりに、GlobalScopeによって開始されたすべてのcoroutineに共通のCoroutineContextを渡す必要があります。

推奨事項:直接使用しないでください。

  • GlobalScope

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/

❌AndroidのProcessLifecycleOwnerスコープ

Androidのandroidx.lifecycle:lifecycle-processライブラリにはapplicationScopeがあり、ProcessLifecycleOwner.get()。lifecycleScopeを使用して呼び出すことができます。

 

これを使用する場合は、LifecycleOwnerを挿入して、前に挿入したCoroutineScopeを置き換える必要があります。実稼働環境では、ProcessLifecycleOwner.get()を渡す必要があります。ユニットテストでは、LifecycleRegistryを使用して仮想LifecycleOwnerを作成できます。

 

このスコープのデフォルトのCoroutineContextはDispatchers.Main.immediateであるため、バックグラウンドタスクの実行には適さない場合があることに注意してください。GlobalScopeを使用する場合と同様に、GlobalScopeを介して開始されるすべてのcoroutineに共通のCoroutineContextを渡す必要もあります。

 

上記の理由により、この代替手段は、Applicationクラスで直接CoroutineScopeを作成するよりも面倒です。さらに、私は個人的に、ViewModelまたはPresenterレイヤーの下でAndroidライフサイクルとの関係を確立するのは好きではありません。これらのレイヤーがプラットフォームに依存しないことを願っています。

 

推奨事項:直接使用しないでください。

⚠️特別な指示

applicationScopeのCoroutineContextをGlobalScopeまたはProcessLifecycleOwner.get()。lifecycleScopeに設定すると、次のように直接使用できます。

class MyApplication : Application() {
  val applicationScope = GlobalScope
}

上記のすべての利点を引き続き利用でき、将来必要に応じて簡単に変更を加えることができます。

❌✅キャンセル不可を使用

このシリーズの2番目の記事「Coroutinesのキャンセルと例外|キャンセル操作の詳細な説明」で見たように、withContext(NonCancellable)を使用して、キャンセルされたCoroutineのサスペンド関数を呼び出すことができます。ハング可能なコードのクリーンアップに使用することをお勧めしますが、悪用しないでください。

 

コルティンの実行を制御することができないため、そうするリスクは高くなります。確かに、それはコードをより簡潔で読みやすくすることができますが、同時に、将来的にいくつかの予測できない問題を引き起こす可能性もあります。

 

使用例は次のとおりです。

class Repository(
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
    withContext(NonCancellable){
        veryImportantOperation()
      }
    }
  }
}

このスキームは魅力的ですが、someImportantOperation()の背後にあるロジックを常に知っているとは限りません。拡張ライブラリの場合もあれば、インターフェイスの背後にある実装の場合もあります。さまざまな問題を引き起こす可能性があります。

  • テストでこれらの操作を終了することはできません。

  • 遅延を使用した無限ループがキャンセルされることはありません。

  • そこからフローを収集すると、フローを外部からキャンセルできなくなります。

  • …..。 

これらの問題は、エラーのデバッグが微妙で非常に困難になる可能性があります。

 

提案:クリーンアップ操作に関連するコードを一時停止する場合にのみ使用してください。

 

現在のスコープのスコープを超える作業を実行する必要がある場合は常に、独自のApplicationクラスでカスタムスコープを作成し、このスコープでコルーチンを実行することをお勧めします。同時に、このようなタスクを実行するときは、GlobalScope、ProcessLifecycleOwnerスコープ、またはNonCancellableを使用しないように注意してください。


推奨読書




 最後に画面をクリックします元の記事を読みます | Androidの公式中国文書を表示します-Kotlinを使用してより良いアプリケーションをより速く記述しますAndroid  


おすすめ

転載: blog.csdn.net/jILRvRTrc/article/details/109108006