Home » Android » android – Network Awareness from Repository using RxJava

android – Network Awareness from Repository using RxJava

Posted by: admin May 14, 2020 Leave a comment

Questions:

I am having trouble to find a proper solution to make view/viewmodel aware of network status, specially using RxJava.

I tried to follow Google’s NetworkBoundResource and Iammert’s networkBoundResouce (also tried GithubBrowserSample
but I just don’t really understand the code or how it should be encapsulated.

I’m looking for how can I implement NetworkBoundResource with RxJava2 in a way that the data gets refreshed automatically with the Database.

I will show what I currently have:

A fragment with arch.lifecycle.ViewModel

public class LoginViewModel extends ViewModel {

    static final int SCREEN_JUST_BUTTONS = 0;
    static final int SCREEN_LOGIN_INPUT = 1;
    static final int SCREEN_PASS_INPUT = 2;

    // using a PublishSubject because we are not interested in the last object that was emitted
    // before subscribing. Like this we avoid displaying the snackbar multiple times
    @NonNull
    private final PublishSubject<Integer> snackbarText;

    private int SCREEN = 0;

    private LoginUiModel loginUiModel;
    @NonNull
    private BaseSchedulerProvider schedulerProvider;
    @NonNull
    private UserRepository userRepository;

    @Inject
    LoginViewModel(@NonNull final BaseSchedulerProvider schedulerProvider, @NonNull final UserRepository userRepository) {
        this.schedulerProvider = schedulerProvider;
        this.userRepository = userRepository;
        snackbarText = PublishSubject.create();
        disposables = new CompositeDisposable();
        loginUiModel = new LoginUiModel();
    }

    int getScreen() {
        return SCREEN;
    }

    void setScreen(final int screen) {
        this.SCREEN = screen;
    }

    @NonNull
    Observable<Integer> getSnackbarText() {
        return snackbarText;
    }

    LoginUiModel getLoginUiModel() {
        return loginUiModel;
    }


    public Single<User> login(final String login, final String password) {
        return userRepository.login(login, password)
                             .subscribeOn(schedulerProvider.io())
                             .observeOn(schedulerProvider.ui())
                             .doOnError(this::handleErrorLogin);
    }

    private void handleErrorLogin(final Throwable t) {
        if (t instanceof HttpException) {
            showSnackbar(R.string.Login_invalid_input);
        } else if (t instanceof IOException) {
            showSnackbar(R.string.Common_no_connection);
        } else {
            showSnackbar(R.string.Common_error_loading);
        }
    }

    private void showSnackbar(@StringRes int textId) {
        snackbarText.onNext(textId);
    }

    void forgotPassword() {
        if (isValidEmail(loginUiModel.getEmail())) {
            showSnackbar(R.string.Login_password_sent);
        } else {
            showSnackbar(R.string.Login_invalid_login);
        }
        //TODO;
    }

    boolean isValidEmail(@Nullable final String text) {
        return text != null && text.trim().length() > 3 && text.contains("@");
    }
}

and in my UserRepository:

@Singleton
public class UserRepository {

    @NonNull
    private final UserDataSource userRemoteDataSource;

    @NonNull
    private final UserDataSource userLocalDataSource;

    @NonNull
    private final RxSharedPreferences preferences;

    @Inject
    UserRepository(@NonNull RxSharedPreferences rxSharedPreferences, @NonNull @Remote UserDataSource userRemoteDataSource, @NonNull @Local UserDataSource
            userLocalDataSource) {
        this.userRemoteDataSource = userRemoteDataSource;
        this.userLocalDataSource = userLocalDataSource;
        preferences = rxSharedPreferences;
    }

    @NonNull
    public Single<User> login(final String email, final String password) {
        return userRemoteDataSource
                .login(email, password) // busca do webservice
                .flatMap(userLocalDataSource::saveUser) // salva no banco local
                .flatMap(user -> userLocalDataSource.login(user.getEmail(), user.getPassword())) // busca usuário no banco local como SSOT
                .doOnSuccess(this::saveUserId); // salva o ID do usuário nas preferências.
    }

    public Single<User> saveUser(String email, String password, String name, String phone, String company) {
        User user = new User(0, name, email, password, phone, company);

        return saveUser(user);
    }

    private Single<User> saveUser(final User user) {
        return userRemoteDataSource.saveUser(user)
                                   .flatMap(userLocalDataSource::saveUser)
                                   .doOnSuccess(this::saveUserId); // salva o ID do usuário nas preferências.
    }

    private void saveUserId(final User user) {
        preferences.getLong(Consts.KEY_USER_ID__long).set(user.getUserId());
    }

    public Flowable<User> getUserUsingSystemPreferences() {
        Preference<Long> userIdPref = preferences.getLong(Consts.KEY_USER_ID__long, -1L);
        if (userIdPref.get() <= 0) {
            //nao deveria acontecer, mas se acontecer, reseta:
            userIdPref.delete();
            return Flowable.error(new RuntimeException("Not logged in."));
        } else if (!userIdPref.isSet()) {
            return Flowable.error(new RuntimeException("Not logged in."));
        } else {
            return userLocalDataSource.getUser(String.valueOf(userIdPref.get()));
            }
        }
    }

and my UserLocalDataSource:

@Singleton
public class UserLocalDataSource implements UserDataSource {

    private final UserDao userDao;

    @Inject
    UserLocalDataSource(UserDao userDao) {
        this.userDao = get(userDao);
    }

    @NonNull
    @Override
    public Single<User> login(final String email, final String password) {
        return userDao.login(email, password);
    }

    @Override
    public Flowable<User> getUser(final String id) {
        return userDao.getUserById(Long.valueOf(id));
    }

    @Override
    public Single<User> saveUser(final User user) {
        return Single.create(e -> {
            long id = userDao.insert(user);
            user.setUserId(id);
            e.onSuccess(user);
        });
    }

    @Override
    public Completable deleteUser(final String id) {
        return getUser(id).flatMapCompletable(user -> Completable.create(e -> {
            int numberOfDeletedRows = userDao.delete(user);
            if (numberOfDeletedRows > 0) {
                e.onComplete();
            } else {
                e.onError(new Exception("Tried to delete a non existing user"));
            }
        }));
    }
}

and the user remote data source:

public class UserRemoteDataSource implements UserDataSource {

    private final ISApiService apiService;

    @Inject
    UserRemoteDataSource(ISApiService apiService) {
        this.apiService = get(apiService);
    }

    @NonNull
    @Override
    public Single<User> login(final String email, final String password) {
        return apiService.login(email, password);
    }

    @Override
    public Flowable<User> getUser(final String id) {
        throw new RuntimeException("NO getUser endpoint");
    }

    @Override
    public Single<User> saveUser(final User user) {
        Map<String, String> params = new HashMap<>();

        params.put(ISApiConsts.EMAIL, user.getEmail());
        params.put(ISApiConsts.NAME, user.getName());
        params.put(ISApiConsts.PASSWORD, user.getPassword());
        params.put(ISApiConsts.PHONE, user.getPhone());
        params.put(ISApiConsts.COMPANY, user.getCompany());

        return apiService.signUp(params);
    }

    @Override
    public Completable deleteUser(final String id) {
        //can't delete user.
        return Completable.complete();
    }
}

UserDao interface ( It is using Room)

@Dao
public interface UserDao extends BaseDao<User> {

    @Query("SELECT * FROM user WHERE user_id = :id")
    Flowable<User> getUserById(long id);

    @Query("SELECT * FROM user WHERE email = :login AND password = :password")
    Single<User> login(String login, String password);

}

What I want to know exactly is: How can I encapsulate my domain while still get access to possible problems like network and show it to the user using RxJava in the MVVM pattern?

How to&Answers:

first of all let me copy past, the fetch from network method from the Github browser sample.

private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
    LiveData<ApiResponse<RequestType>> apiResponse = createCall();
    // we re-attach dbSource as a new source, it will dispatch its latest value quickly
    result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
    result.addSource(apiResponse, response -> {
        result.removeSource(apiResponse);
        result.removeSource(dbSource);
        //noinspection ConstantConditions
        if (response.isSuccessful()) {
            appExecutors.diskIO().execute(() -> {
                saveCallResult(processResponse(response));
                appExecutors.mainThread().execute(() ->
                        // we specially request a new live data,
                        // otherwise we will get immediately last cached value,
                        // which may not be updated with latest results received from network.
                        result.addSource(loadFromDb(),
                                newData -> setValue(Resource.success(newData)))
                );
            });
        } else {
            onFetchFailed();
            result.addSource(dbSource,
                    newData -> setValue(Resource.error(response.errorMessage, newData)));
        }
    });
}

what I understand from this snippet that, you really don’t monitor the network state, but the app just try to hit the Server, when there is a successful response it posted to the UI with a state of “Success”, if it fails to get the Data from Server it trigger the onFetchFailed() method, and set the database data as the retrieved data with state of “fail”.

I think you need the state if you decided to show the cached data with a fail message of getting fresh ones.

there is many other abstract methods in the NetworkBoundResource class, so you can choose the right actions when you don’t get a fresh data.

Speaking of Rxjava, I’m working on a fork from iammert repo to use Rxjava I’ll share the link here once I finished it.