preface

In previous articles, you’ve covered the basics of Flow. In this article, we’ll start with a few exercises to reinforce what we’ve already covered.

So here’s what you need to read:

  • Kotlin
  • Jetpack

1. Preparation

1.1 Take a look at the overall structure of the page

As is shown in

Here prepared five small cases to carry on the corresponding explanation!

1.2 Importing related packages

    implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.4.2'
    implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.4.2'
    implementation "Androidx. Activity: activity - KTX: 1.1.0." "
    implementation "Androidx. Fragments: fragments - KTX: 1.2.5." "
    def room_version = "2.3.0"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    def nav_version = "2.3.2"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
    implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0." "
    implementation "Androidx. Swiperefreshlayout: swiperefreshlayout: 1.1.0."
    implementation 'androidx. Legacy: legacy support - v4:1.0.0'
    implementation "Com. Squareup. Retrofit2: retrofit: 2.9.0"
    implementation "Com. Squareup. Retrofit2: converter - gson: 2.9.0"
Copy the code

1.3 open ViewBinding

    buildFeatures {
        viewBinding = true
    }
Copy the code

1.4 Configuring Network Permissions and allowing HTTP is indispensable


    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FlowPractice">
        <activity android:name=".activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
Copy the code

1.5 network_security_config. XML


      
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
Copy the code

Now that the prep work is done, let’s get started!

2. Flow and file download

As is shown in

As can be seen here, Flow downloads data from the server in the background process and sends it to the corresponding channel through emit. The main thread receives the corresponding data through COLLECT.

2.1 InputStream extension function

inline fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, progress: (Long) - >Unit): Long {
    var bytesCopied: Long = 0
    val buffer = ByteArray(bufferSize)
    var bytes = read(buffer)
    while (bytes >= 0) {
        out.write(buffer, 0, bytes)
        bytesCopied += bytes
        bytes = read(buffer)
        progress(bytesCopied) // Call the inline function at the end
    }
    return bytesCopied
}

Copy the code

Here we see an additional extension of the copyTo function to the global system class InputStream and the corresponding logic.

Now let’s analyze the copyTo function

  • Out: OutputStream Needless to say, download the saved object stream

  • BufferSize: Int = DEFAULT_BUFFER_SIZE. The default value is DEFAULT_BUFFER_SIZE

  • Progress: (Long)-> Unit inline function, argument Long, return Null

    • That is, the business logic for the last parameter of this method needs to be implemented on an external call!

So, take a look at the logic of file downloads!

2.2 File Download DownloadManager

object DownloadManager {
    /** * File download *@urlDownload path *@fileSave the file locally */
    fun download(url: String, file: File): Flow<DownloadStatus> {

        return flow {
            val request = Request.Builder().url(url).get().build()
            val response = OkHttpClient.Builder().build().newCall(request).execute()
            if(response.isSuccessful) { response.body()!! .let { body ->val total = body.contentLength()
                    // File read and write
                    file.outputStream().use { output ->
                        val input = body.byteStream()
                        var emittedProgress = 0L
                        // Use the corresponding extension function, because the last parameter of this function is an inline function, so the corresponding business logic needs to be implemented later
                        input.copyTo(output) { bytesCopied ->
                        	// Get the percentage of download progress
                            val progress = bytesCopied * 100 / total
                            // Notify the UI thread every time the download progress is greater than 5
                            if (progress - emittedProgress > 5) {
                                delay(100)
                                // Use the emit corresponding to Flow to send the corresponding download progress notification
                                emit(DownloadStatus.Progress(progress.toInt()))
                                // Record the current download progress
                                emittedProgress = progress
                            }
                        }
                    }
                }
                // Send notification of download completion
                emit(DownloadStatus.Done(file))
            } else {
                throw IOException(response.toString())
            }
        }.catch {
        	// Download failed, delete the file, and send failure notification
            file.delete()
            emit(DownloadStatus.Error(it))
        }.flowOn(Dispatchers.IO) // Since downloading files is an asynchronous IO operation, the context is changed here}}Copy the code

The instructions are all in the notes, so there’s no more explanation. But here we use the sealed DownloadStatus class to see what it looks like:

2.3 DownloadStatus Seal class

sealed class DownloadStatus {
    object None : DownloadStatus() / / null state
    data class Progress(val value: Int) : DownloadStatus() // Download progress
    data class Error(val throwable: Throwable) : DownloadStatus() / / error
    data class Done(val file: File) : DownloadStatus() / / finish
}
Copy the code

File download is ready, then see how to call!

2.3 Downloading files

class DownloadFragment : Fragment() {

    val URL = "http://10.0.0.130:8080/kotlinstudyserver/pic.JPG"

	// Initialize the ViewBinding block to fix the code
    private val mBinding: FragmentDownloadBinding by lazy {
        FragmentDownloadBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
    	// The layout uses the root corresponding to the ViewBinding
        return mBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState) lifecycleScope.launchWhenCreated { context? .apply {val file = File(getExternalFilesDir(null)? .path,"pic.JPG")
                DownloadManager.download(URL, file).collect { status ->
                    when (status) {
                        is DownloadStatus.Progress -> {
                            mBinding.apply {
                                progressBar.progress = status.value
                                tvProgress.text = "${status.value}%"}}is DownloadStatus.Error -> {
                            Toast.makeText(context, "Download error", Toast.LENGTH_SHORT).show()
                        }
                        is DownloadStatus.Done -> {
                            mBinding.apply {
                                progressBar.progress = 100
                                tvProgress.text = "100%"
                            }
                            Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show()
                        }
                        else -> {
                            Log.d("ning"."Download failed.")}}}}}}}Copy the code

Code analysis:

