Lesson 91. AsyncTask. rotate the screen

Lesson 91. AsyncTask. rotate the screen


In this lesson:

– keep in touch with AsyncTask when you rotate the screen

For a complete understanding of the lesson, it is advisable to know what internal classes and static objects are.

In the past lessons, Activity created an internal class that inherits AsyncTask. Later, after clicking the button, we created an instance of this class and worked with it. All is well … but if we return the screen, Activity will be recreated, all the past objects will be lost. Including we will lose the link to our created AsyncTask. And AsyncTask itself will work with the old Activity and keep it in memory, because the inner class object (AsyncTask) contains a hidden reference to the outer class object (Activity).

Let’s see this and see how it fixes.

Since we are going to work with the screen rotation, create a project for Android 2.2 and use AVD based on Android 2.2, because 2.3 is wrong.

Let’s create a project:

Project name: P0911_AsyncTaskRotate
Build Target: Android 2.2
Application name: AsyncTaskRotate
Package name: en.startandroid.develop.p0911asynctaskrotate
Create Activity: MainActivity

main.xml:



	
	

MainActivity.java:

import java.util.concurrent.TimeUnit;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends Activity {

  MyTask mt;
  TextView tv;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Log.d("qwe", "create MainActivity: " + this.hashCode());

    tv = (TextView) findViewById(R.id.tv);

    mt = new MyTask();
    Log.d("qwe", "create MyTask: " + mt.hashCode());
    mt.execute();
  }

  class MyTask extends AsyncTask {

    @Override
    protected Void doInBackground(String... params) {
      try {
        for (int i = 1; i <= 10; i++) {
          TimeUnit.SECONDS.sleep(1);
          publishProgress(i);
          Log.d("qwe", "i = " + i 
              + ", MyTask: " + this.hashCode()
              + ", MainActivity: " + MainActivity.this.hashCode());
        }

      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
      super.onProgressUpdate(values);
      tv.setText("i = " + values[0]);
    }
  }
}

Normal AsyncTask that pauses a loop (1 sec) and writes a loop iteration number (s) on TextView.

It is notable here that we use the hashCode method. This method returns the hash of the object. Now, let's not get into what it is and why it is needed. The important thing is to know that different objects have different hash codes. That is, by the hash code we will distinguish objects from each other (we can perceive the hash code as some kind of ID).

When creating MainActivity and MyTask, we log their hash codes. Then when we complete the task, we will also log the hash codes of these objects. Now it will be clear why it is necessary.

We will save everything and launch the application. It immediately starts a task that once every second issues a cycle iteration number. Wait until, for example, 5 appears on the screen

and rotate the screen (CTRL + F12 or CTRL + F11)

The countdown again went from one.

We wait for the end of the reference and look at the logs. I will publish their parts and comments:

create MainActivity: 1156854488
create MyTask: 1156875480

Objects have been created and we see their hash codes.

Next, MyTask gets started

i = 1, MyTask: 1156875480, MainActivity: 1156854488
i = 2, MyTask: 1156875480, MainActivity: 1156854488
i = 3, MyTask: 1156875480, MainActivity: 1156854488
i = 4, MyTask: 1156875480, MainActivity: 1156854488
i = 5, MyTask: 1156875480, MainActivity: 1156854488

Outputs the iteration number and hash codes - its and MainActivity with which it works. The hash codes match the ones that were previously logged when created. Everything is clear here.

Now we turn the screen.

create MainActivity: 1156904328
create MyTask: 1156916144

A new MainActivity is created and a new MyTask is created. Their hash codes (1156904328 and 1156916144) are different from the hash codes of the old MainActivity and MyTask (1156854488 and 1156875480). That is, completely different, new objects.

i = 6, MyTask: 1156875480, MainActivity: 1156854488
i = 7, MyTask: 1156875480, MainActivity: 1156854488
i = 1, MyTask: 1156916144, MainActivity: 1156904328
i = 8, MyTask: 1156875480, MainActivity: 1156854488
i = 2, MyTask: 1156916144, MainActivity: 1156904328
i = 9, MyTask: 1156875480, MainActivity: 1156854488
i = 3, MyTask: 1156916144, MainActivity: 1156904328
i = 10, MyTask: 1156875480, MainActivity: 1156854488
i = 4, MyTask: 1156916144, MainActivity: 1156904328
i = 5, MyTask: 1156916144, MainActivity: 1156904328
i = 6, MyTask: 1156916144, MainActivity: 1156904328
i = 7, MyTask: 1156916144, MainActivity: 1156904328
i = 8, MyTask: 1156916144, MainActivity: 1156904328
i = 9, MyTask: 1156916144, MainActivity: 1156904328
i = 10, MyTask: 1156916144, MainActivity: 1156904328

