AsyncTaskLoader incorrect method call onLoadFinished - android

I'm trying to deal with AsyncTaskLoader. I implemented the Interface LoaderCallbacks follows:
public class MainActivity extends Activity implements LoaderCallbacks<String> {
...
...
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
Log.w("loader","onCreate");
return new JSONLoader(this);
}
#Override
public void onLoadFinished(Loader<String> arg0, String arg1) {
Log.w("loader","finish");
}
#Override
public void onLoaderReset(Loader<String> arg0) {
Log.w("loader","onReset");
// TODO Auto-generated method stub
}
public void useLoader() {
Log.w("loader","useLoader");
Bundle args = new Bundle();
// ...
// fill in args
// ...
Loader loader =
this.getLoaderManager().initLoader(0, args, this);
Log.w("HashLoader",String.valueOf(loader.hashCode()));
// with support library:
// Loader loader =
// context2.getSupportLoaderManager().initLoader(0, args, this);
// call forceLoad() to start processing
loader.forceLoad();
}
I get a hung event for button:
btn.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
useLoader();
}
});
When I push the button the first time the method onLoadFinished() is called after the loader. But when push button a second time it calls the first onLoadFinished().
Logcat :
useLoader
onCreate
1087415440
good
finish
useLoader
finish
1087415440
good
A little about AsyncTaskLoader:
public class JSONLoader extends AsyncTaskLoader<String> {
public JSONLoader(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
#Override
public String loadInBackground() {
try {
Thread.sleep(5000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Log.w("loader","good");
return " ";}
Why does the method onLoadFinished() not wait for the end of the Thread?

I think the initLoader function doesn't destroy and create a new loader. then on the second run, your custom loader has already been 'loaded' => it calls onLoadFinish()
try to destroy it and recreate it simply by replacing the initLoader call by a restartLoader
( Reference )
Loader loader = this.getLoaderManager().restartLoader(0, args, this);

Related

How to change fragments depending on a result of async request

I want to manage fragments on my screen depending on a result of async request to local database.
E.g:
1. Go to database
2. Get results
3. Once results are delivered, choose a fragment that should be shown to a user.
(Something like in the code below).
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().restartLoader(0, null, new LoaderManager.LoaderCallbacks<Boolean>() {
#Override
public Loader<Boolean> onCreateLoader(int id, Bundle args) {
return new MyLoader(MainActivity.this);
}
#Override
public void onLoadFinished(Loader<Boolean> loader, Boolean data) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
if (data) {
fragmentTransaction.add(new MyFragment1(), null);
} else {
fragmentTransaction.add(new MyFragment2(), null);
}
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
#Override
public void onLoaderReset(Loader<Boolean> loader) {
}
});
}
private static class MyLoader extends AsyncTaskLoader<Boolean> {
public MyLoader(Context context) {
super(context);
}
#Override
public Boolean loadInBackground() {
try {
Thread.sleep(1000); //Let's pretend we are looking for something in local db
} catch (InterruptedException e) {
e.printStackTrace();
}
return (System.currentTimeMillis() % 2) == 0; // return true or false randomly
}
}
}
As a result, I get:
java.lang.IllegalStateException: Can not perform this action inside of onLoadFinished
I can fool the system of cause and wrap the fragment transaction into a runnable and call this on a handler, but it seems that I will still get an exception is the loader results will be delivered between saveInstanceState() call and onStop().
I can't use "commitAllowingStateLoss" method, cause I rely on the valid stack of my fragments and I do not want it to be messed up.
Can anyone advise how to do that in a proper way?

Fragment has target not in fragment Manager after rotation of screen with retained async task fragment

I have been following this tutorial by Alex Lockwood (2013) on how to make a thread report back to a new activity instance after a configuration change.
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
All was working great until I tried to do this within a nested fragment. basically Activity adds Fragment A and Fragment A gets replaced with Fragment B and inside fragment B starts the a async task thread.
However, if I go back to Fragment A (via backstack) and then try to rotate I get the following exception stated in the title.
Here is the code
Main Activity
public class MainActivity extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null)
{
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(android.R.id.content, new FragmentA()).commit();
}
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
Fragment A
public class FragmentA extends Fragment implements OnClickListener
{
private Button GoToFragmentB;
private ViewGroup container;
#Override
public View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState)
{
// TODO Auto-generated method stub
View view = inflater.inflate(R.layout.fragment_a, container, false);
this.container = container;
GoToFragmentB = (Button)view.findViewById(R.id.bGoToFragmentB);
GoToFragmentB.setOnClickListener(this);
return view;
}
#Override
public void onClick(View v)
{
// TODO Auto-generated method stub
FragmentManager fragmentManager = getFragmentManager();
FragmentB fb = new FragmentB();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(container.getId(), fb, FragmentB.class.getName());
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
}
Fragment B
public class FragmentB extends Fragment implements OnClickListener, ThreadFragment.AsyncTaskCallbacks{
private ThreadFragment mThreadFragment;
private ProgressBar progress_horizontal;
private TextView percent_progress;
private Button task_button;
private static final String KEY_CURRENT_PROGRESS = "current_progress";
private static final String KEY_PERCENT_PROGRESS = "percent_progress";
#Override
public View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) {
// TODO Auto-generated method stub
View view = inflater.inflate(R.layout.fragment_b, container, false);
progress_horizontal = (ProgressBar) view.findViewById(R.id.progress_horizontal);
percent_progress = (TextView)view.findViewById(R.id.percent_progress);
task_button = (Button)view.findViewById(R.id.task_button);
task_button.setOnClickListener(this);
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
// TODO Auto-generated method stub
super.onActivityCreated(savedInstanceState);
if(savedInstanceState != null)
{
progress_horizontal.setProgress(savedInstanceState.getInt(KEY_CURRENT_PROGRESS));
percent_progress.setText(savedInstanceState.getString(KEY_PERCENT_PROGRESS));
}
FragmentManager fm = getActivity().getSupportFragmentManager();
mThreadFragment = (ThreadFragment) fm.findFragmentByTag(ThreadFragment.class.getName());
if(mThreadFragment == null)
{
mThreadFragment = new ThreadFragment();
mThreadFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(mThreadFragment, ThreadFragment.class.getName()).commit();
}
if(mThreadFragment.isRunning() == true)
{
task_button.setText(getString(R.string.cancel));
}
else
{
task_button.setText(getString(R.string.start));
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putInt(KEY_CURRENT_PROGRESS, progress_horizontal.getProgress());
outState.putString(KEY_PERCENT_PROGRESS, percent_progress.getText().toString());
}
#Override
public void onClick(View v)
{
// TODO Auto-generated method stub
if(mThreadFragment.isRunning() == true)
{
mThreadFragment.cancel();
}
else
{
mThreadFragment.start();
}
}
#Override
public void onPreExecute()
{
// TODO Auto-generated method stub
task_button.setText(getString(R.string.cancel));
Toast.makeText(getActivity(), R.string.task_started_msg, Toast.LENGTH_SHORT).show();
}
#Override
public void onProgressUpdate(int percent)
{
// TODO Auto-generated method stub
progress_horizontal.setProgress(percent * progress_horizontal.getMax() / 100);
percent_progress.setText(percent + "%");
}
#Override
public void onCancelled()
{
// TODO Auto-generated method stub
task_button.setText("Start");
progress_horizontal.setProgress(0);
percent_progress.setText("0%");
}
#Override
public void onPostExecute()
{
// TODO Auto-generated method stub
task_button.setText(getString(R.string.start));
progress_horizontal.setProgress(progress_horizontal.getMax());
percent_progress.setText(getString(R.string.one_hundred_percent));
Toast.makeText(getActivity(), R.string.task_complete_msg, Toast.LENGTH_SHORT).show();
}
#Override
public void onPause() {
// TODO Auto-generated method stub
mThreadFragment.pause();
super.onPause();
}
#Override
public void onResume() {
// TODO Auto-generated method stub
mThreadFragment.resume();
super.onResume();
}
}
Thread Fragment
public class ThreadFragment extends Fragment {
static interface AsyncTaskCallbacks
{
void onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
}
private AsyncTaskCallbacks mCallback;
private boolean mRunning;
private boolean isPause;
private TestTask mTask;
#Override
public void onAttach(Activity activity) {
// TODO Auto-generated method stub
super.onAttach(activity);
if(!(getTargetFragment() instanceof AsyncTaskCallbacks))
{
throw new IllegalStateException("Target fragment must implement the AsyncTaskCallbacks interface.");
}
if(getTargetFragment() != null)
{
mCallback = (AsyncTaskCallbacks) getTargetFragment();
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
cancel();
}
public void start()
{
if(mRunning == false)
{
mTask = new TestTask();
mTask.execute();
mRunning = true;
}
}
public void cancel()
{
if(mRunning == true)
{
mTask.cancel(false);
mTask = null;
mRunning = false;
isPause = false;
}
}
public void pause()
{
if(mRunning == true)
{
isPause = true;
}
}
public void resume()
{
isPause = false;
}
public boolean isRunning()
{
return mRunning;
}
private class TestTask extends AsyncTask<Void, Integer, Void>
{
#Override
protected void onPreExecute()
{
// TODO Auto-generated method stub
mCallback.onPreExecute();
mRunning = true;
}
#Override
protected Void doInBackground(Void... params)
{
// TODO Auto-generated method stub
for(int i = 0; !isCancelled() && i < 100; i++)
{
if(isPause == true)
{
sleep();
}
SystemClock.sleep(100);
publishProgress(i);
}
return null;
}
#Override
protected void onProgressUpdate(Integer... values) {
// TODO Auto-generated method stub
mCallback.onProgressUpdate(values[0]);
}
#Override
protected void onCancelled() {
// TODO Auto-generated method stub
mCallback.onCancelled();
mRunning = false;
}
#Override
protected void onPostExecute(Void result) {
// TODO Auto-generated method stub
mCallback.onPostExecute();
mRunning = false;
}
private void sleep()
{
try
{
while(isPause)
{
Thread.sleep(500);
}
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
}
My LogCat
09-06 19:49:31.068: E/AndroidRuntime(2402): FATAL EXCEPTION: main
09-06 19:49:31.068: E/AndroidRuntime(2402): java.lang.IllegalStateException: Failure saving state: ThreadFragment{40ce2700 #2 com.ersen.asynctaskpausetest.ThreadFragment} has target not in fragment manager: FragmentB{40d3a170}
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.support.v4.app.FragmentManagerImpl.saveAllState(FragmentManager.java:1699)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.support.v4.app.FragmentActivity.onSaveInstanceState(FragmentActivity.java:547)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.Activity.performSaveInstanceState(Activity.java:1147)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1216)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3666)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.ActivityThread.access$700(ActivityThread.java:141)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1240)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.os.Handler.dispatchMessage(Handler.java:99)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.os.Looper.loop(Looper.java:137)
09-06 19:49:31.068: E/AndroidRuntime(2402): at android.app.ActivityThread.main(ActivityThread.java:5041)
09-06 19:49:31.068: E/AndroidRuntime(2402): at java.lang.reflect.Method.invokeNative(Native Method)
09-06 19:49:31.068: E/AndroidRuntime(2402): at java.lang.reflect.Method.invoke(Method.java:511)
09-06 19:49:31.068: E/AndroidRuntime(2402): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
09-06 19:49:31.068: E/AndroidRuntime(2402): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
09-06 19:49:31.068: E/AndroidRuntime(2402): at dalvik.system.NativeStart.main(Native Method)
------------EDIT UPDATE 08 SEPT 2014--------------------
I added this to my fragment B on destroy method
#Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
mThreadFragment.setTargetFragment(null , -1);
}
I made the following adjustment to my Fragment B onActivityCreated. Please note the else is invoked if the fragment has been found by fragmentManger.findFragmentByTag
if(mThreadFragment == null)
{
mThreadFragment = new ThreadFragment();
mThreadFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(mThreadFragment, ThreadFragment.class.getName()).commit();
}
else
{
mThreadFragment.setTargetFragment(this, 0);
}
I made the following change to OnAttach inside the thread fragment
if(getTargetFragment() != null)
{
if((getTargetFragment().isVisible()) || getTargetFragment().isAdded())
{
mCallback = (AsyncTaskCallbacks) getTargetFragment();
}
else
{
mCallback = null;
}
}
The difference now is I am no longer getting the error I mentioned above. I can start a task. Destory fragment b and rotate (configuration change) without any issue. However, if I go back into fragment b (make a new instance since its destroyed) and then start a new task. I am getting this new error which is saying that fragment b is not attached.
09-08 14:07:28.495: E/AndroidRuntime(1650): java.lang.IllegalStateException: Fragment FragmentB{40cea860} not attached to Activity
09-08 14:07:28.495: E/AndroidRuntime(1650): at android.support.v4.app.Fragment.getResources(Fragment.java:603)
09-08 14:07:28.495: E/AndroidRuntime(1650): at android.support.v4.app.Fragment.getString(Fragment.java:625)
09-08 14:07:28.495: E/AndroidRuntime(1650): at com.ersen.asynctaskpausetest.FragmentB.onPreExecute(FragmentB.java:119)
09-08 14:07:28.495: E/AndroidRuntime(1650): at com.ersen.asynctaskpausetest.ThreadFragment$TestTask.onPreExecute(ThreadFragment.java:114)
Its saying that fragment b is not attached to activity. These are the culprit lines in question.
#Override
public void onPreExecute()
{
// TODO Auto-generated method stub
task_button.setText(getString(R.string.cancel));
Toast.makeText(getActivity(), R.string.task_started_msg, Toast.LENGTH_SHORT).show();
}
since accessing string resource needs an activity but its weird because if I make new fragment B shouldn't it be attached to activity already?
FINAL UPDATE: FIXED VERSION: Using the Activity to handle the callbacks as opposed to fragment B
Below is the code that was changed.
ThreadFragment: OnAttach
#Override
public void onAttach(Activity activity)
{
// TODO Auto-generated method stub
super.onAttach(activity);
if(!(activity instanceof AsyncTaskCallbacks))
{
throw new IllegalStateException("Target fragment must implement the AsyncTaskCallbacks interface.");
}
mCallback = (AsyncTaskCallbacks) activity;
}
I removed the getTargetFragment and used activity reference
Fragment B
No longer implements the ThreadFragment.AsyncTaskCallbacks interface
Removed all the code related to setTargetFragment.
The methods implemented by ThreadFragment.AsyncTaskCallbacks was kept but no longer overridden just so activity can call these to do the usual stuff.
public void onPreExecute()
{
// TODO Auto-generated method stub
task_button.setText(getString(R.string.cancel));
Toast.makeText(getActivity(), R.string.task_started_msg, Toast.LENGTH_SHORT).show();
}
public void onProgressUpdate(int percent)
{
// TODO Auto-generated method stub
progress_horizontal.setProgress(percent * progress_horizontal.getMax() / 100);
percent_progress.setText(percent + "%");
}
public void onCancelled()
{
// TODO Auto-generated method stub
task_button.setText("Start");
progress_horizontal.setProgress(0);
percent_progress.setText("0%");
}
public void onPostExecute()
{
// TODO Auto-generated method stub
task_button.setText(getString(R.string.start));
progress_horizontal.setProgress(progress_horizontal.getMax());
percent_progress.setText(getString(R.string.one_hundred_percent));
Toast.makeText(getActivity(), R.string.task_complete_msg, Toast.LENGTH_SHORT).show();
Main Activity
Implements ThreadFragment.AsyncTaskCallbacks
Added 3 instance variables FragmentB, FragmentManger, and a Boolean.
public class MainActivity extends ActionBarActivity implements ThreadFragment.AsyncTaskCallbacks{
FragmentB FB;
FragmentManager fragmentManager;
boolean didFragmentBStartATask;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fragmentManager = getSupportFragmentManager();
if(savedInstanceState == null)
{
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(android.R.id.content, new FragmentA()).commit();
}
else
{
didFragmentBStartATask = savedInstanceState.getBoolean("didFragmentBStartATask");
if(didFragmentBStartATask)
{
FB = (FragmentB)fragmentManager.findFragmentByTag(FragmentB.class.getName());
}
}
}
On Create will initialise the fragment manager.
If its a restored instance get value of Boolean to check if fragment b did a task.
if true, find fragment B by tag.
On save instance state just puts Boolean into bundle
#Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putBoolean("didFragmentBStartATask", didFragmentBStartATask);
}
The implemented methods via the interface
#Override
public void onPreExecute()
{
didFragmentBStartATask = true;
if(FB == null)
{
FB = (FragmentB)fragmentManager.findFragmentByTag(FragmentB.class.getName());
}
FB.onPreExecute();
// TODO Auto-generated method stub
}
#Override
public void onProgressUpdate(int percent) {
// TODO Auto-generated method stub
FB.onProgressUpdate(percent);
}
#Override
public void onCancelled() {
// TODO Auto-generated method stub
FB.onCancelled();
}
#Override
public void onPostExecute() {
// TODO Auto-generated method stub
FB.onPostExecute();
didFragmentBStartATask = false;
}
On pre execute,, sets Boolean to true
We must check if FB is null to prevent null pointer . if its null, find the fragment (this need for first time launch)
After that just use the FB instance to call the methods in the to do the same stuff as before.
On post execute set Boolean to false because its done
Kind of late to the party, but you should consider removing your calls to "setTargetFragment(this, 0)" for your "child" fragments. Instead, you should use getChildFragmentManager() instead of getFragmentManager() when you want to create a "child fragment" from your Fragment B.
Then in your "child fragment", you would use getParentFragment() instead of getTargetFragment() to find your enclosing fragment. The two parts of code will look something like:
threadFragment = new ThreadFragment();
FragmentManager fm = getChildFragmentManager();
fm.beginTransaction().add(threadFragment, ThreadFragment.class.getName()).commit();
Then in your ThreadFragment, you will no longer use getTargetFragment() to find your Fragment B. Instead you will call getParentFragment().
if(!(getParentFragment() instanceof AsyncTaskCallbacks))
{
throw new IllegalStateException("Target fragment must implement the AsyncTaskCallbacks interface.");
}
if(getParentFragment() != null)
{
mCallback = (AsyncTaskCallbacks) getParentFragment();
}
And finally, in your Fragment B, you need to make some changes to find your ThreadFragment. You need to change it to use the ChildFragmentManager
FragmentManager fm = getChildFragmentManager();
mThreadFragment = (ThreadFragment) fm.findFragmentByTag(ThreadFragment.class.getName());
From the documentation of setTargetFragment
public void setTargetFragment (Fragment fragment, int requestCode) Optional target for this fragment. This may be used, for example, if this fragment is being started by another, and when done wants to give a result back to the first.
Try doing this.
put this in the fragment that causes the problems(in this case may be in your Fragment A):
#Override
public void onSaveInstanceState(final Bundle outState) {
setTargetFragment(null, -1);
...
}
and remember to set it to the real target fragment when you need it.
Edit
You may try doing this in your Fragment B
#Override
public void onSaveInstanceState(final Bundle outState) {
Fragment threadFrag = new ThreadFrag();
threadFrag.setTargetFragment(this , -1);
...
}

Trying to use a Loader with AsyncTask. (Android)

I am trying to use a Loader with an AsyncTask, however the call to execute the Loader in the DoinBackground method gives me an error: The method initLoader(int, Bundle, LoaderManager.LoaderCallbacks) in the type LoaderManager is not applicable for the arguments (int, null, LoaderClass.MagicCall)
Here is my code:
private class MagicCall extends AsyncTask<Void, Void, String> {
ProgressDialog Asycdialog = new ProgressDialog(LoaderClass.this);
#Override
protected void onPreExecute() {
Asycdialog.setMessage("Working");
Asycdialog.show();
super.onPreExecute();
}
protected String doInBackground(Void... args) {
getLoaderManager().initLoader(0, null, this);
String Z = Integer.toString(insertNameBD());
return Z;
}
#Override
protected void onPostExecute(String result) {
//hide the dialog
Asycdialog.dismiss();
t3.setText(result);
super.onPostExecute(result);
}
}
you should do like this
class FooLoader extends AsyncTaskLoader {
public FooLoader(Context context, Bundle args) {
super(context);
// do some initializations here
}
public String loadInBackground() {
String result = "";
// ...
// do long running tasks here
// ...
return result;
}
}
class FooLoaderClient implements LoaderManager.LoaderCallbacks {
Activity context;
// to be used for support library:
// FragmentActivity context2;
public Loader onCreateLoader(int id, Bundle args) {
// init loader depending on id
return new FooLoader(context, args);
}
public void onLoadFinished(Loader loader, String data) {
// ...
// update UI here
//
}
public void onLoaderReset(Loader loader) {
// ...
}
public void useLoader() {
Bundle args = new Bundle();
// ...
// fill in args
// ...
Loader loader =
context.getLoaderManager().initLoader(0, args, this);
// with support library:
// Loader loader =
// context2.getSupportLoaderManager().initLoader(0, args, this);
// call forceLoad() to start processing
loader.forceLoad();
}
}
You should not use a Loader in an AsyncTask! The answer above is correct as there is an AsynTaskLoader which extends loader and provides additional methods which override the default Loader properties.
You can extend AsynTaskLoader to provide your own implementation and control sevral other loader specific features.

AsyncTaskLoader onLoadFinished with a pending task and config change

I'm trying to use an AsyncTaskLoader to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device before the load for the first selected item has completed, then the onLoadFinished() call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.
Here is the code I'm using. Activity:
public final class DemoActivity extends Activity
implements NumberListFragment.RowTappedListener,
LoaderManager.LoaderCallbacks<String> {
private static final AtomicInteger activityCounter = new AtomicInteger(0);
private int myActivityId;
private ResultFragment resultFragment;
private Integer selectedNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myActivityId = activityCounter.incrementAndGet();
Log.d("DemoActivity", "onCreate for " + myActivityId);
setContentView(R.layout.demo);
resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d("DemoActivity", "onDestroy for " + myActivityId);
}
#Override
public void onRowTapped(Integer number) {
selectedNumber = number;
resultFragment.setResultText("Fetching details for item " + number + "...");
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new ResultLoader(this, selectedNumber);
}
#Override
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
resultFragment.setResultText(data);
}
#Override
public void onLoaderReset(Loader<String> loader) {
}
static final class ResultLoader extends AsyncTaskLoader<String> {
private static final Random random = new Random();
private final Integer number;
private String result;
ResultLoader(Context context, Integer number) {
super(context);
this.number = number;
}
#Override
public String loadInBackground() {
// Simulate expensive Web call
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
}
#Override
public void deliverResult(String data) {
if (isReset()) {
// An async query came in while the loader is stopped
return;
}
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
// Only do a load if we have a source to load from
if (number != null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
result = null;
}
}
}
List fragment:
public final class NumberListFragment extends ListFragment {
interface RowTappedListener {
void onRowTapped(Integer number);
}
private RowTappedListener rowTappedListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
rowTappedListener = (RowTappedListener) activity;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
R.layout.simple_list_item_1,
Arrays.asList(1, 2, 3, 4, 5, 6));
setListAdapter(adapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
rowTappedListener.onRowTapped(adapter.getItem(position));
}
}
Result fragment:
public final class ResultFragment extends Fragment {
private TextView resultLabel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.result_fragment, container, false);
resultLabel = (TextView) root.findViewById(R.id.result_label);
if (savedInstanceState != null) {
resultLabel.setText(savedInstanceState.getString("labelText", ""));
}
return root;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("labelText", resultLabel.getText().toString());
}
void setResultText(String resultText) {
resultLabel.setText(resultText);
}
}
I've been able to get this working using plain AsyncTasks but I'm trying to learn more about Loaders since they handle the configuration changes automatically.
EDIT: I think I may have tracked down the issue by looking at the source for LoaderManager. When initLoader is called after the configuration change, the LoaderInfo object has its mCallbacks field updated with the new activity as the implementation of LoaderCallbacks, as I would expect.
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
However, when there is a pending loader, the main LoaderInfo object also has an mPendingLoader field with a reference to a LoaderCallbacks as well, and this object is never updated with the new activity in the mCallbacks field. I would expect to see the code look like this instead:
// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
It appears to be because of this that the pending loader calls onLoadFinished on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.
The new question is: Have I found a bug, or is this the expected behavior?
In most cases you should just ignore such reports if Activity is already destroyed.
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
if (isDestroyed()) {
Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
return;
}
resultFragment.setResultText(data);
}
Also you should insert checking isDestroyed() in any inner classes. Runnable - is the most used case.
For example:
// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
public void run() {
// some heavy operations
...
// notification to UI thread
handler.post(new Runnable() {
// this runnable can link to 'dead' activity or any outer instance
if (isDestroyed()) {
return;
}
// we are alive
onSomeHeavyOperationFinished();
});
}
});
But in such cases the best way is to avoid passing strong reference on Activity to another thread (AsynkTask, Loader, Executor, etc).
The most reliable solution is here:
// BackgroundExecutor.java
public class BackgroundExecutor {
private static final Executor instance = Executors.newSingleThreadExecutor();
public static void execute(Runnable command) {
instance.execute(command);
}
}
// MyActivity.java
public class MyActivity extends Activity {
// Some callback method from any button you want
public void onSomeButtonClicked() {
// Show toast or progress bar if needed
// Start your heavy operation
BackgroundExecutor.execute(new SomeHeavyOperation(this));
}
public void onSomeHeavyOperationFinished() {
if (isDestroyed()) {
return;
}
// Hide progress bar, update UI
}
}
// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
private final WeakReference<MyActivity> ref;
public SomeHeavyOperation(MyActivity owner) {
// Unlike inner class we do not store strong reference to Activity here
this.ref = new WeakReference<MyActivity>(owner);
}
public void run() {
// Perform your heavy operation
// ...
// Done!
// It's time to notify Activity
final MyActivity owner = ref.get();
// Already died reference
if (owner == null) return;
// Perform notification in UI thread
owner.runOnUiThread(new Runnable() {
public void run() {
owner.onSomeHeavyOperationFinished();
}
});
}
}
Maybe not best solution but ...
This code restart loader every time, which is bad but only work around that works - if you want to used loader.
Loader l = getLoaderManager().getLoader(MY_LOADER);
if (l != null) {
getLoaderManager().restartLoader(MY_LOADER, null, this);
} else {
getLoaderManager().initLoader(MY_LOADER, null, this);
}
BTW. I am using Cursorloader ...
A possible solution is to start the AsyncTask in a custom singleton object and access the onFinished() result from the singleton within your Activity. Every time you rotate your screen, go onPause() or onResume(), the latest result will be used/accessed. If you still don't have a result in your singleton object, you know it is still busy or that you can relaunch the task.
Another approach is to work with a service bus like Otto, or to work with a Service.
Ok I'm trying to understand this excuse me if I misunderstood anything, but you are losing references to something when the device rotates.
Taking a stab...
would adding
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest for that activity fix your error? or prevent onLoadFinished() from saying the activity stopped?

Retrieve data from sqlite and preview it

I have a button when I press it , I start a new activity myActivity. I want the layout of myActivity to preview some data retrieved from myDataBase, but when I change the text of TextView the program crash , here is my code.
public class myActivity extends Activity {
TextView tv1;
myDataBase DB;
String s;
String reqName
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.information);
tv1 = (TextView)findViewById(R.id.testtest);
Bundle bundle = getIntent().getExtras();
reqName = bundle.getString("someKey");
DB = new myDataBase(this);
new GetSomeData().execute(reqName);
}
public class GetSomeData extends AsyncTask<String,Integer ,String>
{
#Override
protected String doInBackground(String... arg0) {
// TODO Auto-generated method stub
try
{
String name = reqName;
DB.open();
s = DB.getData(name);
DB.close();
}
catch(Exception e)
{
e.printStackTrace();
}
tv1.setText(s); // this line makes the program crash
return s;
}
}}
I think the program crash because of cross threading (I am not sure!),
How can I solve this problem? Is there a better idea for previewing the data from the data base?
You're not allowed to modify UI elements from a background thread. An AsyncTasks's doInBackground() method runs in a background thread, so that's why you're getting an error.
Instead, return the String you wish to set to the TextView from your doInBackground() method to the onPostExecute() method, then set that String to your TextView there. This works, because onPostExecute() runs in the main thread, also known as the "UI thread".
public class GetSomeData extends AsyncTask<String,Integer ,String> {
#Override
protected String doInBackground(String... arg0) {
// TODO Auto-generated method stub
try {
String name = reqName;
DB.open();
s = DB.getData(name);
DB.close();
} catch(Exception e) {
e.printStackTrace();
}
return s;
}
#Override
protected void onPostExecute (String s) {
tv1.setText(s);
}
}

Categories

Resources