Update UI from an AsyncTaskLoader
Asked Answered
E

5

5

I've converted my AsyncTask to an AsyncTaskLoader (mostly to deal with configuration changes). I have a TextView I am using as a progress status and was using onProgressUpdate in the AsyncTask to update it. It doesn't look like AsyncTaskLoader has an equivalent, so during loadInBackground (in the AsyncTaskLoader) I'm using this:

getActivity().runOnUiThread(new Runnable() {
    public void run() {
        ((TextView)getActivity().findViewById(R.id.status)).setText("Updating...");
    }
});

I am using this in a Fragment, which is why I'm using getActivity(). This work pretty well, except when a configuration change happens, like changing the screen orientation. My AsyncTaskLoader keeps running (which is why I'm using an AsyncTaskLoader), but the runOnUiThread seems to get skipped.

Not sure why it's being skipped or if this is the best way to update the UI from an AsyncTaskLoader.

UPDATE:

I ended up reverting back to an AsyncTask as it seems better suited for UI updates. Wish they could merge what works with an AsyncTask with an AsyncTaskLoader.

Eliaeliades answered 16/4, 2012 at 2:48 Comment(0)
G
4

It's actually possible. You essentially need to subclass the AsyncTaskloader and implement a publishMessage() method, which will use a Handler to deliver the progress message to any class that implements the ProgressListener (or whatever you want to call it) interface.

Download this for an example: http://www.2shared.com/file/VW68yhZ1/SampleTaskProgressDialogFragme.html (message me if it goes offline) - this was based of http://habrahabr.ru/post/131560/

Gantline answered 29/4, 2012 at 9:7 Comment(2)
I think this is a very clever hack. I never thought about it. I was looking for the way to send my progress updates using Loaders. I am gonna give it a try and let you know. I think your solution should work!!Dade
The link seems to have gone bad. Here's a working one (I think it's the same).Snuffle
W
4

Emm... you shouldn't be doing this.

because how an anonymous class access parent class Method or Field is by storing an invisible reference to the parent class.

for example you have a Activity:

public class MyActivity
    extends Activity
{
    public void someFunction() { /* do some work over here */ }

    public void someOtherFunction() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                while (true)
                    someFunction();
            }
        };
        new Thread(r).start(); // use it, for example here just make a thread to run it.
    }
}

the compiler will actually generate something like this:

private static class AnonymousRunnable {
    private MyActivity parent;
    public AnonymousRunnable(MyActivity parent) {
        this.parent = parent;
    }

    @Override
    public void run() {
        while (true)
            parent.someFunction();
    }
}

So, when your parent Activity destroys (due to configuration change, for example), and your anonymous class still exists, the whole activity cannot be gc-ed. (because someone still hold a reference.)

THAT BECOMES A MEMORY LEAK AND MAKE YOUR APP GO LIMBO!!!

If it was me, I would implement the "onProgressUpdate()" for loaders like this:

public class MyLoader extends AsyncTaskLoader<Something> {
    private Observable mObservable = new Observable();
    synchronized void addObserver(Observer observer) {
        mObservable.addObserver(observer);
    }
    synchronized void deleteObserver(Observer observer) {
        mObservable.deleteObserver(observer);
    }

    @Override
    public void loadInBackground(CancellationSignal signal)
    {
        for (int i = 0;i < 100;++i)
            mObservable.notifyObservers(new Integer(i));
    }
}

And in your Activity class

