Android での Kotlin コルーチン

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

コルーチンとは、Android で使用できる並行実行のデザイン パターンです。これを使用すると、非同期実行するコードを簡略化できます。コルーチン は、バージョン 1.3 で Kotlin に追加され、確立された コンセプトを学習できます。

Android では、メインスレッドをブロックしてアプリの応答を止める可能性のある長時間実行タスクの管理に役立ちます。コルーチンを使用するプロのデベロッパーの 50% 以上が、生産性が向上したと報告しました。このトピックでは、Kotlin コルーチンを使用してこれらの問題に対処する方法を説明し、より簡潔で無駄のないアプリコードを記述できるようにします。

機能

コルーチンは、Android での非同期プログラミングに推奨するソリューションです。主な機能は次のとおりです。

  • 軽量 : 中断 がサポートされているため、1 つのスレッドで多数のコルーチンを実行できます。これにより、コルーチンを実行しているスレッドがブロックされません。中断により、多数の同時実行処理をサポートしつつ、ブロックさせる場合よりメモリを節約できます。
  • メモリリークが少ない : 構造化された同時実行 スコープ内でオペレーションを実行できます
  • 組み込みの解約サポート : 解約 実行中のコルーチン階層を通じて自動的に伝播されます。
  • Jetpack の統合 : Jetpack ライブラリの多くには、コルーチンを全面的にサポートする拡張機能 が用意されています。一部のライブラリでは、構造化された同時実行に使用できる独自のコルーチン スコープ も用意されています。

例の概要

このトピックのサンプルは、アプリ アーキテクチャ ガイド をベースにしており、ネットワーク リクエストを行って、結果をメインスレッドに返し、それをアプリでユーザーに表示するものになっています。

具体的には、 ViewModel アーキテクチャ コンポーネントがメインスレッドのリポジトリ レイヤを呼び出してネットワーク リクエストをトリガーします。このガイドでは、さまざまなソリューションについて説明します。 コルーチンを使用してメインスレッドのブロックを解除します。

ViewModel には、コルーチンを直接扱う KTX 拡張機能である lifecycle-viewmodel-ktx ライブラリ が用意されており、このガイドでも使用されています。

依存関係情報

Android プロジェクトでコルーチンを使用するには、アプリの build.gradle ファイルに次の依存関係を追加します。

Groovy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

バックグラウンド スレッドでの実行

メインスレッドでネットワーク リクエストを行うと、レスポンスを受信するまでの間、待機したり、ブロック されたりすることになります。スレッドがブロックされるため、OS は onDraw() を呼び出せず、アプリがフリーズして、アプリケーション応答なし(ANR)のダイアログが表示される可能性があります。ユーザー エクスペリエンスを向上させるために、この処理をバックグラウンド スレッドで実行するようにしましょう。

