Kotlin Flow Responsive Programming, Getting Started with the Basics

This article is simultaneously published on my WeChat official account. You can follow by scanning the QR code at the bottom of the article or searching for Guo Lin on WeChat. Articles are updated every working day.

Kotlin has become very popular after many years of its introduction. It is believed that at least 80% of Android projects have been developed using Kotlin, or some functions have been developed using Kotlin.

Regarding the knowledge of Kotlin, I actually don’t share many articles, and the main content is concentrated in the book "The First Line of Code 3rd Edition". After reading this book, I believe you will be able to get started with the Kotlin language very well.

In fact, because the book "The First Line of Code 3rd Edition" only has the Kotlin version, the sales volume has been greatly affected, far less than the sales volume of the 2nd edition. The publishing house has communicated with me several times, hoping that I can publish another version for the Java language, because many readers, especially those in colleges and universities, still want to read books in the Java language, but they are all rejected by me.

The reason why I would say no is because Kotlin is already very important for Android developers. If you really want to be a good Android developer (this standard will be reduced to a qualified Android developer in a few years), then Kotlin must be learned.

Because all aspects of new knowledge involved in the modern Android development technology stack have almost been fully Kotlinized. If you still stick to Java, it means that future mainstream Android technology stacks such as coroutines and Compose will have nothing to do with you.

And now with the increasing popularity of Kotlin, I finally plan to write some advanced technical content based on the Kotlin language. The current plan is to write about both Flow and Compose, starting with Flow. So our Kotlin Flow series has officially started.

I plan to start with the basic introductory knowledge of Flow through 3 articles, and gradually teach you the common usage of Flow, applicable scenarios, and pitfalls and precautions that are easily overlooked. I hope that after studying this series of articles, you can use Flow more proficiently.

Another thing to note is that Flow is based on two technologies, Kotlin and coroutines. This article will not introduce these two technologies, so if you haven’t gotten started with Kotlin and coroutines, it is recommended to read "The First Line of Code 3rd Edition" to learn the basics.

That's all for the preface, so let's start.


Flow and Reactive Programming

Let’s talk about reactive programming first.

Since about four or five years ago, responsive programming has gradually entered the field of mobile development and has become more and more popular. The more representative one should be the RxJava framework that everyone in the Android field knows and everyone knows.

In fact, I am not very familiar with RxJava. I also learned various tutorials and articles on the Internet, but because I haven't been able to use it in my work, I still don't remember too many knowledge points.

But RxJava has left me with the impression that it is difficult to get started. The thinking of this responsive programming is different from the thinking of sequential execution of programs that are relatively simple and intuitive in the traditional sense.

So since this kind of programming thinking is so difficult to get started, why should we learn and use it?

In order to prove how good reactive programming is, there are countless tutorials and articles on the Internet that have tried their best to explain it. So here I will no longer find another way to beat my head and create an original one. I will directly quote Google's official explanation example. Official explanation video link: https://youtu.be/fSB6_KE95bU

For example, there is a calf living at the foot of a mountain, and there is a lake on the mountain. The calf needs to run a long way every day to fetch water from the lake with a bucket.

insert image description here

It doesn’t matter if you have to run a long distance every day. The key is that the lake will dry up from time to time. Sometimes when the calf arrives at the lake and finds that the lake has dried up, the trip is completely in vain.

insert image description here

After a long time, anyone with a discerning eye can find that this way of fetching water is too stupid. Why don't you spend more time building the infrastructure and erecting a water pipe from the lake to the foot of the mountain, so that the calf no longer has to run a long way to fetch water, and just turn on the tap every time he wants to drink water. . And judging whether the lake is dry can also be judged by turning on the tap to see if there is water.

insert image description here

And after erecting a pipeline, you can easily connect other pipelines in the future. For the final water end, this process can even be insensible, because he only needs to be responsible for opening and closing the tap.

insert image description here

In the above example, carrying a bucket to the lake to fetch water can be compared to our usual programming method, calling the corresponding function when we need something. And by erecting water pipes to drain water and receiving water at the faucet, it can be compared to the most popular responsive programming at the moment.

