architecture-components-samples icon indicating copy to clipboard operation
architecture-components-samples copied to clipboard

BasicRxJavaSample updateUserName and configuration changes

Open feresr opened this issue 8 years ago • 5 comments

On a real application, a method like updateUserName() might perform a long running operation (network call / db query) This example is clearing the compositeDisposable on onStop(), so this long operation would interrupted upon config changes.

https://github.com/googlesamples/android-architecture-components/blob/master/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserActivity.java

How can I work around this issue? I'm looking into the Completable::cache() operator and the possibility of using subjects but I'd like to hear your take on this.

Thanks :)

feresr avatar Jul 24 '17 22:07 feresr

Also, the state of the button modified when updateUserName() is executed (mUpdateButton.setEnabled(false)) should be saved automatically in the Android Bundle and after rotation the button would be disabled and the user can't click on it anymore.

How would you solve this updateUserName and configuration changes problem?

See: https://github.com/googlesamples/android-architecture-components/blob/master/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserActivity.java#L109

sebaslogen avatar Aug 10 '17 17:08 sebaslogen

@feresr @sebaslogen A subject based solution would fix this problem.

Changes needed in UserActivity.java

    @Override
    protected void onStart() {
        super.onStart();
        
        mDisposable.add(mViewModel.getUserSubscription()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String userName) throws Exception {
                        mUserName.setText(userName);
                    }
                }));

        mDisposable.add(mViewModel.getSaveButtonStateSubscription()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean saveButtonStatus) throws Exception {
                        mUpdateButton.setEnabled(saveButtonStatus);
                    }
                }));

        mDisposable.add(mViewModel.getErrorSubscription()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String errorMessage) throws Exception {
                        Log.e(TAG, errorMessage);
                    }
                }));
    }

    @Override
    protected void onStop() {
        mDisposable.clear();
        super.onStop();
    }

    private void updateUserName() {
        String userName = mUserNameInput.getText().toString();
        mViewModel.updateUserName(userName);
    }

Changes need in UserViewModel.java

    private CompositeDisposable compositeDisposable;
    private BehaviorSubject<String> userSubject = BehaviorSubject.create();
    private BehaviorSubject<String> errorSubject = BehaviorSubject.create();
    private BehaviorSubject<Boolean> saveButtonStateSubject = BehaviorSubject.create();

    public Observable<String> getUserSubscription() {
        return userSubject.serialize();
    }

    public Observable<String> getErrorSubscription() {
        return errorSubject.serialize();
    }

    public Observable<Boolean> getSaveButtonStateSubscription() {
        return saveButtonStateSubject.serialize();
    }

    public UserViewModel(UserDataSource dataSource) {
        mDataSource = dataSource;

        compositeDisposable = new CompositeDisposable();
        userSubject = BehaviorSubject.create();
        saveButtonStateSubject = BehaviorSubject.create();

        getUserName();
    }

    public void getUserName() {
        compositeDisposable.add(mDataSource.getUser()
                .map(new Function<User, String>() {
                    @Override
                    public String apply(User user) throws Exception {
                        mUser = user;
                        return user.getUserName();
                    }
                })
                .delay(3, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.computation())
                .observeOn(Schedulers.computation())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String userName) throws Exception {
                        userSubject.onNext(userName);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        errorSubject.onNext("Unable to update username");
                    }
                }));
    }

    public void updateUserName(final String userName) {
        compositeDisposable.add(new CompletableFromAction(new Action() {
            @Override
            public void run() throws Exception {
                mUser = mUser == null
                        ? new User(userName)
                        : new User(mUser.getId(), userName);

                mDataSource.insertOrUpdateUser(mUser);
            }
        })
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(@NonNull Disposable disposable) throws Exception {
                        saveButtonStateSubject.onNext(false);
                    }
                })
                .delay(3, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.computation())
                .observeOn(Schedulers.computation())
                .subscribe(new Action() {
                    @Override
                    public void run() throws Exception {
                        saveButtonStateSubject.onNext(true);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        errorSubject.onNext("Unable to update username");
                        saveButtonStateSubject.onNext(true);
                    }
                }));
    }

    @Override
    protected void onCleared() {
        compositeDisposable.dispose();
        super.onCleared();
    }

