Kotlin Flow レスポンシブ プログラミング、基礎から始める

この記事は、WeChat 公式アカウントで同時に公開されています. 記事の下部にある QR コードをスキャンするか、WeChat で Guo Lin を検索してフォローできます. 記事は毎日更新されます.

Kotlin は、何年にもわたって導入された後、非常に人気が高まっています。Android プロジェクトの少なくとも 80% が Kotlin を使用して開発されているか、一部の機能が Kotlin を使用して開発されていると考えられています。

Kotlinの知識に関しては、実はあまり記事をシェアしておらず、「The First Line of Code 3rd Edition」という本に主な内容が集中しています。この本を読めば、Kotlin 言語を上手に使い始めることができると思います。

実際、「The First Line of Code 3rd Edition」という本は Kotlin 版しかないため、2 版の販売量をはるかに下回る販売量に大きく影響しています。出版社は、私が Java 言語の別のバージョンを発行できることを期待して、何度か私に連絡してきました。 .

私がノーと言う理由は、Kotlin がすでに Android 開発者にとって非常に重要だからです。本当に優れた Android デベロッパーになりたいのであれば (この基準は数年以内に資格のある Android デベロッパーに格下げされます)、Kotlin を学ぶ必要があります。

最新の Android 開発テクノロジ スタックに関連する新しい知識のすべての側面は、ほぼ完全に Kotlin 化されているためです。まだ Java に固執している場合は、コルーチンや Compose などの将来の主流の Android テクノロジ スタックがあなたとは何の関係もないことを意味します。

Kotlin の人気が高まっている現在、Kotlin 言語に基づいた高度な技術コンテンツを作成する予定です。現在の計画は、Flow から始めて、Flow と Compose の両方について書くことです。これで、Kotlin Flow シリーズが正式に開始されました。

3つの記事でFlowの基礎知識から始めて、徐々にFlowの一般的な使い方や適用シーン、見落としがちな落とし穴や注意点などを教えていく予定です。この一連の記事を読んだ後、Flow をより使いこなせるようになることを願っています。

もう 1 つ注目すべき点は、Flow が Kotlin とコルーチンという 2 つのテクノロジに基づいていることです。この記事ではこれら 2 つのテクノロジについては紹介しません。Kotlin とコルーチンを初めて使用する場合は、 「The First Line of Code 3rd Edition」を読んで基本を学ぶことをお勧めします。

序文は以上ですので、始めましょう。


フローとリアクティブ プログラミング

最初にリアクティブプログラミングについて話しましょう。

約 4、5 年前から、レスポンシブ プログラミングはモバイル開発の分野に徐々に参入し、ますます人気が高まっています。より代表的なものは、Android 分野の誰もが知っている、誰もが知っている RxJava フレームワークです。

実は私はRxJavaにあまり詳しくなく、インターネットでさまざまなチュートリアルや記事も学びましたが、仕事で使用できていないため、まだあまり多くの知識を覚えていません。

しかし、RxJava は、始めるのが難しいという印象を私に残しました。このレスポンシブ プログラミングの考え方は、従来の意味で比較的単純で直感的なプログラムの逐次実行の考え方とは異なります。

では、この種のプログラミングの考え方は始めるのが非常に難しいのに、なぜそれを学び、使用する必要があるのでしょうか?

リアクティブ プログラミングがいかに優れているかを証明するために、インターネット上には無数のチュートリアルや記事があり、それを説明するために最善を尽くしています。ですから、ここで頭を悩ませて独自の方法を作成する別の方法を見つけることはもうありません.Googleの公式説明の例を直接引用します. 公式説明動画リンク:https ://youtu.be/fSB6_KE95bU

たとえば、山のふもとに子牛が住んでいて、山の上に湖があり、子牛は湖からバケツで水を汲むために毎日長い道のりを走る必要があります。

ここに画像の説明を挿入

毎日長い距離を走らなければならないかどうかは関係ありません. 重要なのは、湖が時々干上がることです. 子牛が湖に到着し、湖が干上がっていることに気付くと、旅行は.完全に無駄です。

ここに画像の説明を挿入

長い間、目の肥えた人なら誰でも、この水汲みの方法が愚かすぎることに気付くでしょう。子牛が水汲みに長い道のりを走る必要がなくなり、毎回蛇口をひねるだけで済むように、インフラストラクチャの構築と湖から山のふもとまでの水道管の設置にもっと時間を費やしてみませんか?彼は水を飲みたい. . また、湖が乾いているかどうかの判断は、蛇口をひねって水があるかどうかで判断することもできます。

ここに画像の説明を挿入

また、パイプラインを構築した後は、将来的に他のパイプラインを簡単に接続できます。最後の水やりの場合、彼は蛇口の開閉だけを担当する必要があるため、このプロセスは無意味ですらあります。

ここに画像の説明を挿入

上記の例では、バケツを湖に運んで水を汲むことは、何かが必要なときに対応する関数を呼び出す通常のプログラミング方法と比較できます。また、水道管を立てて水を排出し、蛇口で水を受けることで、現在最も人気のあるレスポンシブ プログラミングと比較することができます。

うわー、このようなイメージのコントラストと大きなコントラストを見て、レスポンシブ プログラミングのコンセプトは素晴らしいと思いますか?すぐに、以前のプログラミング方法が非常に低いと感じますか?

実際、私はこの例えを初めて見たとき、これほど強力なプログラミング手法が発明されたのはずっと前のことだとも感じました。しかし、後で考えてみると、Google が示した例は実際には精査に耐えられないことがわかりました。

今の生活では、バケツを持って水を汲むという大変で疲れる仕事をしたくないのは当然のことです。しかし、プログラミングの世界では、通常、関数を呼び出すことはそれほど辛く疲れる言葉ではありません。代わりに、関数の呼び出しは、関数を呼び出して戻り値を取得するのと同じくらい簡単です。しかし、一見簡単な蛇口、プログラムで同様の機能を実現したい(いわゆるレスポンシブプログラミング)が、単純ではなく、この蛇口のスイッチは簡単に制御できません。

そのため、レスポンシブ プログラミングを試した後、多くのプログラマーは、これは何でもなく、優れた単純なコードは非常に複雑に記述しなければならないと感じるでしょう。

そうです、レスポンシブ プログラミングの考え方は初心者にとって十分に友好的ではなく、元の単純なコードを複雑にする可能性があると思いますが、実際には簡単に解決できないいくつかの問題を解決できます。

先ほどの水汲みの例を見てみましょう。水を汲み上げる関数を呼び出すのは非常に簡単ですが、水を汲み上げるプロセスに非常に時間がかかる場合はどうでしょうか? メイン スレッドで呼び出すと、プログラムがフリーズする場合があります。したがって、現時点では、サブスレッドを開いて水をフェッチすることを検討し、スレッド コールバックの結果などを処理する必要があります。

しかし、リアクティブ プログラミングでは、タップをオンにするだけで済みます。

要するに、プロジェクトが複雑になるほど、リアクティブ プログラミングの利点を感じることができるようになるというのが私の個人的な感覚です。また、プロジェクトが比較的単純な場合、レスポンシブ プログラミングを使用することは多くの場合、自分自身に問題を引き起こします。

さて、上記はレスポンシブ プログラミングに関する私の分析の一部です。そのため、Android の分野で最も影響力のあるレスポンシブ プログラミング フレームワークは RxJava です。しかし、それが Rx Javaであることもわかりました(ただし、Kotlin でも利用できます)。Kotlinはこれにどのように耐えることができますか? そのため、Kotlin チームは、シリーズの主役である Kotlin で特別に使用されるレスポンシブ プログラミング フレームワークのセットを開発しました: Flow.


Flowの基本的な使い方

この記事では、最も簡単な例を使用して、Flow の基本的な使用法をすぐに開始できるようにします。あまりにも単純すぎるため、いくつかの詳細で間違っています。詳細は後日記事で紹介しますが、今のところは走れるようになるのが目標です。

Android Studio で新しい FlowTest プロジェクトを作成し、始めましょう。

それで、例は何ですか?とても簡単です。Android にタイマーの効果を実装し、1 秒ごとに時間を更新するだけです。ただし、Flow テクノロジーを使用して実装する必要があります。

最初のステップは、依存ライブラリを追加することです. Android プロジェクトで Flow を使用する場合は、次の依存ライブラリをプロジェクトに追加する必要があります:

dependencies {
    
    
    ...
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
    implementation "androidx.activity:activity-ktx:1.6.0"
    implementation "androidx.fragment:fragment-ktx:1.5.3"
}

