Home > Net >  How do I use an Async Task listener to return and suspend from a coroutine?
How do I use an Async Task listener to return and suspend from a coroutine?

Time:04-19

What I'm trying to do is the following:

  1. Load an Agent icon from an API
  2. Using Palette, determine what is the vibrant swatch
  3. Set this background RGB color and save it into the database
  4. Database will be updated, updating the flow and pushing the new updates to the UI
  5. UI picks it up, which then gets set into a recycler view

However, I'm struggling to get it working in my repository. My function looks like this:

suspend fun updateAgentBackground(agent: Agent, successResult: SuccessResult) {
    if (agent.backgroundRgb == null) {
        withContext(Dispatchers.IO) {
            Palette.Builder(successResult.drawable.toBitmap()).generate { palette ->
                val rgb = palette?.vibrantSwatch?.rgb
                if (rgb != null) {
                    val agentWithBackground = agent.copy(backgroundRgb = rgb)
                    agentDao.insertAgent(agentWithBackground.toAgentEntity())
                }
            }
        }
    }
}

However, it crashes at runtime on agentDao.insertAgent() because it says it's running the DB operation on the main thread. I also cannot wrap this method within the generate() callback with withContext(Dispatchers.IO) since it's not in a coroutine body.

This is what the Palette.Builder().generate() function looks like in the source code:

@NonNull
public AsyncTask<Bitmap, Void, Palette> generate(
        @NonNull final PaletteAsyncListener listener) {
    if (listener == null) {
        throw new IllegalArgumentException("listener can not be null");
    }

    return new AsyncTask<Bitmap, Void, Palette>() {
        @Override
        @Nullable
        protected Palette doInBackground(Bitmap... params) {
            try {
                return generate();
            } catch (Exception e) {
                Log.e(LOG_TAG, "Exception thrown during async generate", e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(@Nullable Palette colorExtractor) {
            listener.onGenerated(colorExtractor);
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
}

public interface PaletteAsyncListener {
    void onGenerated(@Nullable Palette palette);
}

So I'm passing it the PaletteAsyncListener which just has one function onGenerated(). I somehow want this method to suspend the coroutine, returning to me the Agent with the updated background. Or somehow to be able to call my DB method agentDao.insertAgent() within this callback off the main thread.

Can I somehow mix and match how the source code is using AsyncTask with my coroutines?

CodePudding user response:

AsyncTask was deprecated in API level 30, consider using Kotlin Coroutines instead or other Kotlin concurrency utilities. Without AsyncTask it will look something like this:

In Palette.Builder class:

suspend fun generatePalette() = withContext(Dispatchers.IO) {
    generate()
}

In repository:

suspend fun updateAgentBackground(agent: Agent, successResult: SuccessResult) {
    if (agent.backgroundRgb == null) {
        val palette = Palette.Builder(successResult.drawable.toBitmap()).generatePalette()
        val rgb = palette?.vibrantSwatch?.rgb
        if (rgb != null) {
            val agentWithBackground = agent.copy(backgroundRgb = rgb)
            agentDao.insertAgent(agentWithBackground.toAgentEntity())
        }
    }
}

Also in the AgentDao mark function insertAgent as suspend:

suspend fun insertAgent(...)

CodePudding user response:

If for some reason you cannot modify the source code of the Palette.Builder class, you could write a suspending adapting function like this. suspendCancellableCoroutine is for converting callback-based functions into suspend functions by suspending the coroutine and giving you the suspended continuation to work with directly. Resume the coroutine when the callback fires, and cancel the work if the coroutine is cancelled before it resumed.

suspend fun Palette.Builder.await(): Palette? = suspendCancellableCoroutine { cont ->
    val task = generate { cont.resume(it) }
    cont.invokeOnCancellation(task.cancel())
}

Then your other function can be written in a synchronous way and use withContext where necessary:

suspend fun updateAgentBackground(agent: Agent, successResult: SuccessResult) {
    if (agent.backgroundRgb == null) {
        val palette = Palette.Builder(successResult.drawable.toBitmap()).await()
        val rgb = palette?.vibrantSwatch?.rgb
        if (rgb != null) {
            val agentWithBackground = agent.copy(backgroundRgb = rgb)
            agentDao.insertAgent(agentWithBackground.toAgentEntity())
        }
    }
}

Make sure you mark AgentDao.insertAgent() as a suspend function so you can call if anywhere in a coroutine without worrying about calling thread.

Above code assumes toBitmap() is non-blocking. If it blocks, you should wrap it in an appropriate withContext().

  • Related