Tatsuro のテックブログ

アプリ開発の技術情報を発信します。

【Spring Boot × Kotlin】REST API を作成する

今回は、Spring Boot と Kotlin で REST API を作成する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • IntelliJ IDEA 2022.2.2 (Community Edition)
  • JDK 17.0.4.1
  • Kotlin 1.6.21
  • Gradle 7.5
  • Spring Boot 2.7.3

プロジェクト作成

プロジェクトを Spring Initializr にて作成します。各設定項目は以下のように設定します。

  • Project: Gradle Project
  • Language: Kotlin
  • Spring Boot: 2.7.3
  • Project Metadata: デフォルト値
  • Dependencies: Spring Web

今回、REST API のルーティングを行うため、Dependencies に Spring Web を設定しています。

以上で、プロジェクトの作成は完了です。

REST API を作成する

プロジェクトの作成が完了したら、実際に REST API の作成に入ります。

今回、以下のような JSON を応答する REST API を作成します。

{
  "datetime": "yyyy-mm-ddThh:MM:ss.SSSSSS",
  "message": "Hello, {name}!"
}

datetime はバックエンドがリクエストを受けたときの日時とします。また、message の文字列の一部をクライアントからリクエストできるようにします。

今回は、以下の 3 つのデータの渡し方について、それぞれ実装します。

  • URL クエリパラメータ
  • パスパラメータ
  • JSON リクエストボディ

URL クエリパラメータ

最初に、URL クエリパラメータにてデータを渡す方法です。具体的には、以下のような REST API を実装します。

GET http://localhost:8080/get-greeting/query?name={name} HTTP/1.1

まず、REST API のルーティングをコントロールするクラスを以下のように実装します。

// GreetingController.kt
@RestController
class GreetingController {

    @GetMapping("/get-greeting/query")
    fun getGreetingByQuery(
        @RequestParam(
            name = "name", required = false, defaultValue = "world"
        ) name: String
    ) = GreetingResponse(
        datetime = LocalDateTime.now().toString(),
        message = "Hello, $name!"
    )
}

REST API のコントローラクラスには @RestController を付与します。このコントローラクラスには、HTTP リクエストごとにメソッドを追加します。

GET リクエストに対応するメソッドには、@GetMapping を付与し、そのリクエストのパスを @GetMapping の引数に設定します。

メソッドの引数には URL クエリパラメータを受け取る name を追加します。省略された場合のデフォルト値は "world" とします。

そして、メソッドの戻り値には JSON レスポンスに対応するデータクラスのインスタンスを設定します。このレスポンスのデータクラスは以下のように実装します。

// GreetingResponse.kt
data class GreetingResponse(
    val datetime: String,
    val message: String,
)

プロジェクトを実行すると、以下のようなコマンドを実行できます。

$ curl 'http://localhost:8080/get-greeting/query?name=tatsuro'
{"datetime":"2022-09-24T13:14:41.071979","message":"Hello, tatsuro!"}

期待通りの JSON を受け取れています。

パスパラメータ

次に、パスパラメータにてデータを渡す方法です。具体的には、以下のような REST API を実装します。

GET http://localhost:8080/get-greeting/path/{name} HTTP/1.1

REST API のコントローラクラスを以下のように実装します。

// GreetingController.kt
@RestController
class GreetingController {

    @GetMapping("/get-greeting/path/{name}")
    fun getGreetingByPath(
        @PathVariable("name") name: String
    ) = GreetingResponse(
        datetime = LocalDateTime.now().toString(),
        message = "Hello, $name!"
    )
}

実装は URL クエリパラメータとほぼ同じですが、@GetMapping に設定するリクエストのパスと、引数のアノテーションが異なります。

@GetMapping に設定するパスについては、パラメータになる箇所を波括弧で囲い、波括弧の中にパラメータ名を設定します。

メソッドの引数には、パスパラメータを受け取る name@PathVariable("name") を付与します。