最初の 2 つはコルーチン ライブラリです。Flow は Kotlin コルーチンに基づいて構築されているため、コルーチンの依存関係が不可欠です。3 番目の項目は、コルーチンのスコープを提供するために使用されます。これも不可欠です。

最後の 2 つは ktx の拡張ライブラリです. これらは必須ではありませんが, 多くのコード記述を簡素化するのに役立つので, これらも追加することをお勧めします.

次に、レイアウトの定義を開始します。レイアウト ファイル activity_main.xml の内容も非常に単純です。ボタンを使用してタイミングを開始し、TextView を使用して時間を表示します。

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="20sp"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="Start"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/text_view" />

</androidx.constraintlayout.widget.ConstraintLayout>

これらを書き終えたら、基本的な準備作業は終わったので、フロー技術を使ってタイマー機能を実現していきます。

以前のアナロジーを思い出すと、リアクティブ プログラミングは蛇口を使って水を汲むようなものです。次に、プロセス全体で最も重要な 3 つの部分があります。水源、水道管、蛇口です。

その中で、水源は私たちのデータソースであり、この部分は私たち自身で処理する必要があります。

蛇口はユーザーに表示される最終的な受信側であり、この部分も自分で処理する必要があります。

水道管は、レスポンシブ プログラミングを実装するためのインフラストラクチャ パーツであり、この部分は Flow によってパッケージ化されて提供されているため、自分で実装する必要はありません。

これで、記述する必要があるのは水源と蛇口であることは明らかです。

水源から書き始め、MainViewModel クラスを定義し、ViewModel から継承すると、コードは次のようになります。

class MainViewModel : ViewModel() {
    
    

    val timeFlow = flow {
    
    
        var time = 0
        while (true) {
    
    
            emit(time)
            delay(1000)
            time++
        }
    }

}

ここでは、フロー構築関数を使用して timeFlow オブジェクトを構築します。

フロー構築関数の関数本体内に while ループを記述し、ループごとに時間変数に 1 を加算し、ループごとに遅延関数を呼び出して実行を 1 秒遅らせます。

ここでの遅延関数は、コルーチンの中断関数であり、コルーチン スコープまたは他の中断関数でのみ呼び出すことができます。したがって、フロー構築関数は、関数本体の内部に関数を中断するためのコンテキストも提供することがわかります。

残りのエミット関数は、データ送信機として理解でき、受信パラメータを水道管に送信します。

全体で数行のコードしかありませんが、非常に単純ですか? このようにして、水の部分を完成させます。

この timeFlow 変数はグローバル変数として定義されており、最初に実行されると言う友人もいるかもしれませんが、もしかしたら私たちは水を受け取る予定がなく、ここの水源は継続的に水を送り続けているのでしょうか?

このシナリオではありません。フロー構築機能を用いて構築されたフローは、コールドフローとも呼ばれるコードフローに属するためです。いわゆるコールドフローとは、フローが受信側なしでは機能しないことを意味します。受信側(タップが開いている)がある場合のみ、フロー関数本体のコードが自動的に実行を開始します。

それでは、蛇口部分の実装を開始します。コードは次のとおりです。

