Home > Back-end >  How to schedule an API request asynchronously for one composable screen from another composable scre
How to schedule an API request asynchronously for one composable screen from another composable scre

Time:08-02

I'm a junior Android developer and trying to build a Facebook-like social media app. My issue is that when I bookmark a post in Screen B and the action succeeds, (1) I want to launch an API request in Screen A while in Screen B and (2) update the bookmarked icon ONLY for that particular post.

For the second part of the issue, I tried these two solutions.

I relaunched a manual API request on navigating back to Screen A. This updates the whole list when there's only one small change, hence very inefficient.

I built another URL route to fetch that updated post only and launched it on navigating back to Screen A. But to insert the newly updated post at the old index, the list has to be mutable and I ain't sure this is a good practice.

Please help me on how to solve this issue or similar issues. I'm not sure if this should be done by passing NavArg to update locally and then some or by using web sockets. Thanks in advance.

data class ScreenAState(
val posts: List<Post> = emptyList(),
val isLoading: Boolean = false)

data class ScreenBState(
val post: PostDetail? = null,
val isBookmarked: Boolean? = null)

data class Post(
val title: String,
val isBookMarked: Boolean,
val imageUrl: String)

data class PostDetail(
val title: String,
val content: String,
val isBookMarked: Boolean,
val imageUrl: String)

CodePudding user response:

I suggest you continue with using your logic that will update your list on return from screen B to screen A, but instead of using simple list, you could use:

https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList

This list is designed for what you need I think. Update just that one element. In mean time, you can change that item from list to some loading dummy item, if you want to have loading like view while you wait for API call to finish.

CodePudding user response:

The problem is how to handle data consistency, which is not directly related to jetpack compose. I suggest you solve this problem at the model level. Return flow instead of static data in the repository, and use collectAsState in the jetpack compose to monitor data changes.

It's hard to give an example, because it depends on the type of Model layer. If it's a database, androidx's room library supports returning flow; if it's a network, take a look at this. https://gist.github.com/FishHawk/6e4706646401bea20242bdfad5d86a9e


Triggering a refresh is not a good option. It is better to maintain an ActionChannel in the repository for each list that is monitored. use the ActionChannel to modify the list locally to notify compose of the update.

For example, you can make a PagedList if the data layer is network. With onStart and onClose, channels can be added or removed from the repository, thus giving the repository the ability to update all the observed lists.

sealed interface RemoteListAction<out T> {
    data class Mutate<T>(val transformer: (MutableList<T>) -> MutableList<T>) : RemoteListAction<T>
    object Reload : RemoteListAction<Nothing>
    object RequestNextPage : RemoteListAction<Nothing>
}

typealias RemoteListActionChannel<T> = Channel<RemoteListAction<T>>

suspend fun <T> RemoteListActionChannel<T>.mutate(transformer: (MutableList<T>) -> MutableList<T>) {
    send(RemoteListAction.Mutate(transformer))
}

suspend fun <T> RemoteListActionChannel<T>.reload() {
    send(RemoteListAction.Reload)
}

suspend fun <T> RemoteListActionChannel<T>.requestNextPage() {
    send(RemoteListAction.RequestNextPage)
}

class RemoteList<T>(
    private val actionChannel: RemoteListActionChannel<T>,
    val value: Result<PagedList<T>>?,
) {
    suspend fun mutate(transformer: (MutableList<T>) -> MutableList<T>) =
        actionChannel.mutate(transformer)

    suspend fun reload() = actionChannel.reload()
    suspend fun requestNextPage() = actionChannel.requestNextPage()
}

data class PagedList<T>(
    val list: List<T>,
    val appendState: Result<Unit>?,
)

data class Page<Key : Any, T>(
    val data: List<T>,
    val nextKey: Key?,
)

fun <Key : Any, T> remotePagingList(
    startKey: Key,
    loader: suspend (Key) -> Result<Page<Key, T>>,
    onStart: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
    onClose: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
): Flow<RemoteList<T>> = callbackFlow {
    val dispatcher = Dispatchers.IO.limitedParallelism(1)

    val actionChannel = Channel<RemoteListAction<T>>()

    var listState: Result<Unit>? = null
    var appendState: Result<Unit>? = null
    var value: MutableList<T> = mutableListOf()
    var nextKey: Key? = startKey

    onStart?.invoke(actionChannel)

    suspend fun mySend() {
        send(
            RemoteList(
                actionChannel = actionChannel,
                value = listState?.map {
                    PagedList(
                        appendState = appendState,
                        list = value,
                    )
                },
            )
        )
    }

    fun requestNextPage() = launch(dispatcher) {
        nextKey?.let { key ->
            appendState = null
            mySend()
            loader(key)
                .onSuccess {
                    value.addAll(it.data)
                    nextKey = it.nextKey
                    listState = Result.success(Unit)
                    appendState = Result.success(Unit)
                    mySend()
                }
                .onFailure {
                    if (listState?.isSuccess != true)
                        listState = Result.failure(it)
                    appendState = Result.failure(it)
                    mySend()
                }
        }
    }

    var job = requestNextPage()

    launch(dispatcher) {
        actionChannel.receiveAsFlow().flowOn(dispatcher).collect { action ->
            when (action) {
                is RemoteListAction.Mutate -> {
                    value = action.transformer(value)
                    mySend()
                }
                is RemoteListAction.Reload -> {
                    job.cancel()
                    listState = null
                    appendState = null
                    value.clear()
                    nextKey = startKey
                    mySend()
                    job = requestNextPage()
                }
                is RemoteListAction.RequestNextPage -> {
                    if (!job.isActive) job = requestNextPage()
                }
            }
        }
    }
    launch(dispatcher) {
        Connectivity.instance?.interfaceName?.collect {
            if (job.isActive) {
                job.cancel()
                job = requestNextPage()
            }
        }
    }
    awaitClose {
        onClose?.invoke(actionChannel)
    }
}

And in repository:

val postListActionChannels = mutableListOf<RemoteListActionChannel<Post>>()

suspend fun listPost() =
    daoFlow.filterNotNull().flatMapLatest {
        remotePagingList(
            startKey = 0,
            loader = { page ->
                it.mapCatching { dao ->
                    /* dao function, simulate network operation, return List<Post> */
                    dao.listPost(page)
                }.map { Page(it, if (it.isEmpty()) null else page   1) }
            },
            onStart = { postListActionChannels.add(it) },
            onClose = { postListActionChannels.remove(it) },
        )
    }

suspend fun markPost(title: String) =
    oneshot {
        /* dao function, simulate network operation, return Unit */
        it.markPost(title)
    }.onSuccess {
        postListActionChannels.forEach { ch ->
            ch.mutate { list ->
                list.map {
                    if (it.title == title && !it.isBookMarked)
                        it.copy(isBookMarked = true)
                    else it
                }.toMutableList()
            }
        }
    }
  • Related