如何使用 Kotlin 在 Android 上高效使用进度条和返回函数进行 API 调用?

How can I make API calls with a progress bar and a return function while being efficient on Android using Kotlin?

提问人:Damon 提问时间:11/4/2023 更新时间:11/5/2023 访问量:65

问:

我正在根据我从 API 获得的大量数据制作一个应用程序。在显示实际应用程序之前,我需要这些信息,因此我将负责获取这些信息的代码放在加载屏幕上。我将其全部保存在本地数据库(Room Database)上,因此我不必在每次启动应用程序时都获取它(因为 API 没有太大变化),因此我实际上可以使用这些信息。

第一个问题:当我尝试我的代码时,它会保存两次(或更多)对象(可能是因为协程)。而且由于我基于提取是否结束取决于发生了多少次保存/结果数量,这导致活动认为获取已完成,停止所有调用。这意味着有些对象没有时间保存。

这是我的代码(简化):

class FetchFromAPI {
    fun fetchObjects(save: (Object) -> Unit) {
        //Get number objects from API call
        val count = API.getCount()

        //Calculate number of pages
        val limit = 100
        var pageCount = count / limit
        if (count % limit != 0) pageCount++

        //For each page
        for (i in 1..pageCount) {
            CoroutineScope(Dispatchers.IO).launch {
                //Get page
                val page = API.getPage(i)
                //Save objects from page
                savePage(page, save)
            }
        }
    }

    private fun savePage(
        page: JSONArray,
        save: (Object) -> Unit
    ) {
        //For each object in page
        for (i in 0..page.length()) {
            CoroutineScope(Dispatchers.IO).launch {
                //Get object
                val obj = page.getJSONObject(i)
                //Save object
                saveObject(obj, save)
            }
        }
    }

    private fun saveObject(
        obj: JSONObject,
        save: (Object) -> Unit
    ) {
        //Convert Json to Object
        ...

        save(obj)
    }
}

saveObject(obj)是一个函数,用于将对象保存在房间数据库上并更新提取进度(由 Flow 控制)。

所以我用谷歌搜索并了解了工作,我想“我不需要一个确切的进度数字,因为它是用 ProgressBar 显示的,而且我正在处理几千个对象,所以如果我能知道我所有的协程何时完成,并基于这个而不是进度的返回调用, 我很好(即使多次调用浪费了一些资源)”。join()runBlocking{}

第二个问题:当我尝试使用 or 时,我可以启动一个功能说它已经完成,该功能要么给我一个“应用程序已冻结”错误,要么根本无法启动。runBlocking{}join()

我的代码已更改(简化):

class FetchFromAPI {
    fun fetchObjects(save: (Object) -> Unit) {
        //Get number objects from API call
        val count = API.getCount()

        //Calculate number of pages
        val limit = 100
        var pageCount = count / limit
        if (count % limit != 0) pageCount++

        runBlocking {
            //For each page
            for (i in 1..pageCount) {
                launch {
                    //Get page
                    val page = API.getPage(i)
                    //Save objects from page
                    savePage(page, save)
                }
            }
        }
    }

    private fun savePage(
        page: JSONArray,
        save: (Object) -> Unit
    ) {
        //For each object in page
        runBlocking {
            for (i in 0..page.length()) {
                launch {
                    //Get object
                    val obj = page.getJSONObject(i)
                    //Save object
                    saveObject(obj, save)
                }
            }
        }
    }

    private fun saveObject(
        obj: JSONObject,
        save: (Object) -> Unit
    ) {
        //Convert Json to Object
        ...

        save(obj)
    }
}

fun fetchingFromAPI(onFinished: () -> Unit) {
    CoroutineScope(Dispatchers.IO).launch {
        fetchObjects(::save)
        onFinished()
    }
}

关于协程,我有什么不明白的地方吗?join()runBlocking{}

告诉我你是否需要从我的代码中获取任何其他内容。

android kotlin kotlin-协程协程

评论


答:

1赞 vasberc 11/5/2023 #1