viraj49 avatar Aug 15 '17 09:08 viraj49

@viraj49 instead of Subjects I would recommend to use Relays https://github.com/JakeWharton/RxRelay/

sebaslogen avatar Aug 15 '17 09:08 sebaslogen

@sebaslogen Sure, RxRelay would be a good choice here, I just wanted to share an implementation with minimal changes in the existing BasicRxJavaSample code, so that it's easy to relate.

For production apps, based on your needs, you can choose the library of your choice even subject of your choice, or a completely different implementation, the key concept here is that dataSubscriptions are detached from viewSubscriptions.

viraj49 avatar Aug 15 '17 09:08 viraj49

Another implementation using LiveData would look like this. Which can be improvised by using a "state model/data class". Was playing with different options and this one seems safer & shorter solution in comparison with subject based implementation.

UserActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_user);

    mUserName = (TextView) findViewById(R.id.user_name);
    mUserNameInput = (EditText) findViewById(R.id.user_name_input);
    mUpdateButton = (Button) findViewById(R.id.update_user);

    ViewModelFactory mViewModelFactory = Injection.provideViewModelFactory(this);
    mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(UserViewModel.class);
    mUpdateButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            updateUserName();
        }
    });

    mViewModel.getUserLiveData().observe(this, new Observer<String>() {
        @Override
        public void onChanged(String userName) {
            mUserName.setText(userName);
        }
    });

    mViewModel.getErrorLiveData().observe(this, new Observer<String>() {
        @Override
        public void onChanged(String errorMessage) {
            Log.e(TAG, errorMessage);
        }
    });

    mViewModel.getSaveButtonStateLiveData().observe(this, new Observer<Boolean>() {
        @Override
        public void onChanged(Boolean saveButtonStatus) {
            mUpdateButton.setEnabled(saveButtonStatus);
        }
    });
}

UserViewModel.java

private CompositeDisposable compositeDisposable;
private MutableLiveData<String> userLiveData = new MutableLiveData<>();
private MutableLiveData<String> errorLiveData = new MutableLiveData<>();
private MutableLiveData<Boolean> saveButtonStateLiveData = new MutableLiveData<>();

public UserViewModel(UserDataSource dataSource) {
    mDataSource = dataSource;
    compositeDisposable = new CompositeDisposable();
    getUserName();
}

public LiveData<String> getUserLiveData() {
    return userLiveData;
}

public LiveData<String> getErrorLiveData() {
    return errorLiveData;
}

public LiveData<Boolean> getSaveButtonStateLiveData() {
    return saveButtonStateLiveData;
}

public void getUserName() {
    compositeDisposable.add(mDataSource.getUser()
            .map(new Function<User, String>() {
                @Override
                public String apply(User user) throws Exception {
                    mUser = user;
                    return user.getUserName();
                }
            })
            .delay(3, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.computation())
            .observeOn(Schedulers.computation())
            .subscribe(new Consumer<String>() {
                @Override
                public void accept(String userName) throws Exception {
                    userLiveData.postValue(userName);
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    errorLiveData.postValue("Unable to update username");
                }
            }));
}

public void updateUserName(final String userName) {
    compositeDisposable.add(new CompletableFromAction(new Action() {
        @Override
        public void run() throws Exception {
            mUser = mUser == null
                    ? new User(userName)
                    : new User(mUser.getId(), userName);

            mDataSource.insertOrUpdateUser(mUser);
        }
    })
            .delay(3, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.computation())
            .observeOn(Schedulers.computation())
            .doOnSubscribe(new Consumer<Disposable>() {
                @Override
                public void accept(@NonNull Disposable disposable) throws Exception {
                    saveButtonStateLiveData.postValue(false);
                }
            })
            .subscribe(new Action() {
                @Override
                public void run() throws Exception {
                    saveButtonStateLiveData.postValue(true);
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    errorLiveData.postValue("Unable to update username");
                    saveButtonStateLiveData.postValue(true);
                }
            }));
}

@Override
protected void onCleared() {
    compositeDisposable.dispose();
    super.onCleared();
}

viraj49 avatar Aug 17 '17 09:08 viraj49