  • lifecycleScope.launchWhenCreatedThe saidStarts and runs the given block when Lifecycle controlling this LifecycleCoroutineScope is at least in the State of life.state.created
  • because DownloadManager.download(URL, file)Method, is usedFlow-emitEmission values, therefore external needscollect Receives the corresponding value. The business logic inside is different processing in different states

Let’s see how it works

OK! Perfect run. Next!

3. Flow and Room applications

3.1 Take a look at the corresponding layout structure first

As is shown in

The following list is the list data in the corresponding database table, and the button above indicates that the content of the input box is added to the corresponding user table in the database.

3.2 Data table entity class User

@Entity
data class User(
    @PrimaryKey val uid: Int.@ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)
Copy the code

There’s nothing to say about that. Next.

3.3 the corresponding RoomDatabase

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(a): UserDao

    companion object {

        private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            returninstance ? : synchronized(this) {
                Room.databaseBuilder(context, AppDatabase::class.java, "flow_practice.db")
                    .build().also { instance = it }
            }
        }

    }
}
Copy the code

This was covered in detail in the Jetpack column and won’t be covered again. Don’t know how to use Room: Click me to view the Jetpack-room explanation

3.4 the corresponding UserDao

@Dao
interface UserDao {

	// The Insert DAO method that returns the ID of the inserted row will never return -1 because this policy will always Insert rows even if there is a conflict
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User)

    @Query("SELECT * FROM user")
    fun getAll(a): Flow<List<User>>

}
Copy the code

GetAll returns Flow > with Flow wrap!

That’s easy. Let’s go through it quickly.

3.5 Corresponding ViewModel

class UserViewModel(app: Application) : AndroidViewModel(app) {

    fun insert(uid: String, firstName: String, lastName: String) {
        viewModelScope.launch {
            AppDatabase.getInstance(getApplication())
                .userDao()
                .insert(User(uid.toInt(), firstName, lastName))
            Log.d("hqk"."insert user:$uid")}}fun getAll(a): Flow<List<User>> {
        return AppDatabase.getInstance(getApplication())
            .userDao()
            .getAll()
            .catch { e -> e.printStackTrace() }
            .flowOn(Dispatchers.IO) // Switch the context to IO asynchronous}}Copy the code

3.6 Corresponding UI Operations


class UserFragment : Fragment() {

    private val viewModel by viewModels<UserViewModel>()
	
	//viewBinding fixes the code
    private val mBinding: FragmentUserBinding by lazy {
        FragmentUserBinding.inflate(layoutInflater)
    }
		
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return mBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState) mBinding.apply { btnAddUser.setOnClickListener { viewModel.insert( etUserId.text.toString(), etFirstName.text.toString(), etLastName.text.toString() ) } } context? .let {val adapter = UserAdapter(it)
            mBinding.recyclerView.adapter = adapter
            lifecycleScope.launchWhenCreated {
            	Receive data sent by getAll Flow through collect
                viewModel.getAll().collect { value ->
                    adapter.setData(value)
                }
            }
        }
    }
}
Copy the code

Let’s see how it works

Perfect run. Next one.

Flow and Retrofit applications

Before we begin, let’s analyze the relationship between Flow and Retrofit:

As is shown in

  • We see that what the user enters in editText in real time is sent via Flow to Retrofit in the ViewModel, which then requests data from the server.
  • The data from the server is gradually transmitted to the LiveData in the ViewModel through another Flow
  • Finally, the LiveData of the ViewModel refreshes the UI in real time to display the corresponding article content

Server code

public class ArticleServlet extends HttpServlet {

    List<String> data = new ArrayList<>();

