Search code examples
javaandroidretrofitrx-java2mutablelivedata

Databinding/Live data handle configuration changes with Retrofit/Rxjava


Please look at this code:

Main activity:

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

        MutableLiveData<User> mutableUser = new MutableLiveData<>();
        mutableUser.setValue(new User("John","Gordon","Homeless"));

        activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        activityMainBinding.setHandler(new MainActivityHandler());

        activityMainBinding.setLifecycleOwner(this);
        activityMainBinding.setUser(mutableUser);

        setSupportActionBar(activityMainBinding.toolbar);
    }

MutableLiveData is defined in xml layout:

<variable name="user" type="android.arch.lifecycle.MutableLiveData&lt;test.databindingtemplate.ViewModels.User&gt;"/>

Each field from user is bound to control with data binding:

<EditText
                android:id="@+id/editTextName"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="8dp"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:ems="10"
                android:inputType="textPersonName"
                android:text="@={user.firstName}"
                app:layout_constraintBottom_toTopOf="@+id/textViewSecondName"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@+id/textViewSecondName"
                app:layout_constraintTop_toTopOf="parent"/>

This works fine, configuration changes are tracked and populated to gui (for example, when I type something in "Name" field, value persists when screen rotates).

Next, I want to implement refreshing user details from rest webservice - Retrofit/RxJava and I want to show busy indicator while data loading is in progress, for example, in onCreate method:

showBusyIndicator(); 
testService.getUserDetails(headers)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(user ->
                {
                  hideBusyIndicator();
                  activityMainBinding.getUser().setValue(user);
                }, throwable -> 
                {
                    Log.d(TAG, "ERROR loading user");
                    hideBusyIndicator();
                });

Now I'm not sure how to handle configuration changes properly - when busy indicator is shown, it should be recreated after screen rotation changes. As MutableLiveData handles configuration changes nicely, I am unable to find any solution (other than "classic" way) to solve my problem with loading indicator and handle result in this case.

Can you point me right direction?

[edit] getUserDetails declaration:

@NonNull
@GET("/user")
Observable<User> getUserDetails(@HeaderMap Map<String, String> headers);

@user8035311

I modified your github sample to trigger simulated data loading on button click, this way (button has been added to layout):

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        this.progressDialog = new ProgressDialog(this);
        this.progressDialog.setMessage("Loading...");

        final ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mainBinding.setLifecycleOwner(this);

        mainBinding.button2.setOnClickListener(view ->
        {
            RxViewModel.RxViewModelFactory factory = new RxViewModel.RxViewModelFactory();
            final RxViewModel model = ViewModelProviders.of(MainActivity.this, factory).get(RxViewModel.class);
            LiveData<User> liveData = model.getLiveData();
            progressDialog.show();
            liveData.observe(MainActivity.this, s ->
            {
                progressDialog.dismiss();
                mainBinding.setUser(s);
            });

        });
    }

But this does not work. Task is interrupted on screen rotation, with exception:

06-27 09:05:18.737 11829-11829/app.rxrotation.com.architecturecomponentsrxrotation E/WindowManager: android.view.WindowLeaked: Activity app.rxrotation.com.architecturecomponentsrxrotation.MainActivity has leaked window DecorView@5b16e38[] that was originally added here at android.view.ViewRootImpl.(ViewRootImpl.java:485) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:330) at app.rxrotation.com.architecturecomponentsrxrotation.MainActivity.lambda$onCreate$1$MainActivity(MainActivity.java:35) at app.rxrotation.com.architecturecomponentsrxrotation.MainActivity$$Lambda$0.onClick(Unknown Source:4) at android.view.View.performClick(View.java:6294) at android.view.View$PerformClick.run(View.java:24770) at android.os.Handler.handleCallback(Handler.java:790) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6494) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

After I click button second time, data loading not even executed.


Solution

  • You could use a ViewModel with LiveData to preserve your async tasks after config change. So, your LiveData could look like this:

    public class UserLiveData extends LiveData<User> {
        public UserLiveData() {
            setValue(new User());
        }
    
        public UserLiveData(Headers headers) {
            Observable<User> observable = Observable.defer(new Callable<ObservableSource<? extends User>>() {
                @Override
                public ObservableSource<? extends User> call() throws Exception {
                    User user = testService.getUserDetails(headers);
                    return Observable.just(user);
                }
            });
    
            observable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new DefaultObserver<String>() {
                    @Override
                    public void onNext(User value) {
                        setValue(value);
                    }
    
                    @Override
                    public void onError(Throwable e) {
                        throw new RuntimeException(e);
                    }
    
                    @Override
                    public void onComplete() {
    
                    }
            });
        }
    }
    

    Then, your UserDetailsViewModel is the following:

    public class UserDetailsViewModel extends ViewModel {
        private LiveData<User> userLiveData;
    
        public UserDetailsViewModel() {
            userLiveData = new UserLiveData();    
        }
    
        public void observeLiveDate(LifecycleOwner owner,
                                  Observer<User> observer) {
            userLiveData.observe(owner, observer);
        }
    
        public void loadLiveData(Headers headers) {
            userLiveData = new UserLiveData(someArgs);
        }
    }
    

    And then your MainActivity could be as following:

    public class MainActivity extends AppCompatActivity {
        private ProgressDialog progressDialog;
        private UserViewModel viewModel;
        private ActivityMainBinding mainBinding;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.progressDialog = new ProgressDialog(this);
            this.progressDialog.setMessage("Loading...");
    
            mainBinding = DataBindingUtil.setContentView(this,
                R.layout.activity_main);
            mainBinding.setLifecycleOwner(this);
    
            viewModel = ViewModelProviders.of(this).get(UserViewModel.class);
    
            observeLiveData();
            mainBinding.button2.setOnClickListener(v -> {
                viewModel.initLiveData(<your headers>);
                observeLiveData();
            });
        }
    
        private void observeLiveData() {
            progressDialog.show();
            progressDialog.setCanceledOnTouchOutside(false);
            viewModel.observeRxLiveDate(this, user -> {
                progressDialog.dismiss();
                mainBinding.setUser(user);
            });
        }
    }
    

    I created a very simple example here