Search code examples
kotlininlinecoroutineandroid-progressbarfuel

Why my ProgressBar is showing up after inline coroutine function, instead of before?


When I'm sending HTTP requests to server from my tablet by using Fuel, I want to display a ProgressBar at screen before the concerned function begins to run. But for an unknow reason, it's happened once the function has completed its job.

The ProgressBar is defined programmatically by this function :

protected fun initProgressBar(constraintLayoutID: Int): ProgressBar {
        val originalConstraintLayout = findViewById<ConstraintLayout>(constraintLayoutID)
        progressBar = ProgressBar(this).apply {
            id = getInteger(R.integer.INT_PROGRESS_BAR_ID)
            elevation = 999F
            isIndeterminate = true
            layoutParams = ViewGroup.LayoutParams(300, 300)
            visibility = View.GONE

            originalConstraintLayout.addView(this)
        }

        ConstraintSet().run {
            clone(originalConstraintLayout)

            for(position in listOf(ConstraintSet.TOP, ConstraintSet.START, ConstraintSet.END, ConstraintSet.BOTTOM))
                connect(progressBar!!.id, position, constraintLayoutID, position,0)
            applyTo(originalConstraintLayout)
        }
        return progressBar!!
    }

I've tried to show/hide the ProgressBar lonely and it's working perfectly. Here is the inline function where it should be showed before sending the main request :

inline fun <reified T : DefaultResponse> sendRequest(
        routeName: String = "",
        routeArgs: String = ""
    ): T = runBlocking { // Help : https://fuel.gitbook.io/documentation/support/fuel-coroutines
        owner.runOnUiThread {
            owner.showProgressBar() // Should display at this point, not after
        }

        val adaptedResponse = ResponseFactory.getInstance(owner).getResponseObjectFor(T::class.java.name, routeName)
        val (request, response, result) = Fuel.get(
            "%s%s?%s%s".format(
                networkManager.getURLConnection(),
                (adaptedResponse as DefaultResponse).getRouteName(),
                routeArgs,
                getIndustrialTrypticArgs()
            )
        ).awaitObjectResponseResult(adaptedResponse)

        val exception =
            FailedRequestException(owner.getString(R.string.STR_EXCEPTION_RM_FAILED_REQUEST)).apply {
                setHTTPMessage(response.responseMessage)
                setStatusCode(response.statusCode)
                setUsedRequest(request.url)
            }

        return@runBlocking result.fold<T>(
            { responseObject ->
                if (response.statusCode != 200)
                    throw exception
                return@fold responseObject as T
            },
            { error ->
                throw exception.setOriginalError(error.exception)
            }
        )
    }