    @Override
    public void init(a) throws ServletException {
        data.add("Refactored versions of the Android APIs that are not bundled with the operating system.");
        data.add("Jetpack Compose is a modern toolkit for building native Android UI. Jetpack Compose simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs.");
        data.add("Includes APIs for testing your Android app, including Espresso, JUnit Runner, JUnit4 rules, and UI Automator.");
        data.add("Includes ConstraintLayout and related APIs for building constraint-based layouts.");
        data.add("Includes APIs to help you write declarative layouts and minimize the glue code necessary to bind your application logic  and layouts.");
        data.add("Provides APIs for building Android Automotive apps.");
        data.add("A library for building Android Auto apps. This library is currently in beta. You can design, develop, and test navigation, parking, and charging apps for Android Auto, but you can't distribute these apps through the Google Play Store yet. We will make announcements in the future when you  can distribute these apps through the Google Play Store.");
        data.add("Provides APIs to build apps for wearable devices running Wear OS by Google.");
        data.add("Material Components for Android (MDC-Android) help developers execute Material Design to build beautiful and functional  Android apps.");
        data.add("The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++.");
        data.add("The Android Gradle Plugin (AGP) is the supported build system for Android applications and includes support for compiling many different types of sources and linking them together into an application that you can run on a physical Android device or an emulator.");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String key = request.getParameter("key");
        if(key ! =null) {
            System.out.println(key);
        }else{
            key = "Includes";
        }
        System.out.println("doGet");
        PrintWriter out = response.getWriter();
        JsonArray jsonArray = new JsonArray();
        for (int i = 0; i < data.size(); i++) {
            String text = data.get(i);
            if (text.contains(key)) {
                JsonObject jsonObject = new JsonObject();
                jsonObject.addProperty("id", i);
                jsonObject.addProperty("text", text); jsonArray.add(jsonObject); } } out.write(jsonArray.toString()); System.out.println(jsonArray.toString()); out.close(); }}Copy the code

This method is similar to a text filter and automatically matches the corresponding text based on the client’s input.

Client code

4.1 the corresponding RetrofitClient

object RetrofitClient {

    private val instance: Retrofit by lazy {
        Retrofit.Builder()
            .client(OkHttpClient.Builder().build())
            .baseUrl("http://10.0.0.130:8080/kotlinstudyserver/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    val articleApi: ArticleApi by lazy {
        instance.create(ArticleApi::class.java)
    }
}
Copy the code

Here you see the ArticleApi used, so

4.2 the corresponding ArticleApi

interface ArticleApi {

    @GET("article")
    suspend fun searchArticles(
        @Query("key") key: String
    ): List<Article>

}
Copy the code

Here’s a suspend function that gets network data by key, and here’s the data class Article

Corresponding to the Article 4.3

data class Article(val id: Int.val text: String)
Copy the code

It’s so simple. There’s nothing to say. Let’s look at the core ViewModel

4.4 the corresponding ViewModel

class ArticleViewModel(app: Application) : AndroidViewModel(app) {
	
	// Define the corresponding LiveData data type
    val articles = MutableLiveData<List<Article>>()

	
    fun searchArticles(key: String) {
        viewModelScope.launch {
            flow {
            	// Here is the content of the article filtered by the corresponding key obtained from the server via Retrofit
                val list = RetrofitClient.articleApi.searchArticles(key)
                // Send the corresponding data
                emit(list)
            }.flowOn(Dispatchers.IO)
                .catch { e -> e.printStackTrace() }
                .collect {
                	// Update the corresponding LiveData
                    articles.setValue(it)
                }
        }
    }

}
Copy the code

As is shown in

The Flow of the ViewModel corresponds to that part of the diagram, and the rest is in the comments.

Now that everything is ready, take a look at how the interface works!

4.5 Final Call

class ArticleFragment : Fragment() {
    private val viewModel by viewModels<ArticleViewModel>()

    private val mBinding: FragmentArticleBinding by lazy {
        FragmentArticleBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return mBinding.root
    }

    // Get keyword analysis 1
    private fun TextView.textWatcherFlow(a): Flow<String> = callbackFlow {
        val textWatcher = object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence? , start:Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence? , start:Int, before: Int, count: Int) {}
            override fun afterTextChanged(s: Editable?). {
                offer(s.toString())
            }
        }
        addTextChangedListener(textWatcher)
        awaitClose { removeTextChangedListener(textWatcher) }
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState)
        lifecycleScope.launchWhenCreated {
        	2 / / analysis
            mBinding.etSearch.textWatcherFlow().collect {
                Log.d("ning"."collect keywords: $it") viewModel.searchArticles(it) } } context? .let {val adapter = ArticleAdapter(it)
            mBinding.recyclerView.adapter = adapter
            3 / / analysis
            viewModel.articles.observe(viewLifecycleOwner, { articles ->
                adapter.setData(articles)
            })
        }

    }
}
Copy the code