runBlocking 会阻塞您调用它的线程,直到它完成执行。 这可能是冻结错误的原因,因为您在 for 循环中阻塞了 IO 线程,并从那里调用了保存回调。 我建议您暂停所有函数,以便能够在没有 runBlocking 的情况下使用 join。当一个函数被挂起时,它可以挂起它的执行,并且可以加入其他协程。

    class FetchFromAPI {
    suspend fun fetchObjects(save: (Object) -> Unit) {
        //Get number objects from API call
        val count = API.getCount()

        //Calculate number of pages
        val limit = 100
        var pageCount = count / limit
        if (count % limit != 0) pageCount++

        //Create a list of coroutines and join them all, so the fetchObjects 
        //will return after all the pages are saved
        (1..pageCount).map { i ->
            launch {
                //Get page
                val page = API.getPage(i)
                //Save objects from page
                savePage(page, save)
            }
        }.joinAll()
    }

    private suspend fun savePage(
        page: JSONArray,
        save: (Object) -> Unit
    ) {
        //Same logic here
        (0..page.length()).map { i ->
            launch {
                //Get object
                val obj = page.getJSONObject(i)
                //Save object
                saveObject(obj, save)
            }
        }.joinAll()
    }

    //Here if the save(obj) does not generates any new coroutine you do not need to 
    //use suspend other wise you have to use suspend and make also the save fun 
    //suspend and join the coroutine inside it.
    private fun saveObject(
        obj: JSONObject,
        save: (Object) -> Unit
    ) {
        //Convert Json to Object
        ...

        save(obj)
    }
}

现在,您可以从协程调用 fetchObjects,就像在代码的第二部分中所做的那样:

    fun fetchingFromAPI(onFinished: () -> Unit) {
        CoroutineScope(Dispatchers.IO).launch {
            fetchObjects(::save)
            onFinished()
        }
    }

可能这需要一些调试,但由于我没有您的其余代码,我无法做到这一点,所以如果有任何错误或没有像您预期的那样工作,请告诉我。

在协程中切换线程的示例:

        CoroutineScope(Dispatchers.IO).launch {
            //What ever is inside the fetchObjects will run on the IO thread
            fetchObjects(::save)
            withContext(Dispatchers.Main) {
                //Whatever is inside the onFinished will run on the main thread
                onFinished()
            }
        }

评论

1赞 Damon 11/5/2023
它正在工作!非常感谢!我发现我的第一个问题来自哪里:在我的协程中,我使用了外部声明的 var(所以它们都使用相同的 var) 但是当我放置一些日志时,我注意到处理 1 x 1 的页面(虽然不是按顺序处理)而不是并行启动:/我通过 Dispatchers.IO 启动了我所有的 Coroutiones,我以为是为了创建一个后台线程,每次都是同一个线程吗?
0赞 vasberc 11/5/2023
嗯,它应该并行运行,但我无法从我的系统调试它,因为我没有所有的代码。从文档中: kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine 协程是可挂起计算的实例。它在概念上类似于线程,从某种意义上说,它需要运行一个代码块,该代码块与其余代码同时工作。但是,协程不绑定到任何特定线程。它可能会在一个线程中暂停执行,并在另一个线程中恢复执行。因此,您调用它的方式是在每个循环的 IO 线程上运行。
0赞 vasberc 11/5/2023
因此,正如文档所说,您可以在协程中切换线程。我将编辑我的答案并在那里举个例子。
0赞 vasberc 11/5/2023
在 save 函数或其他任何地方是否有任何 runBlocking 调用?因为如果你说页面没有按顺序处理,意味着我们在这篇文章中看到的代码正在尝试以 paraller 运行。
1赞 vasberc 11/5/2023
您还可以定义一个调度程序并设置每个主机的最大请求数,默认值为 5,默认值一般为 64。我的想法不是为每个请求创建一个新客户端的好做法。下面是执行此操作的代码片段:val dispatcher = Dispatcher() dispatcher.setMaxRequests(set max here) dispatcher.setMaxRequestsPerHost(set max per host here) val client = OkHttpClient.Builder() .dispatcher(dispatcher) .build();