なお、メソッドの戻り値に使用するデータクラスは、さきほどの GreetingResponse を流用します。

プロジェクトを実行すると、以下のようなコマンドを実行できます。

$ curl 'http://localhost:8080/get-greeting/path/tatsuro'
{"datetime":"2022-09-24T13:14:54.326179","message":"Hello, tatsuro!"}

期待通りの JSON を受け取れています。

JSON リクエストボディ

最後に、JSON のリクエストボディにてデータを渡す方法です。具体的には、以下のような REST API を実装します。

POST http://localhost:8080/post-name HTTP/1.1

{"name":"{name}"}

まず、JSON リクエストボディに対応するデータクラスを以下のように実装します。

// NameRequest.kt
data class NameRequest(val name: String)

次に、REST API のコントローラクラスを以下のように実装します。

// GreetingController.kt
@RestController
class GreetingController {

    @PostMapping("/post-name")
    fun postName(
        @RequestBody request: NameRequest
    ) = GreetingResponse(
        datetime = LocalDateTime.now().toString(),
        message = "Hello, ${request.name}!"
    )
}

今回は POST リクエストで実装するので、メソッドには @PostMapping を付与します。

JSON リクエストボディを受け取るためには、@RequestBody を付与した引数を追加します。この引数の型には、JSON リクエストボディに対応するデータクラス NameRequest を使います。

なお、メソッドの戻り値に使用するデータクラスは、いままで使った GreetingResponse を流用します。

プロジェクトを実行すると、以下のようなコマンドを実行できます。

$ curl -H 'Content-Type:application/json' \
    -X POST \
    -d '{"name":"tatsuro"}' \
    'http://localhost:8080/post-name'
{"datetime":"2022-09-24T13:15:12.659395","message":"Hello, tatsuro!"}

期待通りの JSON を受け取れています。

参考

REST API の作成

【Spring Boot × Kotlin】Thymeleaf で Web ページを作成する

今回は、Thymeleaf で Web ページを作成する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • IntelliJ IDEA 2022.2.2 (Community Edition)
  • JDK 17.0.4.1
  • Kotlin 1.6.21
  • Gradle 7.5
  • Spring Boot 2.7.3

プロジェクト作成

プロジェクトを Spring Initializr にて作成します。各設定項目は以下のように設定します。

  • Project: Gradle Project
  • Language: Kotlin
  • Spring Boot: 2.7.3
  • Project Metadata: デフォルト値
  • Dependencies: Spring Web, Thymeleaf

Web ページを作成するため、Dependencies に Spring Web と Thymeleaf を設定しています。

まず、Spring Web は バックエンドのルーティングなどの機能を提供するライブラリです。そして、Thymeleaf はバックエンドで Web ページをレンダリングするテンプレートエンジンです。

以上で、プロジェクトの作成は完了です。

Web ページを作成する

プロジェクトの作成が完了したら、実際に Web ページの作成に入ります。

まず、バックエンドのルーティングを実装します。プロジェクトの src/main/kotlin ディレクトリの com.example.demo パッケージ配下に GreetingController.kt を追加し、以下のように実装します。

@Controller
class GreetingController {

    @GetMapping("/greeting")
    fun greeting(
        @RequestParam(
            name = "name", required = false, defaultValue = "world"
        ) name: String,
        model: Model
    ): String {
        model.addAttribute("name", name)
        return "index"
    }
}

ルーティングをコントロールするため、追加したクラスに @Controller を付与します。このコントローラクラスには、HTTP リクエストごとにメソッドを追加します。

今回、/greeting の HTTP GET リクエストをコントロールするために、@GetMapping("/greeting") を付与したメソッドを追加します。

このメソッドの引数には、URL クエリパラメータを受け取る name と テンプレートの HTML に値を渡すための model を追加します。URL クエリパラメータは、その名前を "name" とし、省略された場合のデフォルト値は "world" とします。