Code parsing

  • TextWatcherFlow () = textWatcherFlow() = textWatcherFlow() = Flow

    • whileEditTextEventually inherited asTextViewTherefore, the corresponding input box also has the function of using the corresponding extension function
  • Analysis 2: It can be known from analysis 1 that all TextViews on the current page have textWatcherFlow() method, so the content when the text changes can be monitored through collect, and the corresponding IT is the current changed value

  • Analysis 3: because viewmodel. articles is the LiveData in the corresponding viewModel, use. Observe to monitor changes in LiveData and refresh the corresponding adapter in real time

Let’s see how it works

Logs are displayed on the background

OK! Perfect run! Next!

5, cold flow or hot flow

  • Cold Flow. What is cold Flow? In short, if the Flow has a subscriber Collector, the emitted value will actually exist in memory, much like the lazy loading concept.
  • As opposed to heat flow, StateFlow and SharedFlow are heat flows that are active in memory before garbage collection!

So!

5.1 StateFlow

StateFlow is a state-container-like stream of observable data that issues current and new state updates to its collector. The hacker reads the current state value through its value property.

Concept said, start actual combat try hand:

5.1.1 corresponding ViewModel

class NumberViewModel : ViewModel() {

    val number = MutableStateFlow(0)

    fun increment(a) {
        number.value++
    }

    fun decrement(a) {
        number.value--
    }
}
Copy the code

This looks like LiveData, forget about it, then see the corresponding use!

5.1.2 Specific Use

class NumberFragment : Fragment() {
    private val viewModel by viewModels<NumberViewModel>()

    private val mBinding: FragmentNumberBinding by lazy {
        FragmentNumberBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return mBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState)
        mBinding.apply {
            btnPlus.setOnClickListener {
                viewModel.increment()
            }

            btnMinus.setOnClickListener {
                viewModel.decrement()
            }
        }

        lifecycleScope.launchWhenCreated {
            viewModel.number.collect { value ->
                mBinding.tvNumber.text = "$value"}}}}Copy the code

The way to use it is the same as LiveData, and very simple

Let’s see how it works

The overall feel is similar to LiveData! How does SharedFlow work?

5.2 SharedFlow

As is shown in

The page has three fragments, each containing only one TextView. There are two buttons below: start and stop.

Want to achieve three fragments, stopwatch synchronous walking effect!

5.2.1 Viewing the Main Fragment First

class SharedFlowFragment : Fragment() {

    private val viewModel by viewModels<SharedFlowViewModel>()

    private val mBinding: FragmentSharedFlowBinding by lazy {
        FragmentSharedFlowBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return mBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState)
        mBinding.apply {
            btnStart.setOnClickListener {
                viewModel.startRefresh()
            }

            btnStop.setOnClickListener {
                viewModel.stopRefresh()
            }
        }
    }

}
Copy the code

There’s nothing to say here, just two buttons that call different ViewModel methods!

5.2.2 Look at the SharedFlowViewModel

class SharedFlowViewModel : ViewModel() {

    private lateinit var job: Job

    fun startRefresh(a) {
        job = viewModelScope.launch(Dispatchers.IO) {
            while (true) {
                LocalEventBus.postEvent(Event((System.currentTimeMillis())))
            }
        }
    }

    fun stopRefresh(a) {
        job.cancel()
    }

}
Copy the code

Here we can see that the IO coroutine is turned on in the startRefresh method, which uses localEventBus.postEvent.

5.2.3 Take a look at LocalEventBus

object LocalEventBus {
    val events = MutableSharedFlow<Event>()
    suspend fun postEvent(event: Event) {
        events.emit(event)
    }

}

data class Event(val timestamp: Long)
Copy the code

You can see that each time the postEvent method is called, the current time is emitted via emit

If there are transmissions, there must be receivers!

5.2.4 of secondary fragments

class TextFragment : Fragment() {


    private val mBinding: FragmentTextBinding by lazy {
        FragmentTextBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return mBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?). {
        super.onActivityCreated(savedInstanceState)
        lifecycleScope.launchWhenCreated {
            LocalEventBus.events.collect {
                mBinding.tvTime.text = it.timestamp.toString()
            }
        }
    }

}
Copy the code

As you can see, straight through the LocalEventBus. Events. Collect to receive, postEvent launch over the value of the real-time and change the text content!

Let’s see how it works

Perfect run!

conclusion

Well, that’s the end of this article. Through a series of comprehensive applications, I believe that the reader will have a more impressive impression of Flow and its corresponding Jetpack! In the next chapter, the Paging component corresponding to Jetpack will be explained first!