class MainActivity : AppCompatActivity() {
    
    

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
    
    
            lifecycleScope.launch {
    
    
                mainViewModel.timeFlow.collect {
    
     time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

このコードの最も重要な部分は、MainViewModel で定義された timeFlow の collect 関数を呼び出すことです。collect 関数を呼び出すことは、蛇口を水道管に接続してオンにすることと同じであり、水源から送信されたデータを蛇口で受信し、受信したデータを TextView に更新することができます。

このコードは単純に見えますが、目に見えない落とし穴がたくさんあります。Flow の collect 関数はサスペンド関数であるため、コルーチン スコープまたは他のサスペンド関数で呼び出す必要があります。ここでは、lifecycleScope を使用して、達成するコルーチン スコープを開始します。

さらに、collect 関数が呼び出されている限り、それは無限ループに入るのと同じであり、その次のコード行は決して実行されません。したがって、収集する必要があるコード内に複数のフローがある場合、次の書き方は完全に間違っています。

lifecycleScope.launch {
    
    
    mainViewModel.flow1.collect {
    
    
        ...
    }
    mainViewModel.flow2.collect {
    
    
        ...
    }
}

このように flow2 を書き込んだデータは、まったく実行できないため、更新できません。

これを記述する正しい方法は、 launch 関数を使用してサブコルーチンを開始して収集することです。これにより、異なるサブコルーチンが互いに影響を与えません。

lifecycleScope.launch {
    
    
    launch {
    
    
        mainViewModel.flow1.collect {
    
    
            ...
        }
    }
    launch {
    
    
        mainViewModel.flow2.collect {
    
    
            ...
        }
    }
}

実際、上記のコードにはまだいくつかの落とし穴がありますが、前述したように、この記事の目標は実行できるようにすることであり、残りの落とし穴については後の記事で詳しく説明します。

これで、プログラムを実行できます。インターフェイスのボタンをクリックすると、次の図にその効果が示されます。

ここに画像の説明を挿入

ご覧のとおり、タイマー機能が正常に実装されています。


不均一な速度の問題

以上がFlowの最も基本的な使い方だと思いますが、最後にもう一つ特筆すべき知識があると思います。

Flow はオブザーバー パターンに基づくリアクティブ プログラミング モデルであるため、水源がデータを送信すると、蛇口がデータを受信します。ただし、蛇口のデータ処理速度は、水源のデータ送信速度と必ずしも同じではなく、蛇口の処理速度が遅すぎると、パイプラインがブロックされる可能性があります。

レスポンシブ プログラミング フレームワークではこの種の問題が発生する可能性があり、RxJava にはこの種の問題に対処するための特別なバック プレッシャ戦略があります。実際、フローにもありますが、今日はそのような高度な手法については説明しませんが、今日、非常に単純なソリューションを使用して、不均一な流量の問題を解決できます。

まずは、この問題の現象を再現してみましょう。MainActivity のコードを次のように変更します。

class MainActivity : AppCompatActivity() {
    
    

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
    
    
            lifecycleScope.launch {
    
    
                mainViewModel.timeFlow.collect {
    
     time ->
                    textView.text = time.toString()
                    delay(3000)
                }
            }
        }
    }
}

ここでは、timeFlow の collect 関数の処理に遅延ロジックを追加して、3 秒遅延させます。

水源では毎秒 1 つのデータを送信しますが、蛇口では 1 つのデータを処理するのに 3 秒かかります。では、結果はどうなるでしょうか?効果を見てみましょう:

ここに画像の説明を挿入

ご覧のとおり、タイマーは 3 秒ごとにのみ更新されます。このように、タイマーは完全に不正確です。

では、この問題を解決するにはどうすればよいでしょうか。

この問題の本質は、faucet のデータ処理速度が遅すぎて、パイプラインに大量のデータのバックログが発生し、データのバックログが 1 つずつ faucet に渡され続けることです。期限切れです。

クライアントは常に最新のデータをインターフェイスに表示する必要がありますが、有効期限が切れたデータをユーザーに表示しても意味がありません。

そのため、更新されたデータがある限り、最後のデータが処理されていない場合は、直接キャンセルし、最新のデータをすぐに処理します。

Flow でこのような機能を実現するには、以下に示すように、collectLatest 関数を使用するだけです。

class MainActivity : AppCompatActivity() {
    
    

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
    
    
            lifecycleScope.launch {
    
    
                mainViewModel.timeFlow.collectLatest {
    
     time ->
                    textView.text = time.toString()
                    delay(3000)
                }
            }
        }
    }
}

ご覧のとおり、ここでは faucet での実装を少し変更しました。データを収集するために collect 関数を呼び出す代わりに、collectLatest 関数に変更しました。

名前からわかるように、collectLatest 関数は最新のデータのみを受信して​​処理します。新しいデータが到着し、前のデータが処理されていない場合、前のデータの残りの処理ロジックはすべてキャンセルされます。

プログラムを再実行して、効果をもう一度見てみましょう。

ここに画像の説明を挿入

問題ありません。タイマーは再び正常に動作しています。

さて、ここまでで、Kotlin Flow シリーズの最初の記事はもうすぐ終わります。これらの内容をマスターすれば、Flow の入門として十分だと思います.Flow の詳細については、次の記事Kotlin Flow Responsive Programming, Advanced Operator Functionsを参照してください。


Kotlin と最新の Android の知識を学びたい場合は、私の新しい本「The First Line of Code 3rd Edition」を参照してください。詳細を表示するには、ここをクリックしてください

おすすめ

転載: blog.csdn.net/sinyu890807/article/details/127466982