/greeting の HTTP GET リクエストを受け取ったときには、name の設定値を HTML に渡します(model.addAttribute("name", name))。

最後に、/greeting で表示する HTML のファイル名 "index" を返します。

次に HTML ファイルを実装します。プロジェクトの src/main/resources/templates 配下に、index.html を追加し、以下のように実装します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Index</title>
  </head>
  <body>
    <p data-th-text="'Hello, ' + ${name} + '!'"></p>
  </body>
</html>

HTML では p タグの data-th-text 属性を評価して、その結果を表示します。${name} にはバックエンドのコントローラで設定された値が付与されます。

プロジェクトを実行して、http://localhost:8080/greeting にアクセスすると、ブラウザに Hello, world! と表示されます。また、例えば http://localhost:8080/greeting?name=tatsuro と URL クエリパラメータを渡すと、ブラウザに Hello, tatsuro! と表示されます。

参考

Thymeleaf Web 画面の作成

【Kotlin】launch ビルダーで Coroutine を実行する

今回は、Kotlin Coroutines の launch ビルダーについて解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • JDK 11.0.12
  • Android Gradle Plugin 7.2.1
  • Kotlin 1.7.0
  • Gradle 7.4.2
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.6.3

そして、アプリの build.gradle ファイルに kotlinx-coroutines-core をインポートしています。

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3"
}

launch ビルダーとは?

launch ビルダーとは、現在のスレッドをブロックせずに新しい Coroutine を起動し、その起動した Coroutine をハンドリングする Job を返す Coroutine ビルダーです。

例えば、以下のように使います。

fun main() = runBlocking {
    // (1) ノンブロッキングで Coroutine を起動する。
    val job = launch {
        delay(100L)
        // (4) "World!" を標準出力する。
        println("World!")
    }
    // (2) "Hello," を標準出力する。
    println("Hello,")
    // (3) launch ビルダーで起動された Coroutine が完了するまで待つ。
    job.join()
    // (5) "End." を標準出力する。
    println("End.")
}

上記のコードを実行すると、以下の結果を得られます。

Hello,
World!
End.

さきにも述べた通り、launch ビルダーは現在のスレッドをブロックせずに新しい Coroutine を起動します。よって、"World!" を標準出力する Coroutine と "Hello," を標準出力する Coroutine は並列して実行されます。

また、launch ビルダーが返す Job を使って、launch ビルダーにより起動された Coroutine をその Coroutine の外からハンドリングできます。上記のコードでは、Job の join を使って、Coroutine の完了をノンブロッキングで待機しています。

【Kotlin】Kotlin Coroutines で非同期処理を行う

今回は、Kotlin Coroutines について解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • JDK 11.0.12
  • Android Gradle Plugin 7.2.1
  • Kotlin 1.7.0
  • Gradle 7.4.2
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.6.3

そして、アプリの build.gradle ファイルに kotlinx-coroutines-core をインポートしています。

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3"
}

基本

Coroutines とは Kotlin にて非同期処理を実現する機能です。

例えば、Coroutines で非同期処理を実現する場合、以下のように実装します。

fun main() {
    // 新しい CoroutineScope を実行する。
    runBlocking {
        // 新しい CoroutineScope を実行しつつ、
        // 現在のスレッドをブロックしない。
        launch {
            // ノンブロッキングで処理を遅延させる。
            delay(100L)
            println("World!")
        }
        println("Hello,")
    }
}

上記のコードを実行すると、以下の結果を得られます。

Hello,
World!

実装を上から見ると、"World!" を先に標準出力して、後に "Hello," を標準出力するように見えます。

しかし、launch の後に続くブロック内の処理と println("Hello,") は並列に実行されており、さらに launch の後に続くブロックでは、最初に delay(100L) で少し処理を中断しているため、実際には "Hello," から "World!" の順番に標準出力されます。