public class MyActivity extends Activity {
    private Observer mObserver = new Observer() {
        @Override
        public void update(Observable observable, Object data) {
            final Integer progress = (Integer) data;
            mTextView.post(new Runnable() {
                mTextView.setText(data.toString()); // update your progress....
            });
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreated(savedInstanceState);

        MyLoader loader = (MyLoader) getLoaderManager().initLoader(0, null, this);
        loader.addObserver(mObserver);
    }

    @Override
    public void onDestroy() {
        MyLoader loader = (MyLoader) getLoaderManager().getLoader(0);
        if (loader != null)
            loader.deleteObserver(mObserver);
        super.onDestroy();
    }
}

remember to deleteObserver() during onDestroy() is important, this way the loader don't hold a reference to your activity forever. (the loader will probably be held alive during your Application lifecycle...)

Wellbeing answered 31/1, 2014 at 10:12 Comment(1)
It seems you are missing a setChanged() on the Observable, aren't you?Southernly
E
1

Answering my own question, but from what I can tell, AsyncTaskLoader isn't the best to use if you need to update the UI.

Eliaeliades answered 24/4, 2012 at 14:34 Comment(0)
S
0

In the class in which you implement LoaderManager.LoaderCallback (presumably your Activity), there is an onLoadFinished() method which you must override. This is what is returned when the AsyncTaskLoader has finished loading.

Sara answered 16/4, 2012 at 3:40 Comment(3)
I may be mistaken but isn't onLoadFinished called when the AsyncTaskLoader is done loading? Not exactly sure how that addresses my issue, but I'm new to AsyncTaskLoaders so I may be missing something. Is onLoadFinished the equivalent to onProgressUpdate in an AsyncTask?Eliaeliades
It's the equivalent of onPostExecuteSara
That's not what I'm looking for. I need the equivalent of onProgressUpdate, so I can show a status as the background process is running. I don't think AsyncTaskLoader has one.Eliaeliades
C
0

The best method is to use LiveData, 100% Working

Step 1: Add lifecycle dependency or use androidx artifacts as yes during project creation

implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"

Step 2: Create the loader class as follow, in loader create in public method to set the livedata that can be observed from activity or fragment. see the setLiveCount method in my loader class.

package com.androidcodeshop.asynctaskloaderdemo;

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import androidx.loader.content.AsyncTaskLoader;

import java.util.ArrayList;

public class ContactLoader extends AsyncTaskLoader<ArrayList<String>> {

private MutableLiveData<Integer> countLive = new MutableLiveData<>();


synchronized public void setLiveCount(MutableLiveData<Integer> observer) {
    countLive = (observer);
}


public ContactLoader(@NonNull Context context) {
    super(context);
}

@Nullable
@Override
public ArrayList<String> loadInBackground() {
    return loadNamesFromDB();
}

private ArrayList<String> loadNamesFromDB() {

    ArrayList<String> names = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        try {
            Thread.sleep(1000);
            names.add("Name" + i);
            countLive.postValue(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return names;
}


@Override
protected void onStartLoading() {
    super.onStartLoading();
    forceLoad(); // forcing the loading operation everytime it starts loading
}
}

Step 3: Set the live data from activity and observe the change as follows

package com.androidcodeshop.asynctaskloaderdemo;

import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.MutableLiveData;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements 
LoaderManager.LoaderCallbacks<ArrayList> {

private ContactAdapter mAdapter;
private ArrayList<String> mNames;
private MutableLiveData<Integer> countLiveData;
private static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mNames = new ArrayList<>();
    mAdapter = new ContactAdapter(this, mNames);
    RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mRecyclerView.setAdapter(mAdapter);
    countLiveData = new MutableLiveData<>();
    countLiveData.observe(this, new androidx.lifecycle.Observer<Integer>() {
        @Override
        public void onChanged(Integer integer) {
            Log.d(TAG, "onChanged: " + integer);
            Toast.makeText(MainActivity.this, "" + 
    integer,Toast.LENGTH_SHORT).show();
        }
    });

    // initialize the loader in onCreate of activity
    getSupportLoaderManager().initLoader(0, null, this);
    // it's deprecated the best way is to use viewmodel and livedata while loading data
}

@NonNull
@Override
public Loader onCreateLoader(int id, @Nullable Bundle args) {

    ContactLoader loader = new ContactLoader(this);
    loader.setLiveCount(countLiveData);
    return loader;
}

@Override
public void onLoadFinished(@NonNull Loader<ArrayList> load, ArrayList data) {
    mNames.clear();
    mNames.addAll(data);
    mAdapter.notifyDataSetChanged();
}


@Override
public void onLoaderReset(@NonNull Loader loader) {
    mNames.clear();
}

@Override
protected void onDestroy() {
    super.onDestroy();
}

}

Hope this will help you :) happy coding

Contrapositive answered 1/11, 2019 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.