序文
Kotlin Flow は、データ ストリームを非同期にフェッチするための Kotlin コルーチン ライブラリです。これにより、非同期に生成された複数の値をコレクションのような方法で発行し、RxJava のような演算子を介してチェーンでこれらの値を処理できるようになります。
基本的な考え方
フローの基本概念は、フローが非同期に生成された値のシーケンスを表すということです。これらの値はさまざまな時点で送信される可能性があり、受信側はサスペンド関数を使用してこのフローをサブスクライブし、これらの値を消費できます。一つずつ。 Flow を使用すると、一度に大量のデータを取得することによって発生するメモリの問題を回避でき、LiveData よりも柔軟性が高くなります。
例
最も単純なフローのサンプル コードの 1 つは次のとおりです。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
val flow = flow {
for (i in 1..3) {
delay(100)
emit(i) // 发射数据
}
}
flow.collect {
value -> println(value) }
}
上記のコードでは、100 ミリ秒ごとに 1、2、3 の数字を出力するフローを作成します。 collect メソッドを使用してこのフローをサブスクライブし、これらの数値を順番に消費して、コンソールに出力します。
この例には、構築演算子flow
と端末演算子の 2 つの演算子が含まれています。collect
要約すると、フロー呼び出しプロセスは次のように簡略化されます。2 つの演算子 + 2 つのクロージャ + エミット関数:
収集オペレーターは呼び出しをトリガーし、フロー・クロージャーを実行します。
フロー クロージャで Emit 関数が呼び出され、Collect クロージャが実行されます。
演算子は 3 つのカテゴリに分類されます。
构建操作符
構築演算子は Flow オブジェクトの作成に使用され、Java の Stream.of() メソッドに似ています。一般的なビルド演算子は次のとおりです。
flowOf: 指定された一連の値からフローを作成します。
asFlow: コレクション、配列、またはイテレータをフローに変換します。
emptyFlow: 空のフローを作成します。
channelFlow: チャネルにデータを送信し、そこからデータを受信することでフローを作成します。
build オペレーターを使用してフローを作成する場合は、フローの実行環境に注意する必要があります。フローがコルーチンのスコープ外で作成された場合、フローはすぐに実行されます。フローがコルーチン スコープで作成された場合、その操作はコルーチン スコープによって制限されます。
中间操作符
中間演算子は、フロー上でデータ変換、フィルタリングなどを実行するために使用され、返されるオブジェクトはフロー オブジェクトのままです。一般的な中間演算子は次のとおりです。
map: フロー内の各要素を変換し、新しいフローを返します。
filter: 条件に基づいてフロー内の要素をフィルターし、新しいフローを返します。
変換: フロー要素に対してカスタム変換操作を実行できます。
take: フロー内の最初のいくつかの要素を選択します。
zip: 2 つのフローを 1 つのフローに結合します。各要素は 2 つのフローの対応する要素で構成されます。
フローの中間オペレーターは直列に接続して操作チェーンを形成することができ、通過中にデータのみが変換および処理され、最後のオペレーターが呼び出されるまで実際のデータの消費はトリガーされません。
末端操作符
ターミナル オペレーターは、フローの実行をトリガーし、結果を返すために使用されます。一般的な端末演算子は次のとおりです。
collect: フローを走査し、各要素に対して指定された操作を実行します。
reduce: フロー内の要素を 1 つの値に集約します。
singleOrNull: フローに要素が 1 つだけあるかどうかを判断します。要素が 1 つだけある場合は要素を返し、それ以外の場合は null を返します。
toList/toSet: フロー内の要素をリストまたはセットに変換します。
ターミナル オペレーターは、終了する前にフロー内のすべての要素が消費されるのを待つ必要があるため、コルーチンをブロックすることに注意してください。コルーチン内に複数のターミナル オペレーターがある場合、各ターミナル オペレーターはフロー全体を再トラバースします。
操作符的使用示例:
1.フロー変換演算子map
マップは、フロー内のデータをマップして変換し、新しいフローを返すことができます。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
val lengthFlow = flowOf(1.0, 2.0, 3.14, 10.0) // 表示各种单位的长度
.map {
it * 1000 } // 将长度转换为 mm
.map {
it.toString() + "mm" } // 转换为字符串表示并加上单位
lengthFlow.collect {
println(it) }
}
まず、さまざまな単位で長さを表す Flow lengthFlow を作成しました。マップを使用してフロー内のデータをミリメートル (mm) 単位の長さに変換し、次にマップを使用して長さを文字列表現に変換し、単位を追加します。最後に、collect メソッドを使用してフローを反復処理し、出力を出力します。
上の例から、マップ オペレーターの役割がわかります。フロー内の要素は、変換オペレーターを通じて別のフォームにマップされ、新しいフローが渡されます。
2.フローフィルターオペレーターfilter
フィルター。指定された条件に従ってフロー内のデータをフィルターし、新しいフローを返すことができます。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
val ageFlow = flowOf(15, 27, 18, 20, 12, 25) // 表示人的年龄集合
.filter {
it >= 18 } // 过滤出大于或等于 18 岁的年龄
.map {
"成年人 $it 岁" } // 转换成字符串表示格式
ageFlow.collect {
println(it) }
}
まず、人々の年齢のコレクションを表すフロー ageFlow を作成します。フィルターを使用して 18 歳以上のすべての年齢を保持するようにフィルターし、マップを使用して保持されている年齢を文字列形式で表される成人向け情報に変換します。最後に、collect メソッドを使用してフローを反復処理し、出力を出力します。
上記の例からフィルター演算子の役割がわかりますが、この演算子を通じてフロー内の要素が条件に従ってフィルターされ、新しいフローが渡されます。
3.フロースレッド切り替えflowOn
フロー スイッチング スレッドは RxJava よりも単純です。 flowOnを使用するだけです。
次の例は、フロー構築オペレーションとマップ オペレータの両方が flowOn の影響を受けることを示しています。
flow {
for(i in 1..5){
delay(100)
emit(i)
}
}.map {
it *it}
.flowOn(Dispatchers.IO)
.collect {
println(it)
}
どのスレッド collect()
が属するかは、フロー全体がどの CoroutineScope
に属するかによって決まります。
たとえば、次のコードcollect()はメインスレッドにあります。
fun main() = runBlocking {
flow {
for(i in1..5){
delay(100)
emit(i)
}
}.map {
it *it}
.flowOn(Dispatchers.IO)
.collect {
println("${
Thread.currentThread().name}: $it")
}
}
結果:
main:1
main:4
main:9
main:16
メイン:25
4.フローハンドル背圧buffer
時間のかかるデータをコンシューマーに送信するプロデューサーがあるとします。 Thread.sleep 関数を呼び出すことで、このプロセスをシミュレートし、プロデューサーとコンシューマーの動作を遅くすることができます。
fun producer(): Flow<Int> = flow {
repeat(10) {
// Generate 10 numbers
// Simulate slow production process
Thread.sleep(1000)
// Send the data to the consumer
emit(it)
println("Producer sent: $it")
}
}
suspend fun consumer() {
producer()
.onEach {
// Simulate slow consumption process
Thread.sleep(2000)
// Process the data
println("Consumer received: $it")
}
.collect()
}
fun main() = runBlocking<Unit> {
consumer()
}
上記のコードでは、flow 関数を使用して、0 から 9 までの整数を送信するプロデューサー関数を定義します。プロデューサはデータを送信するたびに 1 秒間スリープします。消費者がバックプレッシャーの問題を起こしやすくするために、消費者がデータを処理する前に毎回 2 秒スリープできるように、より遅い消費プロセスをシミュレートしました。
上記のコードは Flow のキャッシュ サイズを明示的に指定していませんが、Flow は実際にデフォルトのキャッシュ処理メカニズムを提供し、可能な限りデータをキャッシュし、すべてのデータがスムーズに処理できるようにします。ただし、コンシューマはプロデューサがデータを送信するよりも低速でデータを処理するため、最終的にはバックプレッシャーの問題によりプログラムがクラッシュします。
コンシューマ関数にバッファ演算子を追加して、フローのバッファ サイズを明示的に指定して、データ フローをより適切に制御できます。
suspend fun consumer() {
producer()
.buffer(10) // Set buffer size to 10
.onEach {
// Simulate slow consumption process
Thread.sleep(2000)
// Process the data
println("Consumer received: $it")
}
.collect()
}
この例では、フローにバッファ サイズ 10 を明示的に指定します。これは、コンシューマがデータの一部を処理するまで、プロデューサはデータの送信を続行できないことを意味します。決定論的なバックプレッシャー戦略を通じて、データ バックログの問題を回避し、プログラムの安定性と信頼性を確保できます。
5. アップストリームは古いデータを上書きしますconflate
conflate オペレーターは、連続したデータを 1 つのデータにマージし、それを下流に送信できます。新しいデータは、ダウンストリームが前のデータを処理した後にのみ受信されます。これは、中間データが破棄され、最新のデータのみが保持される可能性があることを意味します。このメカニズムは、「マージ バックプレッシャー戦略」としても知られています。
たとえば、次の方法でプロデューサーをシミュレートして、タイムスタンプ付きのデータを生成できます。
fun producer(): Flow<Pair<String, Long>> = flow {
var counter = 0L
while (true) {
// Simulate a slow production process
delay(1000)
// Generate a new data point
val data = Pair("Data point ${
counter}", System.currentTimeMillis())
// Emit the data point to the consumer
emit(data)
counter++
}
}
このプロデューサーでは、新しいデータ ポイントを毎秒生成し、それをタイムスタンプ付きのペア オブジェクトにラップし、emit を呼び出してコンシューマーに送信します。
次に、conflate オペレーターをフローに追加して、データをマージし、古いデータを上書きします。
suspend fun consumer() {
producer()
.onEach {
data ->
// Simulate a slow processing time
delay(2000)
// Process the data
println("Processing data: ${
data.first}, timestamp: ${
data.second}")
}
.conflate() // Use conflation to merge data
.collect()
}
このコンシューマーでは、遅いプロセスをシミュレートし、conflate オペレーターを使用してデータを結合し、古いデータを上書きします。最後に、collect 関数を呼び出してフローの実行プロセスを開始します。
プロデューサーとコンシューマーの処理速度が一致しない場合、上流で大量のデータが生成され、下流での処理が間に合わない可能性があります。この場合、conflate オペレーターを使用すると、ダウンストリームが最新のデータのみを取得できるようになり、データ バックログの問題を最小限に抑えることができます。
6. フローが最新の値に変更されるflatMapLatest
またはtransformLatest
この演算子は要素ごとに新しいストリームを生成し、下流で使用するために最新のストリームのみを保持します。
たとえば、データを更新する関数 updateData があるとします。この関数は新しいデータ オブジェクトを返し、可変間隔でデータを更新します。これをフローにラップし、 flatMaplatest を呼び出すことで最新のデータを自動的に更新できます。
fun updateData(): Flow<Data> = flow {
while (true) {
delay(Random.nextLong(1000, 5000))
// Generate a new data point
val newData = Data(Random.nextInt(), "Updated at ${
System.currentTimeMillis()}")
emit(newData)
}
}
suspend fun processData() {
updateData()
.flatMapLatest {
data ->
// Process the data
flow {
delay(2000)
println("Processing data: $data")
emit(data)
}
}
.collect()
}
この例では、最初に updateData 関数を定義します。この関数は、一部のデータの更新プロセスをシミュレートし、データ型オブジェクトを持つフローを返します。次に、 flatMaplatest オペレーターを使用して、更新された Data オブジェクトを新しいフローに変換し、以前のフローから自動的にサブスクライブを解除します。これは、ダウンストリームが最新のデータのみを受信することを意味します。最後に、collect 関数を呼び出してフローの実行プロセスを開始します。
プログラムを実行すると、processData 関数が最新のデータを出力し続けていることがわかります。これは、 flatMaplatest 演算子が最新のデータを正確に取得し、古いデータの混乱を避けるのに役立つことを示しています。
7. 最新データの収集collectLatest
この関数は収集関数に似ていますが、最新のデータのみを保持し、以前のデータはキャンセルされます。
たとえば、データを更新する関数 updateData があるとします。この関数は新しいデータ オブジェクトを返し、可変間隔でデータを更新します。これをフローにラップし、collectlatest を呼び出して最新のデータをダウンストリームで取得できます。
fun updateData(): Flow<Data> = flow {
while (true) {
delay(Random.nextLong(1000, 5000))
// Generate a new data point
val newData = Data(Random.nextInt(), "Updated at ${
System.currentTimeMillis()}")
emit(newData)
}
}
suspend fun processData() {
updateData()
.onEach {
data ->
// Process the data
println("Processing data: $data")
delay(2000)
}
.collectLatest {
// Collect latest data only
println("Latest data: $it")
}
}
この例では、最初に updateData 関数を定義します。この関数は、一部のデータの更新プロセスをシミュレートし、データ型オブジェクトを持つフローを返します。次に、onEach 関数を使用して、更新された各 Data オブジェクトを処理し、その内容を出力します。次に、collectlatest 関数を呼び出して最新のデータを取得し、出力します。
プログラムを実行すると、processData 関数が最新のデータを出力し続けていることがわかります。これは、collectlatest 関数が最新のデータを正確に取得し、古いデータの混乱を避けるのに役立つことを示しています。
複数のフローオペレーター
多くの場合、単一のフローを運用するだけでなく、特定のビジネス シナリオを実装するために複数のフローを組み合わせる必要がある場合があります。
1. 流れを平坦化するflatMapConcat
flatMapConcat 演算子を使用して、最初のインターフェイスを要求した後、それによって返されたデータが 2 番目のインターフェイスを要求するためのパラメーターとして使用される方法を示します。
data class User(val id: Int, val name: String)
fun getUserById(id: Int): Flow<User> = flow {
// 模拟网络请求获取用户数据
delay(1000)
emit(User(id, "User-$id"))
}
fun getOrdersByUserId(userId: Int): Flow<String> = flow {
// 模拟网络请求获取订单数据
delay(1000)
emit("Orders for user $userId")
}
suspend fun main() {
getUserById(1)
.flatMapConcat {
user ->
getOrdersByUserId(user.id)
}
.collect {
orders ->
println(orders)
}
}
この例では、まず getUserById と getOrdersByUserId という 2 つの関数を定義します。 getUserById 関数はユーザー ID を受け取り、ユーザー オブジェクトを表す User オブジェクトを含むフローを返します。 getOrdersByUserId 関数はユーザー ID を受け取り、注文データのフローを返します。
main 関数では、まず getUserById 関数を呼び出してユーザー データを取得します。次に、 flatMapConcat オペレーターを使用して、そのユーザー オブジェクトをその注文を表す別のフローに変換します。 flatMapConcat では、上流のフローから発行されたユーザー オブジェクトを受け取り、その ID を使用して 2 番目の関数 getOrdersByUserId を呼び出すラムダ式を指定します。
flatMapConcat オペレーターは、最初のフローのすべての要素が処理されるのを待ってから、2 番目のフローの要素を順番にマージしてフラット化するため、最初にユーザー データを取得し、次にデータを注文する順序が保証されます。
2 番目のフローが新しい要素を発行すると、それらは処理のために下流に送信されます。
2. 流れを平坦化するflatMapMerge
2 つのインターフェイスがあるとします。
/api/users/{id}/posts
、指定したユーザーによって公開されたすべての投稿を取得するために使用されます
/api/posts/{id}/comments
、指定された投稿のすべてのコメントを取得するために使用されます
ここで、ユーザーが作成したすべての投稿に対するすべてのコメントを同時に取得する必要があります。 flatMapMerge 演算子を使用してこれを実現できます。
data class Post(val id: Int, val title: String, val userId: Int)
data class Comment(val id: Int, val postId: Int, val content: String)
fun getPostsByUserId(userId: Int): Flow<List<Post>> = flow {
// 模拟网络请求获取用户发表的所有帖子
delay(1000)
emit(listOf(Post(1, "Post 1", userId), Post(2, "Post 2", userId)))
}
fun getCommentsByPostId(postId: Int): Flow<List<Comment>> = flow {
// 模拟网络请求获取帖子的所有评论
delay(1000)
emit(listOf(Comment(1, postId, "Comment 1"), Comment(2, postId, "Comment 2")))
}
suspend fun main() {
getPostsByUserId(1)
.flatMapMerge {
posts ->
flow {
for (post in posts) {
emit(getCommentsByPostId(post.id))
}
}
}
.flattenConcat()
.collect {
comment ->
println(comment)
}
}
この例では、getPostsByUserId と getCommentsByPostId という 2 つの関数を定義します。これらは、ユーザーが投稿したすべての投稿と、投稿に対するすべてのコメントをそれぞれ取得するために使用されます。各関数はオブジェクトのリストを含むフローを返します。
main 関数では、まず getPostsByUserId 関数を呼び出して、ユーザーが公開したすべての投稿を取得します。次に、 flatMapMerge オペレーターを使用して、この投稿リストをコメントを表す複数のフローに変換します。 flatMapMerge では、上流のフローから出力された投稿のリストを受け取り、for ループを通じて投稿ごとに getCommentsByPostId 関数を呼び出してコメント データを取得するラムダ式を指定します。
flatMapMerge オペレーターは複数のフローを同時に処理できるため、すべてのコメント データが確実に出力されるまで、投稿のコメント データを待つ必要はありません。
次に、 flattenConcat オペレーターを使用して、複数のフローの要素を 1 つのフローにマージおよびフラット化し、正しい順序を確保します。最後に、collect 関数を使用してコメント情報を出力します。
3. 流れを平坦化するflatMapLatest
2 つのインターフェイスからデータを取得する必要があるとします。
/api/posts/latest
、最新の投稿リストを取得するために使用されます
/api/posts/search?key={keyword}
、キーワードに基づいて投稿リストを検索するために使用されます
ユーザーがキーワードを入力して「検索」ボタンをクリックしたとき、リソースを節約し、応答を迅速化するために、最新の投稿リストのリクエストをすぐにキャンセルする必要があります。この目標は、Kotlin コルーチンによって提供される flatMaplatest オペレーターと withTimeoutOrNull 関数を使用して達成できます。
次の例では、まず getlatestPosts と searchPosts という 2 つの関数を定義します。これらの関数は、それぞれ最新の投稿リストを取得し、キーワードに基づいて投稿リストを検索するために使用されます。各関数はオブジェクトのリストを含むフローを返します。ネットワークリクエストの時間をシミュレートするために、特定の遅延時間を指定しました。
次に、main 関数で、 flatMaplatest オペレーターを使用してこれら 2 つのフローを結合します。 flatMaplatest では、上流のフローから出力されたキーワードを受け取り、searchPosts 関数を呼び出して一致する投稿のリストを取得するラムダ式を指定します。この時点で getlatestPosts 関数がまだ進行中の場合は、withTimeoutOrNull 関数を使用してリクエストをキャンセルします。
data class Post(val id: Int, val title: String)
fun getLatestPosts(): Flow<List<Post>> = flow {
delay(1000)
emit(listOf(Post(1, "Latest Post 1"), Post(2, "Latest Post 2")))
}
fun searchPosts(keyword: String): Flow<List<Post>> = flow {
delay(1000)
val matchedPosts = listOf(
Post(3, "Matched Post 1"),
Post(4, "Matched Post 2"),
Post(5, "Matched Post 3")
)
emit(matchedPosts)
}
suspend fun main() {
val keywordFlow = MutableStateFlow("")
keywordFlow
.debounce(300) // 防抖动,避免过于频繁的搜索请求
.flatMapLatest {
keyword ->
flow {
// 如果最新帖子列表请求仍在进行中,则取消该请求
withTimeoutOrNull(500) {
getLatestPosts().collect()
}
emit(searchPosts(keyword).toList())
}
}
.collect {
posts ->
println(posts)
}
// 模拟用户输入关键字并发出搜索请求
keywordFlow.value = "Kotlin"
}
この例では、MutableStateFlow を使用して、ユーザーがキーワードを入力することをシミュレートします。ユーザーがコンテンツを入力するたびに、新しい検索リクエストがトリガーされます。また、頻繁すぎる検索リクエストを避けるために debounce 演算子も使用します。
ユーザーがキーワードを入力すると、 flatMaplatest オペレーターはすぐに検索リクエストの実行を開始します。この時点で getlatestPosts 関数のリクエストがまだ進行中の場合は、リクエストをキャンセルします。次に、searchPosts 関数リクエストが返され、検索結果が出力されるのを待ちます。
4. 合流の流れcombine
2 つのインターフェイスからデータを取得する必要があるとします。
/api/users/{id}/profile
は、指定されたユーザーの基本情報を取得するために使用されます
/api/users/{id}/posts
は、指定されたユーザーによって公開されたすべての投稿を取得するために使用されます
ユーザーの 2 つのデータを取得する必要がある場合、結合演算子を使用して 2 つのフローを結合し、2 つのデータを含む新しいフロー オブジェクトを返すことができます。
以下の例では、これら 2 つのデータを UserProfile クラスにマージします。
data class UserProfile(val userInfo: UserInfo, val posts: List<Post>)
data class UserInfo(val id: Int, val name: String, val age: Int)
data class Post(val id: Int, val title: String)
fun getUserInfo(userId: Int): Flow<UserInfo> = flow {
// 模拟网络请求获取用户基本信息
delay(1000)
emit(UserInfo(userId, "user_$userId", 20))
}
fun getPostsByUserId(userId: Int): Flow<List<Post>> = flow {
// 模拟网络请求获取用户发表的所有帖子
delay(1000)
emit(listOf(Post(1, "Post 1"), Post(2, "Post 2")))
}
suspend fun main() {
getUserInfo(1)
.combine(getPostsByUserId(1)) {
userInfo, posts ->
UserProfile(userInfo, posts)
}
.collect {
userProfile ->
println(userProfile.userInfo)
println(userProfile.posts)
}
}
この例では、まず getUserInfo と getPostsByUserId という 2 つの関数を定義します。これらは、それぞれ指定されたユーザーの基本情報とすべての投稿のリストを取得するために使用されます。各関数はオブジェクトのリストを含むフローを返します。
main 関数では、まず getUserInfo 関数を呼び出して、指定したユーザーの基本情報を取得します。次に、結合演算子を使用してこのフローをフローの getPostsByUserId 関数と結合し、ユーザーの基本情報と上流のフローから出力された投稿のリストを受け取り、それらを UserProfile オブジェクトに結合するラムダ式を指定します。
最後に、collect 関数を使用して UserProfile オブジェクトを出力します。
5. 合流の流れzip
2 つのインターフェイスからデータを取得する必要があるとします。
/api/users/{id}/profile
は、指定されたユーザーの基本情報を取得するために使用されます
/api/users/{id}/posts
は、指定されたユーザーによって公開されたすべての投稿を取得するために使用されます
ユーザーに対して 2 つのデータを取得する必要がある場合、zip 演算子を使用して 2 つのフローを結合し、2 つのデータを含む新しいフロー オブジェクトを返すことができます。違いは、一方のデータを取得した後、もう一方のリクエストがすぐにキャンセルされるため、リソースが節約され、応答が高速化されることです。
次の例では、2 つのデータを UserProfile クラスにマージし、zip 演算子を使用して、一方のデータのみを取得した場合にもう一方のリクエストを即座にキャンセルする機能を実装します。
data class UserProfile(val userInfo: UserInfo, val posts: List<Post>)
data class UserInfo(val id: Int, val name: String, val age: Int)
data class Post(val id: Int, val title: String)
fun getUserInfo(userId: Int): Flow<UserInfo> = flow {
// 模拟网络请求获取用户基本信息
delay(1000)
emit(UserInfo(userId, "user_$userId", 20))
}
fun getPostsByUserId(userId: Int): Flow<List<Post>> = flow {
// 模拟网络请求获取用户发表的所有帖子
delay(1000)
emit(listOf(Post(1, "Post 1"), Post(2, "Post 2")))
}
suspend fun main() {
getUserInfo(1)
.zip(getPostsByUserId(1)) {
userInfo, posts ->
UserProfile(userInfo, posts)
}
.catch {
e ->
println("Error: ${
e.message}")
}
.collect {
userProfile ->
println(userProfile.userInfo)
println(userProfile.posts)
}
}
この例では、引き続き getUserInfo 関数と getPostsByUserId 関数を使用して、指定されたユーザーの基本情報とすべての投稿のリストを取得します。各関数はオブジェクトのリストを含むフローを返します。
違いは、main 関数では、zip 演算子を使用して 2 つのフローを結合し、上流のフローから出力されたユーザーの基本情報と投稿リストを受け取り、それらを UserProfile オブジェクトにマージするラムダ式を指定していることです。リクエストの 1 つがデータを返すと、リソースを節約し、応答時間を短縮するために、もう 1 つのリクエストはすぐにキャンセルされます。
また、catch 関数を使用して考えられる例外を処理し、collect 関数を使用して UserProfile オブジェクトを出力します。