詳細

ここでは、Coroutines によく登場する以下の 4 つについて解説します。

  • CoroutineScope
  • CoroutineContext
  • Coroutine ビルダー
  • suspend 関数

CoroutineScope

CoroutineScope とは Coroutine の有効範囲です。Coroutines で処理を実行したい場合、この CoroutineScope を開始する必要があります。

また、以下のように CoroutineScope は内部に CoroutineContext を保持しており、CoroutineContext の設定通りに Coroutine を実行します。

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineContext

CoroutineContext とは Coroutine の設定のようなものです。CoroutineContext を保持する CoroutineScope は、CoroutineContext の設定通りに Coroutine を実行します。

以下に、代表的な CoroutineContext の要素を記載します。

要素 説明
Job Coroutine のハンドラ。Coroutine のキャンセルができる。
CoroutineDispatcher Coroutine が動作するスレッドを指定する。
CoroutineName Coroutine の名前。デバッグでこの名前が表示される。
CoroutineExceptionHandler Exception のハンドラ。Coroutine でキャッチできなかった Exception を処理できる。

Coroutine ビルダー

Coroutine ビルダーとは CoroutineScope を開始する関数です。さきほどのサンプルコードでは、runBlockinglaunch が Coroutine ビルダーです。Coroutine ビルダーに Coroutines で実行したい処理を渡すと、その処理は CoroutineScope 内部で実行されます。

以下に、代表的な Coroutine ビルダーを記載します。

Coroutine ビルダー 特徴
runBlocking 現在のスレッドをブロックする。
CoroutineScope の外からでも呼び出せる。
ブロック内部の処理の結果を返せる。
launch 現在のスレッドをブロックしない。
CoroutineScope の内部からのみ呼び出せる。
ブロック内部の処理の結果を返せない。
async 現在のスレッドをブロックしない。
CoroutineScope の内部からのみ呼び出せる。
ブロック内部の処理の結果を返せる。

上記の 3 つの内、launch と async については、CoroutineScope の拡張関数として定義されているため、CoroutineScope の内部からしか呼び出すことができません。runBlocking については、CoroutineScope の拡張関数ではないため、CoroutineScope の外からでも呼び出すことができます。

suspend 関数

suspend 関数とは、スレッドをブロックせずに処理を一時停止し、そして再開できる関数のことです。さきほどのサンプルコードでは、delay が suspend 関数です。delay は処理を一時停止する関数ですが、現在のスレッドをブロックせずにスレッドプールに一時返却するため、delay で一時停止している間、別の処理がそのスレッドを使用できます。

また、suspend 関数は suspend 関数からしか呼ぶことができません。よって、例えば delay を呼び出すためには、その呼び出し元の関数に suspend を付与する必要があります。

// NG
fun doWork() {
    delay(100L)
    println("Do work.")
}
// OK
suspend fun doWork() {
    delay(100L)
    println("Do work.")
}

参考

コルーチンの基礎

関連記事

launch ビルダーで Coroutine を実行する方法について、以下で解説しています。 tatsurotech.hatenablog.com

【Android × Kotlin】ViewModelProvider.Factory を使って、コンストラクタ経由で ViewModel に値を渡す

今回は、ViewModelProvider.Factory を使って、コンストラクタ経由で ViewModel に値を渡す方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • JDK 11.0.12
  • Android Gradle Plugin 7.2.1
  • Kotlin 1.7.0
  • Gradle 7.4.2
  • androidx.lifecycle 2.4.1
  • androidx.activity 1.4.0

概要

通常、ViewModel のコンストラクタに独自の引数、すなわち Application や SavedStateHandle 以外の引数を追加することはできません。

なぜなら、ViewModel の生成はその利用者が直接コンストラクタを呼ぶのではなくて、ViewModel を管理する ViewModelProvider にて ViewModel の Factory を使って行われるからです。