まず、Repository クラスを見て、ネットワーク リクエストがどのように行われているかを確認します。

        sealed
         
        class
         
        Result<out
         
        R>
         
        {

           
         data
         
        class
         
        Success<out
         
        T>
        (
        val
         
        data
        :
         
        T
        )
         
        :
         
        Result<T>
        ()

           
         data
         
        class
         
        Error
        (
        val
         
        exception
        :
         
        Exception
        )
         
        :
         
        Result<Nothing>
        ()

        }


        class
         
        LoginRepository
        (
        private
         
        val
         
        responseParser
        :
         
        LoginResponseParser
        )
         
        {

           
         private
         
        const
         
        val
         
        loginUrl
         
        =
         
    "    https
        :
        //example.com/login
    "

           
         // Function that makes the network request, blocking the current thread

           
         fun
         
        makeLoginRequest
        (

               
         jsonBody
        :
         
        String

           
         ):
         
        Result<LoginResponse>
         
        {

               
         val
         
        url
         
        =
         
        URL
        (
        loginUrl
        )

               
         (
        url
        .
        openConnection
        ()
         
        as?
         
        HttpURLConnection
        )
        ?.
        run
         
        {

                   
         requestMethod
         
        =
         
    "    POST
    "
                   
         setRequestProperty
        (
    "    Content
        -
        Type
    "    ,
         
    "    application
        /
        json
        ;
         
        utf
        -
        8
    "    )

                   
         setRequestProperty
        (
    "    Accept
    "    ,
         
    "    application
        /
        json
    "    )

                   
         doOutput
         
        =
         
        true

                   
         outputStream
        .
        write
        (
        jsonBody
        .
        toByteArray
        ())

                   
         return
         
        Result
        .
        Success
        (
        responseParser
        .
        parse
        (
        inputStream
        ))

               
         }

               
         return
         
        Result
        .
        Error
        (
        Exception
        (
    "    Cannot
         
        open
         
        HttpURLConnection
    "    ))

           
         }

        }

makeLoginRequest は同期しており、呼び出し元のスレッドをブロックします。ネットワーク リクエストのレスポンスをモデル化するために、独自の Result クラスが用意されています。

ViewModel は、ボタンなどがクリックされたときにネットワーク リクエストをトリガーします。

        class
         
        LoginViewModel
        (

           
         private
         
        val
         
        loginRepository
        :
         
        LoginRepository

        ):
         
        ViewModel
        ()
         
        {


           
         fun
         
        login
        (
        username
        :
         
        String
        ,
         
        token
        :
         
        String
        )
         
        {

               
         val
         
        jsonBody
         
        =
         
    "    {
         
        username
        :
         
        \"$
        username
        \
    "    ,
         
        token
        :
         
        \"$
        token
        \
    "    }
    "
               
         loginRepository
        .
        makeLoginRequest
        (
        jsonBody
        )

           
         }

        }

前のコードでは、ネットワーク リクエストを行うときに、LoginViewModel が UI スレッドをブロックしています。この実行をメインスレッドから移動させるには、新しいコルーチンを作成して、I/O スレッドでネットワーク リクエストを実行するのが最も簡単です。

        class
         
        LoginViewModel
        (

           
         private
         
        val
         
        loginRepository
        :
         
        LoginRepository

        ):
         
        ViewModel
        ()
         
        {


           
         fun
         
        login
        (
        username
        :
         
        String
        ,
         
        token
        :
         
        String
        )
         
        {

               
         // Create a new coroutine to move the execution off the UI thread

               
         viewModelScope
        .
        launch
        (
        Dispatchers
        .
        IO
        )
         
        {

                   
         val
         
        jsonBody
         
        =
         
    "    {
         
        username
        :
         
        \"$
        username
        \
    "    ,
         
        token
        :
         
        \"$
        token
        \
    "    }
    "
                   
         loginRepository
        .
        makeLoginRequest
        (
        jsonBody
        )

               
         }

           
         }

        }

login 関数のコルーチン コードを詳しく見てみましょう。

  • viewModelScope は定義済みの CoroutineScope で、ViewModel KTX 拡張機能に含まれています。すべてのコルーチンはスコープ内で実行する必要があります。CoroutineScope により、1 つ以上の関連するコルーチンを管理します。
  • launch は、コルーチンを作成し、この関数自体の実行内容を対応するディスパッチャーにディスパッチする関数です。
  • Dispatchers.IO は、このコルーチンが I/O 処理用に予約されたスレッドで実行されることを示します。

login 関数は次のように実行されます。

  • アプリが、メインスレッドで View レイヤから login 関数を呼び出します。
  • launch が新しいコルーチンを作成し、I/O 処理用に予約されたスレッドで独自にネットワーク リクエストが行われます。
  • このコルーチンの実行中も、login 関数は実行を継続し、場合によってはネットワーク リクエストが完了する前に戻ります。簡単にするため、ここではネットワーク レスポンスを無視します。

このコルーチンは viewModelScope で開始されるため、ViewModel のスコープで実行されます。ユーザーが画面から移動して ViewModel が破棄された場合、viewModelScope は自動的にキャンセルされ、実行中のすべてのコルーチンもキャンセルされます。

前のサンプルには、makeLoginRequest を呼び出すときに、実行を明示的にメインスレッドから移動させる必要があるという問題があります。Repository を変更して、この問題を解決する方法を見てみましょう。

コルーチンでメインセーフティにする

ある関数がメインスレッドでの UI の更新をブロックしないとき、その関数はメインセーフティ であると言います。メインスレッドから makeLoginRequest 関数を呼び出すと UI がブロックされるため、makeLoginRequest はメインセーフティではありません。コルーチンの実行を別のスレッドに移動するために、次のようにコルーチン ライブラリの withContext() 関数を使用します。

        class
         
        LoginRepository
        (...)
         
        {

           
         ...

           
         suspend
         
        fun
         
        makeLoginRequest
        (

               
         jsonBody
        :
         
        String

           
         ):
         
        Result<LoginResponse>
         
        {


               
         // Move the execution of the coroutine to the I/O dispatcher

               
         return
         
        withContext
        (
        Dispatchers
        .
        IO
        )
         
        {

                   
         // Blocking network request code

               
         }

           
         }

        }

withContext(Dispatchers.IO) は、コルーチンの実行を I/O スレッドに移動し、呼び出し元の関数をメインセーフティにして、必要なときに UI が更新されるようにします。

makeLoginRequest には suspend キーワードも付いています。このキーワードは、Kotlin 独自のもので、関数がコルーチン内から呼び出されるようにするものです。

次のサンプルでは、LoginViewModel 内でコルーチンが作成されます。makeLoginRequest の実行がメインスレッドの外に移り、login 関数のコルーチンがメインスレッドで実行されるようになります。

        class
         
        LoginViewModel
        (

           
         private
         
        val
         
        loginRepository
        :
         
        LoginRepository

        ):
         
        ViewModel
        ()
         
        {


           
         fun
         
        login
        (
        username
        :
         
        String
        ,
         
        token
        :
         
        String
        )
         
        {


               
         // Create a new coroutine on the UI thread

               
         viewModelScope
        .
        launch
         
        {

                   
         val
         
        jsonBody
         
        =
         
    "    {
         
        username
        :
         
        \"$
        username
        \
    "    ,
         
        token
        :
         
        \"$
        token
        \
    "    }
    "

                   
         // Make the network call and suspend execution until it finishes

                   
         val
         
        result
         
        =
         
        loginRepository
        .
        makeLoginRequest
        (
        jsonBody
        )


                   
         // Display result of the network request to the user

                   
         when
         
        (
        result
        )
         
        {

                       
         is
         
        Result
        .
        Success<LoginResponse>
         
        -
    >     
        // Happy path

                       
         else
         
        -
    >     
        // Show error in UI

                   
         }

               
         }

           
         }

        }

makeLoginRequestsuspend 関数であり、すべての suspend 関数はコルーチンで実行する必要があるため、ここでは引き続きコルーチンが必要です。

このコードには、前の login のサンプルとは異なる点があります。

  • launchDispatchers.IO パラメータがない。launchDispatcher を渡さない場合、viewModelScope から起動されたコルーチンがすべてメインスレッドで実行されます。
  • ネットワーク リクエストの結果が処理され、成功または失敗の UI が表示される。

login 関数は次のように実行されます。

  • アプリが、メインスレッドで View レイヤから login() 関数を呼び出します。
  • launch がメインスレッドで新しいコルーチンを作成し、そのコルーチンの実行が開始されます。
  • コルーチン内では、loginRepository.makeLoginRequest() を呼び出すと、makeLoginRequest() 内の withContext ブロックの実行が終了するまでコルーチンの実行を中断 するようになりました。
  • withContext ブロックが終了すると、login() のコルーチンはメインスレッド で実行を再開し、ネットワーク リクエストの結果を返します。

例外の処理

Repository レイヤがスローする例外を処理するには、Kotlin の例外に対する組み込みサポート を使用します。次のサンプルでは、try-catch ブロックを使用しています。

        class
         
        LoginViewModel
        (

           
         private
         
        val
         
        loginRepository
        :
         
        LoginRepository

        ):
         
        ViewModel
        ()
         
        {


           
         fun
         
        login
        (
        username
        :
         
        String
        ,
         
        token
        :
         
        String
        )
         
        {

               
         viewModelScope
        .
        launch
         
        {

                   
         val
         
        jsonBody
         
        =
         
    "    {
         
        username
        :
         
        \"$
        username
        \
    "    ,
         
        token
        :
         
        \"$
        token
        \
    "    }
    "
                   
         val
         
        result
         
        =
         
        try
         
        {

                       
         loginRepository
        .
        makeLoginRequest
        (
        jsonBody
        )

                   
         }
         
        catch
        (
        e
        :
         
        Exception
        )
         
        {

                       
         Result
        .
        Error
        (
        Exception
        (
    "    Network
         
        request
         
        failed
    "    ))

                   
         }

                   
         when
         
        (
        result
        )
         
        {

                       
         is
         
        Result
        .
        Success<LoginResponse>
         
        -
    >     
        // Happy path

                       
         else
         
        -
    >     
        // Show error in UI

                   
         }

               
         }

           
         }

        }

このサンプルでは、makeLoginRequest() の呼び出しでスローされた予期しない例外は、UI でエラーとして処理されます。

コルーチンに関する参考情報

Android でのコルーチンの詳細については、Kotlin コルーチンでアプリのパフォーマンスを改善する をご覧ください。

コルーチンに関するその他の参考情報については、次のリンクをご覧ください。

このページのコンテンツやコードサンプルは、コンテンツ ライセンス に記載のライセンスに従います。Java および OpenJDK は Oracle および関連会社の商標または登録商標です。

最終更新日 2024-08-29 UTC。