Also, I've added a call to runOnUiThread for the display, with an encapsulation in a different coroutine thread with launch at some point, but it didn't work (still appearing after the execution's end of the inline function).

It's because it's an inline function ? Or there's a concern about concurrent threads ?

UPDATE N°1

Following Tenfour04's advices, I changed the sendRequest into a suspend function :

suspend inline fun <reified T : DefaultResponse> sendRequest(
        routeName: String = "",
        routeArgs: String = "",
        isBlocking: Boolean = true
    ): T? {
        val adaptedResponse = ResponseFactory.getInstance(owner).getResponseObjectFor(T::class.java.name, routeName) // 1
        val fuelRequest = Fuel.get( // 2
            "%s%s?%s%s".format(
                networkManager.getURLConnection(),
                (adaptedResponse as DefaultResponse).getRouteName(),
                routeArgs,
                getIndustrialTrypticArgs()
            )
        )
        var fuelResponse: T? = null // 3

        CoroutineScope(Dispatchers.Main).launch {
            owner.showProgressBar() // 4
        }
        try {
            fuelResponse =
                if(isBlocking) {
                    val answer = fuelRequest.awaitObjectResponseResult(adaptedResponse) // 5

                    treatsResponse(answer) as T // 6
                }
                else {
                    fuelRequest.responseObject(adaptedResponse) { httpRequest, httpResponse, httpResult ->
                        try {
                            treatsResponse(httpRequest, httpResponse, httpResult) as T
                        }
                        catch (e: FailedRequestException) {
                            println(e.getOriginalError())
                            println(e.getUsedRequest())
                        }
                        finally {
                            owner.hideProgressBar()
                        }
                    }
                    null
                }
        }
        catch (e: FailedRequestException) {
            println(e.getOriginalError())
            println(e.getUsedRequest())
        }
        finally {
            owner.hideProgressBar()
        }
        return fuelResponse
    }

The call of sendRequest is encapsulated into a CoroutineScope(Dispatchers.Main).launch. It's an improvement since the ProgressBar begins to appear within the function and not after its call, but I still want that it display before the fuelRequest.awaitObjectResponseResult(adaptedResponse) line (// 5).

awaitObjectResponseResult is also a suspend function.

UPDATE N°2

While a line-per-line debug session, I remarked the following on sendRequest : when owner.showProgressBar() is encapsulated into CoroutineScope(Dispatchers.Main).launch (// 4), it's clearly executed after // 5th instruction.

So, the actual order of execution since the beginning is always 1 -> 2 -> 3 -> 5 -> 4 -> 6.

When I add Dispatchers.Main as the second argument of awaitObjectResponseResult, the order become 1 -> 2 -> 3 -> 5 -> 6 -> 4. It looks like this function always get the priority for some reason...


Solution

  • It appears that Fuel has an open issue with native Java's async IO API. Whatever I've tried so far just can't work on the main thread since Fuel's method awaitObjectResponseResult blocks it completely until it have been called. Apparently, this issue will be fixed in the next major update.

    UPDATE N° 1 [21/05/2024] :

    I've found a solution using a thread with a slightly priority above to main thread. If anyone is having the same kind of issue, here's the codebase :

    inline fun <reified T : DefaultResponse> sendRequestAndExecute(
        routeName: String = "",
        routeArgs: String = "",
        routeDetails: JSONObject = JSONObject(),
        isBlocking: Boolean = true,
        mustCatchFailedRequestException: Boolean = true
    ) {
        thread(priority = Process.THREAD_PRIORITY_MORE_FAVORABLE) {
            owner.uiBuilder.showProgressBar()
    
            requestScope.launch {
                val adaptedResponse = ResponseFactory.getInstance(owner).getResponseObjectFor(
                    T::class.java.name,
                    routeName,
                    routeArgs,
                    routeDetails
                )
                val fuelRequest = createFuelRequest(
                    (adaptedResponse as DefaultResponse).routeName,
                    routeArgs
                )
                var fuelResponse: T? = null
    
                if(mustCatchFailedRequestException) {
                    try {
                        fuelResponse = execute(fuelRequest, adaptedResponse, isBlocking)
                    } catch (e: FailedRequestException) {
                        println(e.getOriginalError())
                        println(e.getUsedRequest())
                        e.printStackTrace()
                    }
                }
                else
                    fuelResponse = execute(fuelRequest, adaptedResponse, isBlocking)
                owner.uiBuilder.hideProgressBar()
                fuelResponse?.execute()
            }
        }
    }
    

    UPDATE N° 2 [26/06/2024] :

    Previous solution worked fine until recently. I don't know exactly why, but Android were showing then hiding immediatly the ProgressBar while the response from the serveur was been expected (no more await on the method "execute"). Here's a fixed codebase if this happened on your side :

    inline fun <reified T : FuelRequest> sendRequestAndExecute(
        requestArgs: String = "",
        requestDetails: JSONObject = JSONObject(),
        mustShowProgressBar: Boolean = true,
        noinline callback: ((response: FuelRequest) -> Unit)? = null
    ) {
        thread(priority = Process.THREAD_PRIORITY_MORE_FAVORABLE) {
            val fuelRequest = FuelResponseFactory.getFuelRequestWith(
                owner,
                T::class.java.name,
                requestArgs,
                requestDetails
            )
    
            if(mustShowProgressBar)
                owner.uiBuilder.showProgressBar()
            execute( // The hiding ProgressBar method have been moved in this one
                createFuelRequest(
                    (fuelRequest as FuelRequest).httpRouteName,
                    requestArgs
                ),
                fuelRequest,
                callback
            )
        }.join()
    }
    
    
    fun execute(
        fuelRequest: Request,
        httpResponse: ResponseDeserializable<FuelRequest>,
        callback: ((response: FuelRequest) -> Unit)? = null,
        fuelResponseID: Long = 0
    ) {
        fuelRequest.responseObject(httpResponse) { request, response, result ->
            owner.uiBuilder.hideProgressBar() // Now present in the asynchrone response callback
    
            // Here's your code...
        }
    }