よって、ViewModel のコンストラクタに引数を追加して、その引数ありのコンストラクタで ViewModel を生成するためには、カスタムの Factory を作成し、そのインスタンスを ViewModelProvider に設定する必要があります。

今回は、実際に ViewModelProvider.Factory を継承して ViewModel のカスタム Factory を作成しながら、ViewModelProvider.Factory を使って、コンストラクタ経由で ViewModel に値を渡す方法を解説します。

ライブラリのインポート

ViewModel を使用するために、アプリの build.gradle ファイルの dependencieslifecycle-viewmodel-ktx を追加します。また、Activity で viewModels を使って ViewModel インスタンスを取得するため、activity-ktx も追加します。

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
    implementation "androidx.activity:activity-ktx:1.4.0"
}

ViewModelProvider.Factory を使って、ViewModel に値を渡す

今回は、以下のようなサービスクラスをコンストラクタ経由で ViewModel に渡すコードを作成します。

interface SampleService {
    fun getText(): String
}

class SampleServiceImpl : SampleService {
    override fun getText(): String = "Sample"
}

まず、ViewModel から作成します。

class SimpleViewModel(
    private val service: SampleService
) : ViewModel() {

    /** テキストを取得する。 */
    fun getText() = service.getText()

    class Factory(
        private val service: SampleService
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("unchecked_cast")
        override fun <T : ViewModel> create(modelClass: Class<T>) =
            SimpleViewModel(service) as T
    }
}

ViewModel ではコンストラクタ経由でさきほどの SampleService を受け取りたいため、コンストラクタ引数に SampleService を追加します。

class SimpleViewModel(
    private val service: SampleService
) : ViewModel() {
    ︙
}

次に、ViewModel の Factory を作成します。

ViewModel の Factory では、ViewModelProvider.NewInstanceFactory を継承して、コンストラクタ引数に SampleService を追加します。この ViewModelProvider.NewInstanceFactory は ViewModelProvider.Factory を継承元に持ち、引数なしのコンストラクタで ViewModel を生成する Factory です。

そして、create メソッドをオーバーライドします。このメソッドは ViewModel を生成するメソッドなので、SimpleViewModel を生成して、それを返します。

    class Factory(
        private val service: SampleService
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("unchecked_cast")
        override fun <T : ViewModel> create(modelClass: Class<T>) =
            SimpleViewModel(service) as T
    }

最後に、ViewModel の利用側を作成します。

今回は、Activity で viewModels を使って ViewModel を取得します。このとき、viewModels に ViewModel の Factory のインスタンスを返すラムダを渡します。こうすることで、ViewModelProvider は渡された Factory を使って、ViewModel を生成します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    private val viewModel: SimpleViewModel by viewModels {
        SimpleViewModel.Factory(SampleServiceImpl())
    }
    ︙
}

以上で、コンストラクタ経由で ViewModel に値を渡せるようになります。

関連記事

ViewModel の使用方法については、以下で解説しています。 tatsurotech.hatenablog.com

【Android × Kotlin】FragmentFactory を使って、コンストラクタ経由で Fragment に値を渡す

今回は、FragmentFactory を使って、コンストラクタ経由で Fragment に値を渡す方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • JDK 11.0.12
  • Android Gradle Plugin 7.2.1
  • Kotlin 1.6.21
  • Gradle 7.4.2
  • androidx.fragment 1.4.1

概要

通常、Fragment のコンストラクタに引数を追加することはできません。コンストラクタに引数を追加すると、Fragment を生成する際に例外が発生するからです。

これは、FragmentManager に設定されている FragmentFactory が Fragment を生成するのですが、デフォルトの FragmentFactory は引数が存在しないコンストラクタを使用して、Fragment を生成しようとするためです。

よって、Fragment のコンストラクタに引数を追加して、その引数ありのコンストラクタで Fragment を生成するためには、FragmentFactory を継承したカスタムの FragmentFactory を作成し、これを FragmentManager に設定する必要があります。

