Vim では ]]
や [[
入力で関数(セクション)単位にカーソル移動できるコマンドがあります。[[
だと一つ前の関数に移動、]]
だと一つ後ろの関数に移動、といった具合です。
自分は日記を ChangeLog 形式で書いていて、ChangeLog でもこの動作ができたら良いな、と常々思ってました。ただ Vim script はなんだか敷居が高そう。ということで近ごろ話題の ChatGPT に作ってもらうことにしました。
続きを読む今回は、Kotlin Serialization を使って JSON をパースする方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
最初に、Kotlin Serialization を使用するために必要なライブラリをインポートします。
まず、以下のようにプロジェクトの build.gradle の plugins
に Kotlin Serialization のプラグインを追加します。
// Project's build.gradle plugins { id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.20' apply false }
そして、モジュールの build.gradle の plugins
にも Kotlin Serialization のプラグインを追加します。
// Module's build.gradle plugins { id 'org.jetbrains.kotlin.plugin.serialization' }
後は、モジュールの build.gradle の dependencies
に以下を追加します。
// Module's build.gradle dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" }
以上で、ライブラリのインポートは完了です。
ここから、実装に入っていきます。
まず、パースする JSON のフォーマットをデータクラスで宣言する必要があります。今回は、以下のような JSON の読み書きを行ってみます。
{ "hidden_card": { "rank": "6", "suit": "SPADES" }, "visible_cards": [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] }
上記の JSON をデータクラスで宣言すると、以下のようになります。
@Serializable data class BlackjackHand( @SerialName("hidden_card") val hiddenCard: Card, @SerialName("visible_cards") val visibleCard: List<Card>, ) @Serializable data class Card( val rank: Char, val suit: Suit, ) enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES; }
Kotlin Serialization で使用するデータクラスには、@Serializable
を追加します。また、hidden_card
と hiddenCard
のように、JSON とデータクラスで名前が異なる場合、データクラスのプロパティに @SerialName
を付与して、対応する JSON の名前を指定します。なお、JSON とデータクラスで名前が一致する場合は、@SerialName
を省略できます。
以上で、データクラスの宣言は完了です。
Json.decodeFromString
に JSON 文字列を渡すと、パースした後のオブジェクトを返してくれます。
val json = """ { "hidden_card": { "rank": "6", "suit": "SPADES" }, "visible_cards": [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] } """.trimIndent() Log.d(TAG, Json.decodeFromString<BlackjackHand>(json).toString())
上記を実行すると Logcat に以下のログが表示され、JSON からオブジェクトにパースが成功していることがわかります。
D/MainActivity: BlackjackHand(hiddenCard=Card(rank=6, suit=SPADES), visibleCard=[Card(rank=4, suit=CLUBS), Card(rank=A, suit=HEARTS)])
逆に、Json.encodeToString
にオブジェクトを渡すと、パースした後の JSON を文字列で返してくれます。
val blackjackHand = BlackjackHand( Card('6', Suit.SPADES), listOf(Card('4', Suit.CLUBS), Card('A', Suit.HEARTS)) ) Log.d(TAG, Json.encodeToString(blackjackHand))
上記を実行すると Logcat に以下のログが表示され、オブジェクトから JSON にパースが成功していることがわかります。
D/MainActivity: {"hidden_card":{"rank":"6","suit":"SPADES"},"visible_cards":[{"rank":"4","suit":"CLUBS"},{"rank":"A","suit":"HEARTS"}]}
今回は、Moshi を使って JSON をパースする方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
最初に、Moshi を使用するために必要なライブラリをインポートします。
まず、今回は KSP を使用するため、KSP をインポートします。プロジェクトの build.gradle の plugins
に com.google.devtools.ksp
を追加します。
// Project's build.gradle plugins { id 'com.google.devtools.ksp' version '1.6.20-1.0.4' apply false }
そして、モジュールの build.gradle の plugins
にも com.google.devtools.ksp
を追加します。
// Module's build.gradle plugins { id "com.google.devtools.ksp" }
これで、KSP を使用できるようになります。
後は、Moshi を使用するためにモジュールの build.gradle の dependencies
に以下を追加します。
// Module's build.gradle dependencies { implementation "com.squareup.moshi:moshi:1.13.0" ksp "com.squareup.moshi:moshi-kotlin-codegen:1.13.0" }
以上で、ライブラリのインポートは完了です。
ここから、実装に入っていきます。
まず、パースする JSON のフォーマットをデータクラスで宣言する必要があります。今回、以下のような JSON の読み書きを行ってみます。
{ "hidden_card": { "rank": "6", "suit": "SPADES" }, "visible_cards": [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] }
上記の JSON をデータクラスで宣言すると、以下のようになります。
@JsonClass(generateAdapter = true) data class BlackjackHand( @Json(name = "hidden_card") val hiddenCard: Card, @Json(name = "visible_cards") val visibleCard: List<Card>, ) @JsonClass(generateAdapter = true) data class Card( val rank: Char, val suit: Suit, ) enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES; }
Moshi で使用するデータクラスには、@JsonClass(generateAdapter = true)
を追加します。また、hidden_card
と hiddenCard
のように、JSON とデータクラスで名前が異なる場合、データクラスのプロパティに @Json
を付与して、対応する JSON の名前を指定します。なお、JSON とデータクラスで名前が一致する場合は、@Json
を省略できます。
以上で、データクラスの宣言は完了です。
val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter(BlackjackHand::class.java)
上記 1 行目の Moshi.Builder().build()
にて Moshi のインスタンスを生成します。そして、2 行目で Moshi のインスタンスに JSON のデータクラスを渡して、JsonAdapter を生成します。この JsonAdapter を使って、JSON をパースします。
さきほど生成した JsonAdapter を使って JSON からオブジェクトにパースします。JsonAdapter#fromJson に JSON 文字列を渡すと、パースした後のオブジェクトを返してくれます。
val json = """ { "hidden_card": { "rank": "6", "suit": "SPADES" }, "visible_cards": [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] } """.trimIndent() val blackjackHand = jsonAdapter.fromJson(json) Log.d(TAG, blackjackHand.toString())
上記を実行すると Logcat に以下のログが表示され、JSON からオブジェクトにパースが成功していることがわかります。
D/MainActivity: BlackjackHand(hiddenCard=Card(rank=6, suit=SPADES), visibleCard=[Card(rank=4, suit=CLUBS), Card(rank=A, suit=HEARTS)])
逆に、オブジェクトから JSON にパースすることも可能です。JsonAdapter#toJson にオブジェクトを渡すと、パースした後の JSON を文字列で返してくれます。
val blackjackHand = BlackjackHand( Card('6', Suit.SPADES), listOf(Card('4', Suit.CLUBS), Card('A', Suit.HEARTS)) ) val json = jsonAdapter.toJson(blackjackHand) Log.d(TAG, json)
上記を実行すると Logcat に以下のログが表示され、オブジェクトから JSON にパースが成功していることがわかります。
D/MainActivity: {"hidden_card":{"rank":"6","suit":"SPADES"},"visible_cards":[{"rank":"4","suit":"CLUBS"},{"rank":"A","suit":"HEARTS"}]}
今回は、Activity Result API で他の Activity の結果を取得する方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
以前、他の Activity の結果を取得する方法として、startActivityForResult と onActivityResult を使う方法がありました。しかし、現在この方法は非推奨になっており、代わりに Activity Result API を使う方法が推奨されています。
今回は、文字列を結果として返す Activity から Activity Result API でその結果を取得するサンプルの作成を通して、Activity Result API の使い方を解説します。
なお、Activity Result API は、ComponentActivity と Fragment の両方で使用することができるため、Activity と Fragment のそれぞれで解説します。
Activity で Activity Result API を使う場合、アプリの build.gradle ファイルの dependencies
に activity-ktx
を追加してください。
dependencies {
implementation "androidx.activity:activity-ktx:1.4.0"
}
ここから、文字列を結果として返す 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 を使う場合、アプリの build.gradle ファイルの dependencies
に fragment-ktx
を追加してください。
dependencies {
implementation "androidx.fragment:fragment-ktx:1.4.1"
}
ここから、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 からその文字列を取得できるようになります。
今回は、Kotlin Coroutines について解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
そして、アプリの 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 とは Coroutine の有効範囲です。Coroutines で処理を実行したい場合、この CoroutineScope を開始する必要があります。
また、以下のように CoroutineScope は内部に CoroutineContext を保持しており、CoroutineContext の設定通りに Coroutine を実行します。
public interface CoroutineScope { public val coroutineContext: CoroutineContext }
CoroutineContext とは Coroutine の設定のようなものです。CoroutineContext を保持する CoroutineScope は、CoroutineContext の設定通りに Coroutine を実行します。
以下に、代表的な CoroutineContext の要素を記載します。
要素 | 説明 |
---|---|
Job | Coroutine のハンドラ。Coroutine のキャンセルができる。 |
CoroutineDispatcher | Coroutine が動作するスレッドを指定する。 |
CoroutineName | Coroutine の名前。デバッグでこの名前が表示される。 |
CoroutineExceptionHandler | Exception のハンドラ。Coroutine でキャッチできなかった Exception を処理できる。 |
Coroutine ビルダーとは CoroutineScope を開始する関数です。さきほどのサンプルコードでは、runBlocking
と launch
が Coroutine ビルダーです。Coroutine ビルダーに Coroutines で実行したい処理を渡すと、その処理は CoroutineScope 内部で実行されます。
以下に、代表的な Coroutine ビルダーを記載します。
Coroutine ビルダー | 特徴 |
---|---|
runBlocking | 現在のスレッドをブロックする。 CoroutineScope の外からでも呼び出せる。 ブロック内部の処理の結果を返せる。 |
launch | 現在のスレッドをブロックしない。 CoroutineScope の内部からのみ呼び出せる。 ブロック内部の処理の結果を返せない。 |
async | 現在のスレッドをブロックしない。 CoroutineScope の内部からのみ呼び出せる。 ブロック内部の処理の結果を返せる。 |
上記の 3 つの内、launch と async については、CoroutineScope の拡張関数として定義されているため、CoroutineScope の内部からしか呼び出すことができません。runBlocking については、CoroutineScope の拡張関数ではないため、CoroutineScope の外からでも呼び出すことができます。
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.") }
今回は、Retrofit を使って HTTP 通信を行う方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
Retrofit は HTTP クライアント通信ができるライブラリです。使用できる HTTP メソッドは、GET、POST、PUT、PATCH、DELETE、OPTIONS、HEAD です。
Retrofit では、使用する API をインターフェースとアノテーションにて定義します。
なお、Retrofit の実態は OkHttp のラッパーであり、OkHttpClient を使用して機能を拡張することができます。例えば、HTTP 通信の送受信内容を logcat に表示するができます。
実際に Retrofit にて HTTP 通信を行ってみます。
今回は、以下の GitHub の API を使って、指定したユーザーのリポジトリ情報を取得します。
初めに、Retrofit を使えるようにするために、アプリの build.gradle ファイルに次の依存関係を追加します。
dependencies {
implementation "com.squareup.retrofit2:retrofit:2.9.0"
}
次に、今回はインターネット通信を行うので、AndroidManifest.xml に android.permission.INTERNET
を追加します。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tatsuro.app.retrofitsample02"> <uses-permission android:name="android.permission.INTERNET" /> ︙ </manifest>
ここから実装に入っていきます。
まず、次のように使用する API を定義します。
interface GitHubService { @GET("users/{username}/repos") fun loadRepos( @Path("username") username: String, @Query("sort") sort: String ): Call<ResponseBody> }
API を定義するために、インターフェースを宣言します。
interface GitHubService {
︙
}
そして、使用する API 1 つに対して、メソッドを 1 つ追加します。
@GET("users/{username}/repos") fun loadRepos( @Path("username") username: String, @Query("sort") sort: String ): Call<ResponseBody>
追加したメソッドには、次のように使用する API のメソッドに合わせてアノテーションを追加します。
@GET("users/{username}/repos")
今回は GET メソッドなので、@GET を追加します。そして、@GET の引数に 今回使用する API のパスを設定します。このとき、API の中の username
パラメータは実際に API を呼び出すときに後から設定できるようにするために、波括弧で囲います。
そして、メソッドに引数を追加します。
まず、次のようにAPI の中の username
パラメータを引数に追加します。
@Path("username") username: String
この引数には、それがパスの一部であることを表す @Path を追加して、その引数にパタメータの名称 "username"
を設定します。
次に、API のクエリを引数に追加します。
@Query("sort") sort: String
今回は、sort
クエリを引数に追加します。この引数には、それがクエリであることを表す @Query を追加して、その引数にクエリの名称 "sort"
を設定します。
最後に、メソッドに戻り値 Call<ResponseBody>
を設定します。実際には、この Call インスタンスを使用して API を呼び出し、その結果を受け取ります。
以上で、API インターフェースの定義は完了です。
さきほど定義したメソッドを使って、HTTP 通信の API を呼び出します。
Retrofit で HTTP 通信を行う場合、同期方式で HTTP 通信を行う Call#execute と 非同期方式で HTTP 通信を行う Call#enqueue があります。
メソッド | 方式 |
---|---|
execute | 同期 |
enqueue | 非同期 |
ここでは、これら 2 つの方式で HTTP 通信を行います。
同期方式 Call#execute で HTTP 通信を行う場合、以下のように実装します。
class MainActivity : AppCompatActivity(R.layout.main_activity) { companion object { private const val TAG = "MainActivity" private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build() private val service = retrofit.create(GitHubService::class.java) private val loadRepos = service.loadRepos("octocat", "created") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) } } } }
まず、Retrofit のインスタンスを作成します。このインスタンスには、API のホストを渡す必要があります。
companion object { ︙ private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build()
次に、API インターフェースの実体を作成します。Retrofit#create に、さきほど定義した API インターフェース を渡し、API インターフェースの実体を取得します。
private val service = retrofit.create(GitHubService::class.java)
そして、API の Call インスタンスを取得します。このとき、API にユーザ名とソートのパラメータを設定します。
private val loadRepos = service.loadRepos("octocat", "created")
Call インスタンスを取得できたので、Call#execute を使って API を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) } } }
まず、UI スレッド上で Retrofit を使って同期通信を行うことはできないので、UI スレッドとは別のスレッドを作成します。また、Call インスタンスは、連続で同じ API を呼び出すことができないので、Call#execute を呼ぶ前に Call#clone を使い、API 呼び出しのたびに、新しい Call インスタンスを生成するようにします。
private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } ︙
API 呼び出しは通信不良などで失敗する場合があり、その場合は Call#execute が例外を発します。よって、今回は、runCatching を使って Call#execute が発する例外をキャッチできるようにします。
そして、API 呼び出し成功と失敗それぞれに対して、その結果を Logcat に出力します。
なお、Call#execute が例外を発しなかった場合でも、HTTP のステータスコードが 404 または 500 の場合があるため、Response#isSuccessful で HTTP 通信が成功であることを確認します。
runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) }
以上の API 呼び出しを行うと、その結果が Logcat に出力されます。
非同期方式 Call#enqueue で HTTP 通信を行う場合、以下のように実装します。
class MainActivity : AppCompatActivity(R.layout.main_activity) { companion object { private const val TAG = "MainActivity" private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build() private val service = retrofit.create(GitHubService::class.java) private val loadRepos = service.loadRepos("tatsuroappdev") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { loadRepos.clone().enqueue(object : Callback<ResponseBody> { override fun onResponse( call: Call<ResponseBody>, response: Response<ResponseBody> ) { if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } override fun onFailure( call: Call<ResponseBody>, t: Throwable ) { Log.e(TAG, t.toString()) } }) } }
Call インスタンスを取得するまでの流れは、同期方式のときと同じであるため、解説を省略します。また、API 呼び出しのたびに、Call#clone を使って、新しい Call インスタンスを生成するのも、同期方式のときと同様に行います。
Call#enqueue を使って API 呼び出しを行う場合、API 呼び出し後に呼び出される Callback を定義して、呼び出し後の処理を実装します。
Callback#onResponse は、通信が成功した場合に呼び出されます。処理の中身は、同期方式のときの Result.onSuccess と同じにします。
Callback#onFailure は、通信が失敗した場合に呼び出されます。処理の中身は、同期方式のときの Result.onFailure と同じにします。
loadRepos.clone().enqueue(object : Callback<ResponseBody> { override fun onResponse( call: Call<ResponseBody>, response: Response<ResponseBody> ) { if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } override fun onFailure( call: Call<ResponseBody>, t: Throwable ) { Log.e(TAG, t.toString()) } })
以上の API 呼び出しを行うと、その結果が Logcat に出力されます。
今回は、map や switchMap で LiveData を変換する方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
また、実際に LiveData の map や switchMap を使用する際は、以下のようにアプリの build.gradle ファイルに lifecycle-livedata-ktx
をインポートしてください。
dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
}
map は LiveData の拡張関数の 1 つです。
public inline fun <X, Y> LiveData<X>.map( crossinline transform: (X) -> Y ): LiveData<Y>
この関数の使い方は、変換したい LiveData に対して変換処理 transform を渡すだけです。そうすると、変換元の LiveData が変更されたとき、map に渡した transform が実行されて、変換後の LiveData を取得できます。
例えば、整数値を持つ LiveData の値を 2 倍にしたい場合、以下のような使い方をします。
val before = MutableLiveData(0) // before を 2 倍にして返す。 val after = before.map { it * 2 }
内部的には MediatorLiveData で実装されており、監視対象が 1 つだけの MediatorLiveData のような振る舞いをします。
switchMap も map と同様に LiveData の拡張関数の 1 つです。
public inline fun <X, Y> LiveData<X>.switchMap( crossinline transform: (X) -> LiveData<Y> ): LiveData<Y>
使い方も、map と同様に変換したい LiveData に対して変換処理 transform を渡すだけです。
map との違いは、変換処理 transform の中で変換結果を返すとき、値の型ではなくて LiveData 型で返す必要があることです。
よって、さきほどの整数値を 2 倍にする例を switchMap で実装する場合、以下のようになります。
val before = MutableLiveData(0) // before を 2 倍にして返す。 val after = before.switchMap { MutableLiveData(it * 2) }
switchMap の使い道は、戻り値が LiveData 型のメソッドで変換を行いたいときでしょう。
例えば、Room データベースの DAO で、ID を引数に渡すとその ID に紐付くユーザーデータを読み出すメソッドを変換に使いたい場合です。
@Dao interface UserDao { @Query("SELECT * FROM user WHERE id = :id") fun loadUserById(id: Int): LiveData<User> }
class MainViewModel( private val id: Int, private val dao: UserDao ) : ViewModel() { private val _id = MutableLiveData(id) // ID からユーザーデータを読み出す。 val user = _id.switchMap { dao.loadUserById(it) } fun setId(id: Int) { _id.value = id } ︙ }
上記のように switchMap であれば、そのメソッドの戻り値をそのまま返すだけで、LiveData 型のユーザーデータを取得できます。
また、上記の user
は LiveData 型なので、user
を LiveData#observe にて監視しておけば、ID が変わるたびに user
の変更を補足できるようになります。