Wow, seeing such an image contrast and such a huge contrast, do you think the concept of responsive programming is awesome, and instantly feel that your previous programming method is so low?

In fact, when I saw this analogy for the first time, I also felt that such a powerful programming method had not been invented long ago. But after thinking about it later, I found that the example given by Google actually couldn't stand the scrutiny.

In today's life, of course no one wants to do the hard and tiring work of carrying a bucket to fetch water. How easy it is to turn the faucet. But in the programming world, we usually call a function is not such a bitter and tiring word. Instead, calling a function is as simple as calling it to get its return value. But the seemingly easy faucet, you want to achieve similar functions in the program (so-called responsive programming), but it is not simple, the switch of this faucet is not so easy to control.

Therefore, after trying responsive programming, many programmers will feel that this is nothing, and good and simple code must be written so complicated.

That's right, I also think that the thinking of responsive programming is not friendly enough for beginners, it can complicate the original simple code, but it can indeed solve some problems that are not easy to solve.

Take the example of fetching water just now, it is very simple to call a function to fetch water, but what if the process of fetching water is very time-consuming? Calling in the main thread may cause the program to freeze. So at this time, you need to consider opening sub-threads to fetch water, and then deal with some things such as thread callback results.

But with reactive programming, all you need to do is turn on the tap.

In short, my personal feeling is that as the project becomes more and more complex, you can more and more feel the advantages of reactive programming. And if the project is relatively simple, many times using responsive programming is to make trouble for yourself.

Well, the above is some of my analysis of responsive programming. So in the Android field, the most influential responsive programming framework before is RxJava. But you also found out, it's Rx Java (although it's also available on Kotlin). How can Kotlin bear this? Therefore, the Kotlin team has developed a set of responsive programming framework specially used on Kotlin, which is the protagonist of our series: Flow.


Basic usage of Flow

In this article, I am going to use the simplest example to let you quickly get started with the basic usage of Flow. Because it is too simple, it is even wrong in some details. But it doesn't matter, I will introduce the details in a later article. At present, our goal is to be able to run.

Create a new FlowTest project in Android Studio, and let's get started.

So what is an example? It's very simple, it is to implement the effect of a timer in Android, and update the time every second. But it must be implemented using Flow technology.

The first step is to add dependent libraries. If you want to use Flow in an Android project, the following dependent libraries need to be added to the project:

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"
}

The first two are coroutine libraries, because Flow is built on the basis of Kotlin coroutines, so coroutine dependencies are essential. The third item is used to provide the scope of the coroutine, which is also essential.

The last two items are the extended library of ktx. These are not necessary, but they can help us simplify a lot of code writing, so it is also recommended to add them.

Next, start to define the layout. The content in the layout file activity_main.xml is also very simple. A Button is used to start timing, and a TextView is used to display the time:

<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>

After writing these, we have basically done the preparation work, then we will use Flow technology to realize the timer function.

Recalling the analogy from earlier, reactive programming is like using a faucet to catch water. Then there are three most important parts in the whole process: water source, water pipe and faucet.

Among them, the water source is our data source, and this part needs to be handled by ourselves.

The faucet is the final receiving end, which may be displayed to the user, and this part also needs to be handled by ourselves.

The water pipe is the infrastructure part for implementing responsive programming. This part is packaged and provided to us by Flow, and we don't need to implement it ourselves.

So it's clear now that what we need to write is the water source and the faucet.

Start writing from the source of water, define a MainViewModel class, and inherit from ViewModel, the code is as follows:

class MainViewModel : ViewModel() {
    
    

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

}

Here, a timeFlow object is constructed using the flow construction function.

Inside the function body of the flow construction function, we wrote a while loop, each loop will add 1 to the time variable, and each loop will call the delay function to delay execution by 1 second.

The delay function here is a suspending function in a coroutine, which can only be called in the coroutine scope or other suspending functions. Therefore, it can be seen that the flow construction function will also provide a context for suspending the function to the inside of the function body.

The remaining emit function can be understood as a data transmitter, which will send the incoming parameters to the water pipe.

There are only a few lines of code in total, is it very simple? This way we get the water part done.