今回は、実際に FragmentFactory を継承してカスタム FragmentFactory を作成しながら、FragmentFactory を使って、コンストラクタ経由で Fragment に値を渡す方法を解説します。

ライブラリのインポート

FragmentFactory を使用するため、アプリの build.gradle ファイルの dependenciesfragment-ktx を追加してください。

dependencies {
    implementation "androidx.fragment:fragment-ktx:1.4.1"
}

FragmentFactory を使って、Fragment に値を渡す

今回は、以下のようなサービスクラスをコンストラクタ経由で Fragment に渡すコードを作成します。

interface SampleService {
    fun getText(): String
}

class SampleServiceImpl : SampleService {
    override fun getText(): String = "Sample"
}

まず、Fragment から作成します。

Fragment ではコンストラクタ経由でさきほどの SampleService を受け取りたいため、コンストラクタ引数に SampleService を追加します。

class MainFragment(
    private val service: SampleService
) : Fragment(R.layout.main_fragment) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val textView: TextView = view.findViewById(R.id.message)
        textView.text = service.getText()
    }
}

次に、FragmentFactory を継承したカスタム FragmentFactory を作成します。

FragmentFactory を使って、引数ありのコンストラクタで Fragment を生成する場合、instantiate をオーバーライドします。このメソッドは、Fragment を生成するメソッドです。カスタム FragmentFactory でオーバーライドした instantiate では、引数 className が該当の Fragment である場合、その Fragment のコンストラクタを呼ぶようにします。そうではない場合、継承元クラスの instantiate を呼ぶようにします。

class CustomFragmentFactory(
    private val service: SampleService
) : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String) =
        if (className == MainFragment::class.java.name) {
            MainFragment(service)
        } else {
            super.instantiate(classLoader, className)
        }
}

最後に、Activity を作成します。

Activity では、FragmentManager にさきほどのカスタム FragmentFactory を設定します。なお、このカスタム FragmentFactory の設定は、継承元の onCreate を呼ぶ前に行ってください。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory =
            CustomFragmentFactory(SampleServiceImpl())
        super.onCreate(savedInstanceState)
    }
}

以上で、コンストラクタ経由で Fragment に値を渡せるようになります。

参考

フラグメントに依存関係を渡す

【Android × Kotlin】Activity Result API で他の Activity の結果を取得する

今回は、Activity Result API で他の Activity の結果を取得する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • JDK 11.0.12
  • Android Gradle Plugin 7.2.1
  • Kotlin 1.6.21
  • Gradle 7.4.2
  • androidx.activity 1.4.0
  • androidx.fragment 1.4.1

概要

以前、他の Activity の結果を取得する方法として、startActivityForResult と onActivityResult を使う方法がありました。しかし、現在この方法は非推奨になっており、代わりに Activity Result API を使う方法が推奨されています。

今回は、文字列を結果として返す Activity から Activity Result API でその結果を取得するサンプルの作成を通して、Activity Result API の使い方を解説します。

なお、Activity Result API は、ComponentActivity と Fragment の両方で使用することができるため、Activity と Fragment のそれぞれで解説します。

Activity で Activity Result API を使う

ライブラリのインポート

Activity で Activity Result API を使う場合、アプリの build.gradle ファイルの dependenciesactivity-ktx を追加してください。

dependencies {
    implementation "androidx.activity:activity-ktx:1.4.0"
}

他の Activity の結果を取得する

ここから、文字列を結果として返す Activity と、Activity Result API でその結果を取得する Activity を作成します。

まず、文字列を結果として返す Activity から作成します。

class SubActivity : AppCompatActivity(R.layout.sub_activity) {

