Color Mode


    Language

Let it flow

August 12, 2020

I've been working with Flow on production for almost a year now. The light weight kotlin coroutines stream library has completely replaced RxJava for our new projects. RxJava is still a trend for Android Development even in 2020. But this article is not another RxJava x Coroutines. It is here to show that you can use Flow with almost anything in the Android framework.

Similar to when RxJava started to become a trend in Android development, there were also tons of libraries that would make anything on Android into an RxJava observer. RxBindings, RxActivityResult, RxPermissions, RxSharedPreferences, you name it. Can the same be done using Flow? Yes of course. And with the power of Kotlin, easier, more comprehensible and lighter.

Before we begin I would like to make a small disclaimer. Using flow for these scenarios can be an overkill and we don't actually need any threading for the below examples. If the scope is not handled properly it can cause memory leaks.

Gradle setup:

    dependencies {
        ...
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}"
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}"
        // Not required, used in this example to use `lifecycleScope` available from version 2.2.0 and onwards
        implementation "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleVersion}"
    }

Let's think of a simple example first: Click listener.

    fun View.onClickFlow(): Flow<View> {
        return callbackFlow {
            setOnClickListener {
                offer(it)
            }
            awaitClose { setOnClickListener(null) }
        }
    }
    lifecycleScope.launch {
        btn.onClickFlow()
            .collect { view ->
                Toast.makeText(view.context, "Clicked", Toast.LENGTH_SHORT).show()
            }
    }

clickflow

Now we can use all Flow operators on a view click listener. Let's try with a TextWatcher:

    fun EditText.afterTextChangedFlow(): Flow<Editable?> {
        return callbackFlow {
            val watcher = object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    offer(s)
                }

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
            }

            addTextChangedListener(watcher)
            awaitClose { removeTextChangedListener(watcher) }
        }
    }
    lifecycleScope.launch {
        editText.afterTextChangedFlow()
            .collect {
                textView.text = it
            }
    }

textwacherflow

Now we can add debounce, filter, map, whatever we want, let's try it.

    lifecycleScope.launch {
        editText.afterTextChangeFlow()
            .debounce(1000)
            .collect {
                textView.text = it
            }
    }

textwatcherdebounce

Adding debounce makes the flow wait an amount of time before emitting values. This is a very common use case for search operations that require network requests as an example.

Enough with View flows, let's try applying the same concept on SharedPreferences:

    fun <T> SharedPreferences.observeKey(
        key: String,
        default: T
    ): Flow<T> {
        return callbackFlow {
            send(getItem(key, default))

            val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
                if (key == k) {
                    offer(getItem(key, default))
                }
            }

            registerOnSharedPreferenceChangeListener(listener)
            awaitClose {
                unregisterOnSharedPreferenceChangeListener(listener)
            }
        }
    }

    fun <T> SharedPreferences.getItem(key: String, default: T): T {
        @Suppress("UNCHECKED_CAST")
        return when (default) {
            is String -> getString(key, default) as T
            is Int -> getInt(key, default) as T
            is Long -> getLong(key, default) as T
            is Boolean -> getBoolean(key, default) as T
            is Float -> getFloat(key, default) as T
            is Set<*> -> getStringSet(key, default as Set<String>) as T
            is MutableSet<*> -> getStringSet(key, default as MutableSet<String>) as T
            else -> throw IllegalArgumentException("generic type not handled")
        }
    }
    lifecycleScope.launch {
        launch {
            repeat(10) {
                delay(300)
                sharedPreferences.edit { putString("key", "Counting $it") }
            }
        }
        sharedPreferences.observeKey("key", "")
            .collect { string ->
                textView.text = string
            }
    }

sharedpreferenceflow

Super easy right? How about BroadcastReceiver? Why not.

    fun broadcastReceiverFlow(c: Context, intentFilter: IntentFilter): Flow<Intent> {
        return callbackFlow {
            val broadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context, intent: Intent) {
                    offer(intent)
                }
            }
            c.registerReceiver(broadcastReceiver, intentFilter)
            awaitClose {
                c.unregisterReceiver(broadcastReceiver)
            }
        }
    }

That's all for today's blog post, see you soon!

Article photo by SpaceCitySpin

androidcoroutineskotlinflowengineering

Author

Lucas Sales

Lucas Sales

Android Developer

Android Veteran, Software architecture, video games and football

You may also like

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

May 27, 2024

Introducing UCL Max AltPlay, a turn-by-turn real-time Football simulation

At this year's MonstarHacks, our goal was to elevate the sports experience to the next level with cutting-edge AI and machine learning technologies. With that in mind, we designed a unique solution for football fans that will open up new dimensions for wa...

Rayhan NabiRokon UddinArman Morshed

Rayhan Nabi, Rokon Uddin, Arman Morshed

MonstarHacks

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service