We see how the old MyTask (1156875480) continues to work, and it works with the old MainActivity (1156854488), continuing the count from 6 to 10.

And in parallel with it works the new MyTask (1156916144) with the new MainActivity (1156904328), it started from 1. On the screen we see exactly the work of these new objects. Therefore, the numbers in TextView have again gone from one. And the old objects continue to exist somewhere in memory and work. But the main thing is that we lost touch with the old MyTask, a new task was created and the work started again.

Each time you start the task again when you rotate the screen, it will turn out the wrong application. We will fix. When creating a new Activity, we need to somehow get a link to the old MyTask and not create a new one, so as not to start from the beginning but continue it. The onRetainNonConfigurationInstance and getLastNonConfigurationInstance methods will help us do this. You can read about them in lesson 70.

Add to the MainActivity class a method implementation onRetainNonConfigurationInstance:

  public Object onRetainNonConfigurationInstance() {
    return mt;
  }

When you rotate the screen, the system will store references to the mt object for us.

And we will rewrite onCreate:

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Log.d("qwe", "create MainActivity: " + this.hashCode());

    tv = (TextView) findViewById(R.id.tv);

    mt = (MyTask) getLastNonConfigurationInstance();
    if (mt == null) {
      mt = new MyTask();
      mt.execute();
    }
    Log.d("qwe", "create MyTask: " + mt.hashCode());
  }

When creating an Activity, we ask that the system (getLastNonConfigurationInstance) be returned to us and stored in the onRetainNonConfigurationInstance method and returned to MyTask. If the Activity is not created after turning the screen, then we get null, so we create MyTask ourselves.

So, when you rotate the screen, we regain old MyTask. Let's see what happens.

Rotate the emulator vertically. Save and run the application.

We are waiting till 5

and return the screen:

And nothing happens on the screen, though the logs keep going. Let's see what's in the logs:

create MainActivity: 1156854504
create MyTask: 1156875408
i = 1, MyTask: 1156875408, MainActivity: 1156854504
i = 2, MyTask: 1156875408, MainActivity: 1156854504
i = 3, MyTask: 1156875408, MainActivity: 1156854504
i = 4, MyTask: 1156875408, MainActivity: 1156854504
i = 5, MyTask: 1156875408, MainActivity: 1156854504

Everything is clear here, objects are created, the task started to work

create MainActivity: 1156904256
create MyTask: 1156875408

Creating a new MainActivity with a new hash code (1156904256). But MyTask we got old (the same hash code - 1156875408), we managed to regain access to the old MyTask and not create a new one. So work will continue and will not start again. It's good. But there is also bad news.

i = 6, MyTask: 1156875408, MainActivity: 1156854504
i = 7, MyTask: 1156875408, MainActivity: 1156854504
i = 8, MyTask: 1156875408, MainActivity: 1156854504
i = 9, MyTask: 1156875408, MainActivity: 1156854504
i = 10, MyTask: 1156875408, MainActivity: 1156854504

The old MyTask continues to work with the old MainActivity (1156854504), and the new (1156904256) does not see the focus.

This happens because the inner class object (MyTask) contains hidden link to an external class object (MainActivity). Note that MyTask methods work with the tv object. But there is no such object in MyTask, it is only in MainActivity. This uses a hidden link - this allows MyTask to work with MainActivity objects.

Therefore, our old MyTask is associated with its external object MainActivity class and only sees it. And changes the text in the TextView of the old MainActivity that hangs somewhere in memory. And on the screen we see the new MainActivity. And it does not change.

The fact that MyTask contains references to the old MainActivity is bad because the MainActivity cannot be destroyed and hangs in memory.

So we need to get rid of MainActivity and MyTask. For this we will apply static to the MyTask inner class. Internal static class in no way not connected with an external class object and does not contain a hidden reference to it. But we need access to (tv) MainActivity objects. If there is no link, there will be no access. So, we will create such a link ourselves. In MyTask we describe an object, it will refer to MainActivity. And we'll manage this link - when a new MainActivity is created, we'll link to it in MyTask.

rewrite MainActivity.java:

package ru.startandroid.develop.p0911asynctaskrotate;

import java.util.concurrent.TimeUnit;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends Activity {

  MyTask mt;
  TextView tv;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Log.d("qwe", "create MainActivity: " + this.hashCode());

    tv = (TextView) findViewById(R.id.tv);

    mt = (MyTask) getLastNonConfigurationInstance();
    if (mt == null) {
      mt = new MyTask();
      mt.execute();
    }
    // передаем в MyTask ссылку на текущее MainActivity
    mt.link(this);
    
    Log.d("qwe", "create MyTask: " + mt.hashCode());
  }

  public Object onRetainNonConfigurationInstance() {
    // удаляем из MyTask ссылку на старое MainActivity
    mt.unLink();
    return mt;
  }
  

  static class MyTask extends AsyncTask {
    
    MainActivity activity;
    
    // получаем ссылку на MainActivity
    void link(MainActivity act) {
      activity = act;
    }
    
    // обнуляем ссылку
    void unLink() {
      activity = null;
    }

    @Override
    protected Void doInBackground(String... params) {
      try {
        for (int i = 1; i <= 10; i++) {
          TimeUnit.SECONDS.sleep(1);
          publishProgress(i);
          Log.d("qwe", "i = " + i + ", MyTask: " + this.hashCode()
              + ", MainActivity: " + activity.hashCode());
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
      super.onProgressUpdate(values);
      activity.tv.setText("i = " + values[0]);
    }
  }
}

we added static to the class description MyTask. Also described in it the object activity MainActivity class and two methods:

link - using it, MyTask will receive a link to MainActivity from which it will work

unlink - zeroing the link

And now, in the MyTask class, we can no longer just work with MainActivity objects because MyTask is static, and it does not contain a hidden reference to MainActivity. We must explicitly indicate that we are referring to MainActivity, for example activity.tv.

In the method onRetainNonConfigurationInstance before saving MyTask for transmission to the new Activity, we will zero the references to the old MainActivity. MyTask will no longer hold the old MainActivity and the system will be able to destroy it (MainActivity).

And in onCreate we, after creating / receiving the MyTask object, call the link method and pass the link to the current new MainActivity there. It will continue to work with MyTask.

Let's check it out. Rotate the emulator vertically. We will save everything and launch the application. The countdown is over. we are expecting 5

and rotate the screen

The countdown continued, which was to be obtained. We look at the logs:

create MainActivity: 1156967624
create MyTask: 1156978504
i = 1, MyTask: 1156978504, MainActivity: 1156967624
i = 2, MyTask: 1156978504, MainActivity: 1156967624
i = 3, MyTask: 1156978504, MainActivity: 1156967624
i = 4, MyTask: 1156978504, MainActivity: 1156967624
i = 5, MyTask: 1156978504, MainActivity: 1156967624

Objects were created, work was gone

rotate the screen

create MainActivity: 1156991528
create MyTask: 1156978504

MainActivity is new (1156991528), MyTask is old (1156978504).

i = 6, MyTask: 1156978504, MainActivity: 1156991528
i = 7, MyTask: 1156978504, MainActivity: 1156991528
i = 8, MyTask: 1156978504, MainActivity: 1156991528
i = 9, MyTask: 1156978504, MainActivity: 1156991528
i = 10, MyTask: 1156978504, MainActivity: 1156991528

Old MyTask has received a link to the new MainActivity and is still working with it. And the old MainActivity is gone.

Ugh! Wanted to show the mechanism superficially, but got into explaining the "what to what" and turned out to be quite difficult to understand the lesson. If there are still unclear moments - velcom in the forum, the branch of this lesson. Let's understand 🙂

There is another way (other than static) to avoid AsyncTask and Activity links - just make your class that inherits AsyncTask not internal but separate from MainActivity.

Please re-open lesson 86 and read the 4 rules for using AsyncTask. I think, now they will be much more informative for you than at the first reading.

P.S. The forum is right to notice that there is a slight downside to this. It will be bad if onProgressUpdate is executed between when the old Activity executes the unLink method and the moment when the new Activity executes the link method. In this case, our activity will be null and we will get NullPointerException. Probability is all, of course, small, but somehow it is necessary to solve a problem.

I'll write my solution here. In the onProgressUpdate method, we put a check activity == null. If activity is NOT null, then we change textView without any problems. If activity is null, then the text that we wanted to write in TextView, we store in some thread our variable of class MyTask. And the new Activity, when receiving MyTask, retrieves data from this variable and places it in TextView.

Please write your suggestions for solving this problem at the branch of this lesson.

In the next lesson:

- create, start and stop simple service




Discuss in the forum [113 replies]

Leave a Comment