Home > database >  I want to use Kotlin Flow to update my UI when SharedPreferences have changed
I want to use Kotlin Flow to update my UI when SharedPreferences have changed

Time:11-21

Ok so I want to start using Kotlin-Flow like all the cool kids are doing. It seems like what I want to do meets this reactive pattern. So I receive a Firebase message in the background

...

override fun onMessageReceived(remoteMessage: RemoteMessage) {
    super.onMessageReceived(remoteMessage)
    val msg = gson.fromJson(remoteMessage.data["data"], MyMessage::class.java)
    // persist to SharedPreferences
    val flow = flow<MyMessage> { emit(msg) }

and I have a dashboard UI that simply would refresh a banner with this message. Not sure how to observe or collect this message from my DashboardViewModel. Examples and tutorials all seem to emit and collect in the same class. Sounds like I need more direction and more experience here but not much luck finding more real world examples.

CodePudding user response:

Have a look at the Kotlin docs for it: https://kotlinlang.org/docs/flow.html#flows

The basic idea is you create a Flow, and it can produce values over time. You run collect() on that in a coroutine, which allows you to asynchronously handle those updates as they come in.

Generally that flow does a bunch of work internally, and just emits values as it produces them. You could use this within a class as a kind of worker task, but a lot of the time you'd expose flows as a data source, for other components to observe. So you'll see, for example, repositories that return a Flow when you try to get a thing - it's basically "ok we don't have that yet, but it'll come through here".


I'm not an expert on them, and I know there are some caveats about the different builders and flow types, and how you emit to them - it's not always as simple as "create a flow, hand back a reference to it, emit data to it when it comes in". There's actually a callbackFlow builder specially designed around interfacing callbacks with the flow pattern, that's probably worth checking out: https://developer.android.com/kotlin/flow#callback

The example is about Firebase specifically too - it looks like the idea is broadly that the user requests some data, and you return a flow which internally does a Firebase request and provides a callback. When it gets the data, it uses offer (a special version of emit that handles the callback coming through on a different coroutine context) to output data to the observer. But it's the same general idea - all the work the flow does is encapsulated within it. It's like a task that runs on its own, producing values and outputting them.

Hope that helps! I think once you get the general idea, it's easier to follow the examples, and then understand what the more specialised things like StateFlow and SharedFlow are there for. This might be some helpful reading (from the Android devs):

edit- while I was finding those I saw a new Dev Summit video about Flows and it's pretty good! It's a nice overview of how they work and how to implement them in your app (especially for UI stuff where there are some things to consider): https://youtu.be/fSB6_KE95bU

CodePudding user response:

flow<MyMessage> { emit(msg) } could just be flowOf(msg), but it's weird to wrap a single item in a Flow. If you're making a manual request for a single thing, this is more appropriately handled with a suspend function that returns that thing. You can convert the async callback code to a suspend function with suspendCoroutine(), but Firebase already provides suspend functions you can use instead of callbacks. If you were making repeated requests for data that changes over time, a Flow would be appropriate, but you need to do it higher up by converting the async code using callbackFlow.

In this case, it looks like you are using FirebaseMessagingService, which is an Android Service, and it directly acts as a callback using this onMessageReceived function.

What you possibly could do (and I haven't tried this before), is adapt a local BroadcastReceiver into a Flow you can use from elsewhere in your app. The FirebaseMessangingService can rebroadcast local Intents that can be picked up by such a Flow. So, you could have a function like this that creates a Flow out of a local broadcast.

fun localBroadcastFlow(context: Context, action: String) = callbackFlow {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            intent.extras?.run(::trySend)
        }
    }
    LocalBroadcastManager.getInstance(context).registerReceiver(receiver, IntentFilter(action))
    awaitClose { LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver) }
}

Then in your service, you could expose the flow through a companion object, mapping to your data class type.

class MyMessageService: FirebaseMessagingService() {
    companion object {
        private const val MESSAGE_ACTION = "mypackage.MyMessageService.MyMessage"
        private const val DATA_KEY = "MyMessage key"
        private val gson: Gson = TODO()

        fun messages(context: Context): Flow<MyMessage> =
            localBroadcastFlow(context, MESSAGE_ACTION)
                .mapNotNull { bundle ->
                    val messageData = bundle.getString(DATA_KEY) ?: return@mapNotNull null
                    gson.fromJson(messageData, MyMessage::class.java)
                }
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val intent = Intent(MESSAGE_ACTION)
        intent.putExtra(DATA_KEY, remoteMessage.data["data"])
        LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
    }
}

And then in your Fragment or Activity, you can collect from MyMessageService.messages().

Note that LocalBroadcastManager is recently deprecated because it promotes the practice of exposing data to all layers of your app. I don't really understand why this should be considered always bad. Any broadcast from the system is visible to all layers of your app. Any http address is visible to all layers of your app, etc. They suggest exposing an observable or LiveData as an alternative, but that would still expose the data to all layers of your app.

  • Related