Some friends may say that this timeFlow variable is defined as a global variable, and it will be executed at the beginning. Could it be that we have not planned to start receiving water, and the water source here is continuously sending water?

Not in this scenario. Because the Flow constructed by using the flow construction function belongs to Code Flow, also called cold flow. The so-called cold flow means that Flow will not work without any receiving end. Only when there is a receiving end (the tap is open), the code in the Flow function body will automatically start executing.

Ok, then we start to implement the faucet part, the code is as follows:

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()
                }
            }
        }
    }
}

The most important part of this code is that we call the collect function of timeFlow defined in MainViewModel. Calling the collect function is equivalent to connecting the faucet to the water pipe and turning it on, so that any data sent from the water source can be received by the faucet, and then the received data can be updated to the TextView.

Although this code looks simple, there are many invisible pits. Since Flow's collect function is a suspending function, it must be called in the coroutine scope or other suspending functions. Here we use lifecycleScope to start a coroutine scope to achieve.

In addition, as long as the collect function is called, it is equivalent to entering an infinite loop, and its next line of code will never be executed. Therefore, if there are multiple Flows in your code that need to be collected, the following way of writing is completely wrong:

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

The data in this way of writing flow2 cannot be updated, because it cannot be executed at all.

The correct way to write it should be to use the launch function to start the sub-coroutine to collect, so that different sub-coroutines will not affect each other:

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

In fact, there are still some pitfalls in the above code, but as I said earlier, our goal in this article is to be able to run, and the rest of the pitfalls will be discussed in detail in later articles.

Now you can run the program, click the Button on the interface, the effect is shown in the following figure:

insert image description here

As you can see, the timer function has been successfully implemented.


Uneven velocity problem

I feel that these are the most basic usages of Flow, but in the end I think there is another knowledge point that is worth mentioning.

Since Flow is a reactive programming model based on the observer pattern, when the water source sends a data, the faucet will receive a data. However, the data processing speed of the faucet is not necessarily the same as the data sending speed of the water source. If the processing speed of the faucet is too slow, the pipeline may be blocked.

Responsive programming frameworks may encounter this kind of problem, and there is a special back pressure strategy in RxJava to deal with this kind of problem. In fact, there are also in Flow, but we will not discuss such high-end techniques today. Today, we can use a very simple solution to solve the problem of uneven flow rate.

First, let's reproduce the phenomenon of this problem. Modify the code in MainActivity as follows:

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)
                }
            }
        }
    }
}

Here, a delay logic is added to the collect function processing of timeFlow to delay it for 3 seconds.

You know, at the water source, we send one piece of data per second, but it takes 3 seconds to process one piece of data at the faucet. So what will the result be? Let's take a look at the effect:

insert image description here

As you can see, the timer is now only updated every 3 seconds. In this way, our timer is completely inaccurate.

So how to solve this problem?

The essence of this problem is that the data processing speed of the faucet is too slow, resulting in a large backlog of data in the pipeline, and the backlog of data will continue to be passed to the faucet one by one, even if the data has expired.

The client should always display the latest data on the interface. If it is expired data, it is of no value to display it to the user.

Therefore, as long as there is updated data, if the last data has not been processed, then we will cancel it directly and process the latest data immediately.

To achieve such a function in Flow, you only need to use the collectLatest function, as shown below:

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)
                }
            }
        }
    }
}

As you can see, here we slightly changed the implementation at the faucet, instead of calling the collect function to collect data, we changed it to the collectLatest function.

Then it can be seen from the name that the collectLatest function only receives and processes the latest data. If new data arrives and the previous data has not been processed, all remaining processing logic of the previous data will be cancelled.

Re-run the program, let's see the effect again:

insert image description here

No problem, now the timer is working normally again.

Well, so far, the first article of the Kotlin Flow series is almost over. I think mastering these contents is enough to be regarded as an introduction to Flow. For more knowledge about Flow, please refer to the next article Kotlin Flow Responsive Programming, Advanced Operator Functions .


If you want to learn Kotlin and the latest Android knowledge, you can refer to my new book "The First Line of Code 3rd Edition" , click here to view details .

Guess you like

Origin blog.csdn.net/sinyu890807/article/details/127466982