MrHash
MrHash

Reputation: 45

Observer not called and postvalue not working from background thread when activity is destroyed then recreated

The following simple example code have an asynchronous task that repeatedly sleeps for 2 seconds then trigger a callback function onResultAvailable with "some text .." (which I use here to simulate a websocket thread that receives messages ...).

The onResultAvailable method in the MyViewModel class is supposed to update the variable text (which is a MutableLiveData<String>) using postValue (because it is called from a background thread).

So this code is supposed to update the text variable each two seconds and display the text via the observer in MyTestActivity; so it is supposed to display the following, which works as expected the first time:

Some text 0
Some text 1
Some text 2
... etc

However, when I press the backbutton and then open MyTestActivity again, the text variable isn't modified anymore by postvalue and the observer isn't called anymore. Note that the other variables which are modified using setvalue are still behaving as expected.

Any ideas why the postvalue works fine from background thread the first time, but doesn't work anymore when the activity is destroyed then recreated ? How, can I update the value of the text variable in this case ?

MainActivity.java

public class MainActivity extends AppCompatActivity {
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.myButton);

        button.setOnClickListener(v -> {
            startActivity(new Intent(MainActivity.this, MyTestActivity.class));
        });
    }
}

MyTestActivity.java

public class MyTestActivity extends AppCompatActivity {
    TextView myTextView, myTextViewTime;
    Button myButton;
    MyViewModel vm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_test);
        myTextView = findViewById(R.id.myTextView);
        myTextViewTime = findViewById(R.id.myTextViewTime);
        myButton = findViewById(R.id.myButton);

        vm = ViewModelProviders.of(this).get(MyViewModel.class);

        vm.getText().observe(this, text -> {
            myTextView.setText(text);
            Log.d("DEBUG", "observed: " + text);
        });

        vm.getTimestamp().observe(this, strTs -> {
            myTextViewTime.setText(strTs);
            Log.d("DEBUG", "observed: " + strTs);
        });

        myButton.setOnClickListener(v -> {
            vm.setTimestamp("" + System.currentTimeMillis());
        });
    }
}

MyViewModel.java

public class MyViewModel extends ViewModel implements MyAsyncTaskDelegate {
    private MutableLiveData<String> text;
    private MutableLiveData<String> timestamp;
    private MyAsyncTask task;

    public MyViewModel() {
        text = new MutableLiveData<>();
        text.setValue("Hello World!");

        timestamp =  new MutableLiveData<>();
        timestamp.setValue(""+System.currentTimeMillis());

        task = new MyAsyncTask(this);
        task.execute();
    }

    public LiveData<String> getText() {
        return text;
    }

    public LiveData<String> getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(String ts) {
        timestamp.setValue(ts);
    }

    @Override
    public void onResultAvailable(String result) {
        text.postValue(result);
    }
}

MyAsyncTask.java

public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    private MyAsyncTaskDelegate delegate;

    MyAsyncTask(MyAsyncTaskDelegate delegate) {
        this.delegate = delegate;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        for(int i = 0; i < 100; i++) {
            try {
                Thread.sleep(2000);
                delegate.onResultAvailable("Some text " + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

MyAsyncTaskDelegate.java

public interface MyAsyncTaskDelegate {
    void onResultAvailable(String result);
}

Upvotes: 2

Views: 1907

Answers (1)

B&#246; macht Blau
B&#246; macht Blau

Reputation: 13009

Any ideas why the postvalue works fine from background thread the first time, but doesn't work anymore when the activity is destroyed then recreated ?

In a nutshell: this happens because MyAsyncTask.doInBackground() will loop infinitely and for most Android versions, there is no parallel execution of AsyncTasks.

In MyTestActivity.onCreate(), you generate a new instance of MyViewModel which starts its own MyAsyncTask. Because the first instance of MyAsyncTask never finishes, the second one never really starts. The first instance of MyAsyncTask still holds on to the first instance of MyViewModel (aka MyAsyncTaskDelegate). But since this first MyViewModel instance does not have any connection to an active UI element, the updates don't show on the screen.

You can verify this with a debugger by setting breakpoints in the various methods and comparing the "instance numbers" (there is a '@' and a unique number appended to each class name)

Please note that the problematic behavior only happens if you leave MyTestActivity by pressing BACK. If you rotate the device, the updates continue.

This is "working as intended". By pressing BACK, you indicate that you don't need the current instance of MyTestActivity anymore: it won't be kept on the back stack, it can and will be completely destroyed.

The AsyncTask is another case: it is a member of MyViewModel and on the other hand holds a reference to MyViewModel. One could say there is a reference deadlock: it will only be stopped if the whole surrounding pocess is stopped.

How can I update the value of the text variable in this case ?

Override onBackPressed() in MyTestActivity and stop the execution of MyAsyncTask. See the docs on AsyncTask.cancel() on how to do so.

Upvotes: 2

Related Questions