    companion object {

        const val EXTRA_RESULT = "result"

        fun createIntent(context: Context) =
            Intent(context, SubActivity::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        findViewById<Button>(R.id.inputButton)
            .setOnClickListener {
                setResult(RESULT_OK, Intent().apply {
                    val editText: TextInputEditText =
                        findViewById(R.id.textInputEditText)
                    val text = editText.text.toString()
                    putExtra(EXTRA_RESULT, text)
                })
                finish()
            }
    }
}

この Activity には EditText と Button があり、Button が押されると EditText に入力された文字列を Activity の結果として返すようにします。

Activity の結果として返す際には、setResult に RESULT_OK と 返す文字列を格納した Intent を渡します。setResult を使って Activity の結果を返す方法は、startActivityForResult と onActivityResult を使っていたときと同じです。

次に、Activity Result API で他の Activity の結果を取得する Activity を作成します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    private val subActivityLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val text = result.data?.getStringExtra(SubActivity.EXTRA_RESULT) ?: ""
            findViewById<TextView>(R.id.resultTextView).text = text
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        findViewById<Button>(R.id.inputButton)
            .setOnClickListener {
                val intent = SubActivity.createIntent(this)
                subActivityLauncher.launch(intent)
            }
    }
}

Activity Result API を使う場合、まず registerForActivityResult を使って、他のアクティビティを開始するために使用する ActivityResultLauncher を取得します。

この registerForActivityResult には、他のアクティビティから結果を取得することを表す ActivityResultContracts.StartActivityForResult() を渡します。そして、結果を取得した後に呼び出されるコールバックも渡します。

    private val subActivityLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val text = result.data?.getStringExtra(SubActivity.EXTRA_RESULT) ?: ""
            findViewById<TextView>(R.id.resultTextView).text = text
        }
    }

そして、文字列を結果として返す Activity を開始する処理を作成します。Intent を生成し、さきほど取得した ActivityResultLauncher の launch に Intent を渡して、Activity を開始します。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        findViewById<Button>(R.id.inputButton)
            .setOnClickListener {
                val intent = SubActivity.createIntent(this)
                subActivityLauncher.launch(intent)
            }
    }

以上で、文字列を結果として返す Activity からその文字列を取得できるようになります。

Fragment で Activity Result API を使う

ライブラリのインポート

Fragment で Activity Result API を使う場合、アプリの build.gradle ファイルの dependenciesfragment-ktx を追加してください。

dependencies {
    implementation "androidx.fragment:fragment-ktx:1.4.1"
}

他の Activity の結果を取得する

ここから、Activity Result API で他の Activity の結果を取得する Fragment を作成します。

なお、文字列を結果として返す Activity については、Activity で Activity Result API を使う場合と同様なので、ここでは省略します。

以下が、実際のコードです。

class MainFragment : Fragment(R.layout.main_fragment) {

    private val subActivityLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            view?.let { view ->
                val text = result.data?.getStringExtra(SubActivity.EXTRA_RESULT) ?: ""
                val textView = view.findViewById<TextView>(R.id.resultTextView)
                textView.text = text
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<Button>(R.id.inputButton)
            .setOnClickListener {
                val intent = SubActivity.createIntent(requireContext())
                subActivityLauncher.launch(intent)
            }
    }
}

Activity のときと同様に、registerForActivityResult を使って、他のアクティビティを開始するために使用する ActivityResultLauncher を取得します。

    private val subActivityLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            view?.let { view ->
                val text = result.data?.getStringExtra(SubActivity.EXTRA_RESULT) ?: ""
                val textView = view.findViewById<TextView>(R.id.resultTextView)
                textView.text = text
            }
        }
    }

そして、文字列を結果として返す Activity を開始する処理を作成します。こちらも、Activity のときと同様です。

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<Button>(R.id.inputButton)
            .setOnClickListener {
                val intent = SubActivity.createIntent(requireContext())
                subActivityLauncher.launch(intent)
            }
    }

以上で、Fragment でも文字列を結果として返す Activity からその文字列を取得できるようになります。