5

Kotlin Flow: The easy way to deal with local and remote data

 2 years ago
source link: https://proandroiddev.com/kotlin-flow-the-easy-way-to-deal-with-local-and-remote-data-fbcbc67eb7a5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Kotlin Flow: The easy way to deal with local and remote data

Photo by Rod Long on Unsplash

@Geev, we needed to redesign our current messaging feature. We thought to improve it as an instant messaging. We made a Proof of Concept: a simple instant messaging app using Websockets. Our team developed all platforms together — Web, WAP, iOS, Android and server-side — making all features one by one.

One of the features was a local database to keep the messages persistent. On Android, instead of using Rx as we usually do, we toke advantage of this PoC to create all by coroutines. And we found a simple way to deal between local and remote data with Kotlin Flow.

Dealing with local and remote 🤝

The goal was to be able to display saved messages if the user has no network connection, but also to avoid useless view updates.

Our process should follow these steps:

  • Fetch the messages from local database and display them
  • Get the messages from server
  • Check the difference between the two messages lists
  • If there is a difference, update the local storage and display them
  • If not.. well, enjoy doing nothing 😉

This process should avoid updating the database for no reason and to prevent redrawing the view if the messages did not changed from the last fetching.

Persistent data with Room 🗂

As a persistent data library, Room is probably the easiest and fastest recommended tool to implement on Android. It provides an abstraction layer over SQLite to allow fluent database access. We will not present Room since the documentation is well describing and you can find a thousand of topics on its integration.

However, to get a little context, we have to share some examples. The Message entity class below is the data model for the database:

@Entity data class Message( @PrimaryKey @ColumnInfo(name = “messageId”) val messageId: String, @ColumnInfo(name = “username”) val username: String, @ColumnInfo(name = “text”) val text: String )

The keyword data class is important. In Kotlin, this auto-generates equals function which we need to check our local and remote data equality. But it only uses the properties defined inside the primary constructor, so you might have to override this method to exclude or include other properties.

Next, the MessageDao to interact with the local table message:

@Dao interface MessagesDao { @Query(“SELECT * FROM message”) fun getAllMessages(): List<Message> @Insert(onConflict = ConflictStrategy.REPLACE) fun insertAllMessages(messages: List<Message>) }

To keep this post simple, there are only two functions: fetch all messages in local database and insert them. We choose REPLACE conflict strategy which replaces an item if it already exists with the same messageId. As a side note, if you use auto generated ID, this will probably not work and you will have to implement @Update function.

Getting data with Flow 🧞‍♂️

Kotlin Flow is a stream of data which can be produced and consumed sequentially and asynchronously.

It is well-suited to keep a stream alive when listening to events emitted by the server. Indeed, since Websockets works with events, the app needs to constantly listening on specific events. The function to create the Flow could be as follows:

fun listenMessages(): Flow<List<Message>> = callbackFlow { socket.on(“MessagesEvent”) { args -> val result = … trySend(result) } awaitClose { socket.off(“MessagesEvent”) } }

Let’s walk through this method above:

  • callbackFlow builder allows us to create a Flow and emit new values inside a callback.
  • socket.on listens on the event “MessagesEvent” and retrieves, inside its callback, the arguments emitted by the server.
  • trySend lets us sending a value — a parsed result — into the Flow.
  • awaitClose keeps the Flow alive. It is mandatory in order to let the Flow opened. Its lambda is triggered when the callback-based API invokes close manually or when the Flow’s consumer is cancelled. When it is called, we remove the event’s listener.

The Flow processing 🪄

Okay, the Flow is created and each time the server sends a list of messages, we will emit the new values. This is now where the magic happens...

This is the process we talk at the beginning:

fun getMessages(): Flow<List<Message>> { val messages = messagesDao.getAllMessages() return listenMessages() .onStart { emit(messages) } .onEach { result -> if (result != messages) { messagesDao.insertAllMessages(result) } } }

First, we fetch the messages from the DAO with getAllMessages and emit them into onStart. This function returns a Flow that triggers the given action before the upstreamFlow starts to be collected. This means we emit the local messages before listening to the socket.

Then, we call onEach which returns a Flow with the upstream values that invokes the action before continuing the process downstream. It allows us to check the values in the Flow and the local values equality.

So what is it happening exactly? The first passing will send the local messages. It has no differences, the Flow continues and the UI displays the list when collecting it. Next, the Flow listens on socket’s events. The server emits new values into the Flow. If these values are equal to the local ones, it does nothing and returns them to the UI, otherwise, it inserts them into the database and returns them to the UI.

This process lets us to deal nicely between local and remote data. But there is one more thing to do: how to prevent the UI to display the same list again when collecting?

viewModelScope.launch(context = Dispatchers.IO) { getMessages() .distinctUntilChanged() .collect { result -> listMessages.postValue(result) } }

When we start the Flow by collecting it, we add distinctUntilChanged. This will trigger collect only when the values has changed. Therefore, the UI will not redraw if the same list of messages is collected.

Flow makes it easy 🙌

That’s it! Thanks to Kotlin Flow, we can create a successful process to manage local and remotes values. This is also readable, flexible and easy to implement.

In the same Flow, we send the local first, emit the remote values, check if they differ, perform a local saving and also avoid redrawing if it is unnecessary… By combining onStart, onEach and distinctUntilChanged, we are able to deal easily between local and remote data.

If you found this post helpful, feel free to clap! 👏 Thanks for reading.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK