Skip to main content

Retain AsyncTask when configuration change

One of the frequent facing situation in Android is how to handle configuration change such as device rotation, soft keyboard and so on. For example, what happens if you rotate the device when you download a file using AsyncTask?

The Android would create your Activity again to load the proper configuration including layout. The lifecycle methods(onCreate - onStart - onResume) will be called again and you can get data onCreate() if you have saved any data onSavedInstance(). However, your AsyncTask lost pipeline to your Activity and the code of executing the AsyncTask will be called again. This is not definitely what we want.

Let's have a look at code how to avoid it:

I'm going to create a simple app to display weather using Open Weather API for demo. We need one Activity, one Fragment for UI ListView first.

MainActivity.java
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new MainFragment())
                    .commit();
        }
    }
}
AppCompatActivity is new class introduced begining AppCompat library v21.1.1. ActionBar Activity has been deprecated. MainFragment.java
public class MainFragment extends Fragment {

    private ProgressBar mProgressBar;
    private ListView mListView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_main, container, false);

        mProgressBar = (ProgressBar)rootView.findViewById(R.id.progressBar);
        mListView = (ListView)rootView.findViewById(android.R.id.list);

        return rootView;
    }
}
res/layout/fragment_main.xml


    

    


It's just normal implementation so far. From now, it's important part. Let's create AsyncTask. But it should not lost the pipe line to the MainActivity. How can we achieve this?
How about we use a Fragment without UI? If then, the Fragment will not be affected by device rotation. Fortunately you can use a Fragment without layout. We can create a dummy Fragment and make it retained calling setRetained(true). This can only be used with fragments not in the back stack like our case. The fragment lifecycle will be slightly different when an activity is recreated, which means onDestory() will not be called but onDetach() will be still called. And then we put the AsyncTask inside of the dummy Fragment.

public class TaskFragment extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    private class WeatherTask extends AsyncTask {
        @Override
        protected ArrayList doInBackground(String... params) {

            String cityName = params[0];
            return WeatherFetcher.getForecastJson(cityName);
        }
   }

}
WeatherFetcher.getForecastJson is to get JSON data using Open Weather API and parse to return an ArrayList of weather forecast.

Now, we have to communicate MainFragment to display forecast list. You can use an interface to communicate between Activity and Fragment or Fragments. We can define an Interface inside of DummyFragment for this:

public class TaskFragment extends Fragment {

    ......
    public static interface Callback {
        public void onReady(TaskFragment fragment);
        public void onPreExecute();
        public void onProgressUpdate(Integer... values);
        public void onPostExecute(ArrayList forecasts);
        public void onCancelled();
    }
}

The interface has almost similar method to AsyncTask. Of course, you can use different prototype depending on your needs. Now, you have to implement DummyFragment.Callback in MainFragment to communicated with DummyFragment.
public class MainFragment extends Fragment implements TaskFragment.Callback{
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        //if (savedInstanceState != null){
        //Recover your data here
        //mProgressBar.setProgress(savedInstanceState.getInt(KEY_PROGRESS));
        //}

        FragmentManager fm = getActivity().getSupportFragmentManager();
        mTaskFragment= (TaskFragment)fm.findFragmentByTag(ASYNC_TASK);
        if (mTaskFragment == null){
            mTaskFragment = new TaskFragment();
            mTaskFragment.setTargetFragment(this, 0);
            fm.beginTransaction().add(mTaskFragment, ASYNC_TASK).commit();
        }

        if (!mTaskFragment.isRunning()){
            mTaskFragment.start("Sydney");
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        //Save your data here
        //outState.putInt(KEY_PROGRESS, mProgressBar.getProgress());
    }

    @Override
    public void onPreExecute() {
        mProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void onProgressUpdate(Integer... values) {
        //Do something
    }

    @Override
    public void onPostExecute(ArrayList forecasts) {
        if (forecasts != null) {
        if (mAdapter == null) {
            mAdapter = new ForecastAdapter(getActivity(), forecasts);
            mListView.setAdapter(mAdapter);
        } else {
            mAdapter.setItems(forecasts);
        }
        }
        mProgressBar.setVisibility(View.GONE);
    }

    @Override
    public void onCancelled() {
        mProgressBar.setVisibility(View.GONE);
    }
}

You have to call callback methods in the DummyFragment.

public class TaskFragment extends Fragment {
    private boolean isRunning = false;
    private Callback mCallback = null;
    private WeatherTask mTask = null;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (!(getTargetFragment() instanceof Callback)) {
            throw new IllegalStateException("Target fragment must implement the Callback interface.");
        }

        mCallback = (Callback)getTargetFragment();
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    public void start(String cityName){
        if (!isRunning){
           mTask = new WeatherTask();
           mTask.execute(cityName);
        }
    }

    public void cancel() {
        if (mCallback != null) mCallback.onCancelled();
        if (mTask != null) {
           mTask.cancel(false);
           mTask = null;
        }
        isRunning = false;
    }

    public boolean isRunning() {
        return isRunning;
    }


    private class WeatherTask extends AsyncTask {

        @Override
        protected ArrayList doInBackground(String... params) {
            if (!isCancelled()) {
                 String cityName = params[0];
                 return WeatherFetcher.getForecastJson(cityName);
            }

            return null;
        }

        @Override
        protected void onPreExecute() {
            if (mCallback!=null) mCallback.onPreExecute();
            isRunning = true;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            if (mCallback!=null) mCallback.onProgressUpdate(values);
            isRunning = true;
        }

        @Override
        protected void onPostExecute(ArrayList forecasts) {
            if (mCallback!=null) mCallback.onPostExecute(forecasts);
            isRunning = false;
        }

        @Override
        protected void onCancelled() {
            if (mCallback!=null) mCallback.onCancelled();
            isRunning = false;
        }
    }
}

Lifecycle of the fragments

Lifecylcle of the MainFragment and TaskFragment will be like below when a device is rotated.

MainFragment.onStop()
TaskFragment.onStop()
MainFragment.onDestroy()
MainFragment.onDetach()
TaskFragment.onDetach()
MainFragment.onAttach()
MainFragment.onCreate()
TaskFragment.onAttach()
MainFragment.onActivityCreated()
MainFragment.onStart()
TaskFragment.onStart()
MainFragment.onResume()
TaskFragment.onResume()
You can view the source code on Github.

Comments

Popular posts from this blog

Apply Kotlin DataBinding to Android Studio Generated Main Activity

I posted how to setup Kotlin and DataBinding in Android Stuido in the last blog (http://marksunghunpark.blogspot.com.au/2017/04/kotlin-binding-library-setup-in-android.html). In this post, I am going to how to use DataBiding in the MainActivity when you create a navigation drawer project from Android Studio templates. Layouts You will have four layouts in app/src/res/layout folder: app/src/main/res/layout/activity_main.xml app/src/main/res/layout/app_bar_main.xml app/src/main/res/layout/content_main.xml app/src/main/res/layout/nav_header_main.xml And activity_main.xml contains all other layout using include layout. You need to have tag in activity_main.xml , app_bar_main.xml and content_main.xml . If you don't have the tag, Binding library cannot recognise the sub layouts properly. Binding library doesn't support toolbar and navigation drawer yet, so you can use using BindingAdapter if you want to use binding library.(But I'm gong to skip this part for simplici

How to test AsyncTask in Android

In Android, test is not as easy as any other platform. Because Android test cannot be run without emulator. Particulary when it comes to AsyncTask or Service, it is difficult to test because they are different type of thread and hard to check their result. Then, how can we ensure the result of AsyncTask valid? AsyncTask is a thread and an asynchnorous as the name means. So, we need to wait for it finishes its job and need to capture the event. Then, when it happens in AsyncTask. It can be one of onBackground() and onPostExecute() methods. It doesn't matter you use onBackground() or onPostExecute() but I prefer onPostExecute(). Anyway, we can test an AsyncTask if we can hook it. Then, how can we hook it? For that, we can use callback pattern. But we need to delay main thread to wait for the AsyncTask's job done because we want to check the result. So the structure for the test would be like: 1. Create AsyncTask A 2. Injection a callback into A 3. Wait until A finish 4.

Let's start Lambda in Android

In Android programming, I think Java & J2EE programming are not that different, we developers put many boiler plate code particularly when we use anonymous instances. Example1. Simple example button1.setOnClickListener(new OnClickListener(){ @Override public void onClick(View view){ //Do something } }); In the above example, what we really need is inside of onClick() method. Other code is actually decorations and we don't want it. How about we can pass just onClick method body as a parameter of setOnClickListener method like below? button1.setOnClickListener(view->{ //Do something }); That's what we have exactly wanted. We can use the code style in Java 8. I think you already might know about Java 8 Lambda but there is no official support for Java 8 in Android 8. It would be awesome we can use it in Android and there is a way to be able to use Lambda in Android as well. You can refer my previous blog to setup RetroLambda in Andro