Why android room inserts an object at the end of the asynctask generating it (the object)?

Vincent AUDIBERT :

Summary

Room immediately inserts entities generated through UI, but delays those sent by an asynctask until the (far) end of the generating asynctask : the entity objects received are usable and displayed on UI, but without any id from database, hampering any other operation relying on id. The insert operation happens only when the generating asynctask is properly stopped: Why? And how to solve this?

More context

The generating asynctask

We use an asynctask to monitor a socket and send back some events (as Room entity) to the application repository (as intended by android architecture components). This asynctask basically runs continuously in background (with some sleep regularly set) and is only stopped a while before the end of use of the application (if done right). So far it hasn't caused any issue for us to deviate so much from the original concept of short-lived asynctask. I am pretty much aware we could improve the design, but this is another subject/question/time-hole ;-).

Room insert operation

Insertion happens through a dedicated asynctask, where the returned id of the entry in database is affected back to the entity just inserted (see code below). This is logged and entities from UI are persisted "immediately", they get back their ID and all is well. The asynctask-generated entities, well they wait for their "parent" task to stop and are then all inserted.

Entity composition

At first, the entity was generated inside the asynctask and sent through progress message. Then the construction of the object was moved outside of the asynctask and at the same level of the UI event construction, yet same behavior. These events are some longs (timestamps) and several strings.

From the generating asynctask all starts from here:

    @Override
    protected void onProgressUpdate(OnProgressObject... values) {
        OnProgressObject onProgressObject = values[0];

        if (onProgressObject instanceof OnProgressEvent) {
            eventRecipient.sendAutoEvent(((OnProgressEvent) onProgressObject).autoEvent);
        }
    }

The eventRecipient is the EventsRepository:

    public void sendAutoEvent(AutoEvent autoEvent) {
        Log.d(LOG_TAG, "got an autoevent to treat...");
        EventModel newEvent = EventModel.fromCub(
                autoEvent.cubTimeStamp,
                autoEvent.description,
                autoEvent.eventType
        );
        addEvent(newEvent);
    }


    public void addEvent(EventModel event) {

        new insertEventAsyncTask(event).execute(event);

        // other operations using flawlessly the "event"...
    }

    private class insertEventAsyncTask extends AsyncTask<EventModel, Void, Long> {
        private EventModel eventModel;
        public insertEventAsyncTask(EventModel eventModel) {
            this.eventModel = eventModel;
        }
        @Override
        protected Long doInBackground(EventModel... eventModels) {
            // inserting the event "only"
            return eventDao.insert(eventModels[0]);
        }

        @Override
        protected void onPostExecute(Long eventId) {
            super.onPostExecute(eventId);
            // inserting all the medias associated to this event
            // only one media is expected this way though.
            eventModel.id = eventId;
            Log.d(LOG_TAG, "event inserted in DB, got id : " + eventId);
        }

    }
Vincent AUDIBERT :

Why ?

Basically, it was written in the AsyncTask documentation : all asynctasks are executed serially on a unique background thread.

My code, even without nested asynctask, was blocking this thread with an almost never-ending task, delaying all database operations until its completion (or app crash, hence some data loss).

A quick solution : moving an AsyncTask to a Thread

Other alternatives were nicely listed by (CommonsWare)[https://stackoverflow.com/a/56925864/9138818], here are the steps I followed that solved this issue.

The main difficulty was to redirect code that was executed on UI thread (onPreExecute, onProgressUpdate, onPostExecute) through a Handler associated to the main thread.

First step was get a reference to a handler :

// Inside Runnable task's constructor :
// get the handler of the main thread (UI), needed for sending back data.
this.uiHandler = new Handler(Looper.getMainLooper());

Then, the "doInBackground" is refactored to fit a Runnable main method signature :

// previously "public String doInBackground()"
//  returned value handled through publishProgress.
@Override
public void run() {
    // recommended by Android Thread documentation
    android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

    // code previously in doInBackground

Now, code in the onProgressUpdate (that was called by publishProgress inside doInBackground method) was moved into a Runnable posted on the UI thread handler :

// asynctask method onProgressUpdate was renamed publishProgress => 
//  doInBackground's body is almost untouched.
private void publishProgress(final OnProgressObject... values) {
    uiHandler.post(new Runnable() {
        @Override
        public void run() {
            // move here code previously in the AsyncTask's publishProgress()
        }
    });

}

At last, I had to change the way the task was created, runned and stopped by using Thread.interrupted instead of isCancelled and by creating the Runnable task before the thread :

public void startCUBMonitoring() {
    if (autoEventThread == null) {
        Log.d(LOG_TAG, "startCUBMonitoring");
        addUIEvent("CUB MONITORING STARTED", "CUB_connexion");
        SessionRepository sessionRepository =
                ElabsheetApplication.getInstance().getSessionRepository();

        // Creation of the task
        AutoEventTask autoEventTask = new AutoEventTask(
                this,
                sessionRepository,
                sessionRepository.getCUBConfig()
        );
        autoEventThread = new Thread(autoEventTask);
        autoEventThread.start();
    }

}

public void stopCUBMonitoring() {
    if (autoEventThread != null) {
        Log.d(LOG_TAG, "stopCUBMonitoring");
        addUIEvent("CUB MONITORING STOPPED", "CUB_connexion");
        autoEventThread.interrupt();
        autoEventThread = null;
    }
}

Hoped it could help...

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=155239&siteId=1