Room. RxJava

У цьому уроці розглянемо можливість спільного використання RxJava і Room. Як отримувати дані у Flowable, Single і Maybe.

У build.gradle модуля додавайте dependencies

implementation "android.arch.persistence.room:rxjava2:1.0.0"

Flowable

У Dao вказуємо для методу вихідний тип Flowable

@Query("SELECT * FROM employee")
Flowable<List<Employee>> getAll();

У коді підписуємося й отримуємо дані

db.employeeDao().getAll()
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(new Consumer<List<Employee>>() {
           @Override
           public void accept(List<Employee> employees) throws Exception {
               // ...
           }
       });

subscribeOn у випадку з Flowable не потрібен. Запит до бази буде виконано не в UI потоці. А ось, щоб результат прийшов у UI потік, використовуємо observeOn

Тепер при будь-якій зміні даних у базі, ми будемо отримувати свіжі дані в методі accept. І нам не треба буде щоразу їх знову запитувати самим.

Якщо під час запиту декількох записів, замість Flowable<List<Employee>> використовувати Flowable<Employee>:

@Query("SELECT * FROM employee")
Flowable<Employee> getAll();

то ми отримаємо тільки перший запис з усього результату

Якщо ж ми складаємо запит для отримання тільки одного запису, то Flowable<Employee> цілком підійде. Давайте розглянемо цей приклад докладніше.

Метод у Dao

@Query("SELECT * FROM employee WHERE id = :id")
Flowable<Employee> getById(long id);

У коді підписуємося на Flowable

db.employeeDao().getById(1)
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(new Consumer<Employee>() {
           @Override
           public void accept(Employee employee) throws Exception {
               // ...
           }
       });

Отже, ми запитуємо з бази запис за id. І тут можливі варіанти.

Якщо запис є в базі, то він прийде в accept відразу ж після підписки. І під час кожного наступного оновлення цього запису в базі даних, він також приходитиме в accept.

Якщо запису немає, то відразу після підписки нічого не прийде. А ось якщо він пізніше з'явиться, то він прийде в accept.

У вищеописаного прикладу є мінус. Якщо запису немає в базі, то Flowable взагалі нічого нам не надішле. Тобто це буде виглядати так, ніби він все ще виконує запит.

Це можна виправити таким чином:

@Query("SELECT * FROM employee WHERE id = :id")
Flowable<List<Employee>> getById(long id);

Хоч ми й очікуємо всього один запис, але використовуємо не Flowable<Employee>, а Flowable<List<Employee>>. І якщо запису немає, то ми хоча б отримаємо порожній аркуш замість повної тиші.

Single

Розглянемо той самий приклад із запитом одного запису, але з використанням Single. Нагадаю, що в Single може прийти тільки один onNext, або OnError. Після цього Single вважається завершеним.

Метод в Dao

@Query("SELECT * FROM employee WHERE id = :id")
Single<Employee> getById(long id);

У коді підписуємося

db.employeeDao().getById(1)
       .subscribeOn(Schedulers.io())
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(new DisposableSingleObserver<Employee>() {
           @Override
           public void onSuccess(Employee employee) {
               // ...
           }
 
           @Override
           public void onError(Throwable e) {
               // ...
           }
       });

На відміну від Flowable, з Single необхідно використовувати onSubscribe, щоб задати потік для виконання запиту. Інакше в onError прийде помилка: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Знову розглядаємо варіанти наявності потрібного запису в базі.

Якщо такий запис у базі є, то він прийде в onSuccess. Після цього Single вважатиметься завершеним і під час наступних оновлень цього запису нічого приходити вже не буде.

Якщо такого запису в базі немає, то ми в onError отримаємо помилку: android.arch.persistence.room.EmptyResultSetException: Query returned empty result set: SELECT * FROM employee WHERE id = ?. Після цього Single вважатиметься завершеним, і навіть якщо такий запис з'явиться в базі, то нам нічого приходити вже не буде.

Maybe

Розглянемо той самий приклад із запитом одного запису, але з використанням Maybe. Нагадаю, що в Maybe може прийти або один onNext, або onComplete, або OnError. Після цього Maybe вважається завершеним.

Метод в Dao

@Query("SELECT * FROM employee WHERE id = :id")
Maybe<Employee> getById(long id);

У коді підписуємося

db.employeeDao().getById(1)
       .subscribeOn(Schedulers.io())
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(new DisposableMaybeObserver<Employee>() {
           @Override
           public void onSuccess(Employee employee) {
               // ...
           }
 
           @Override
           public void onError(Throwable e) {
               // ...
           }
 
           @Override
           public void onComplete() {
               // ...
           }
       });

З Maybe також необхідно використовувати onSubscribe, щоб задати потік для виконання запиту.

Розглядаємо варіанти наявності необхідного запису в базі.

Якщо такий запис у базі є, то він прийде в onSuccess. Після цього Maybe вважатиметься завершеним і під час наступних оновлень цього запису нічого приходити вже не буде.

Якщо такого запису в базі немає, то ми отримаємо onComplete. Після цього Maybe вважатиметься завершеним, і навіть якщо такий запис з'явиться в базі, то нам нічого приходити вже не буде.

У якому випадку що краще використовувати?

Flowable підходить, якщо ви запитуєте дані і далі плануєте автоматично отримувати їх оновлення.

Single і Maybe підходять для одноразового отримання даних. Різниця між ними в тому, що Single логічніше використовувати, якщо запис має бути в базі. Якщо його немає, вам прийде помилка. А Maybe допускає, що запису може і не бути.

Приклад

Крок 1: Залежності

Додайте необхідні залежності в build.gradle:

dependencies {
    implementation 'androidx.room:room-runtime:2.5.0'
    implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
    implementation 'androidx.room:room-ktx:2.5.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
    annotationProcessor 'androidx.room:room-compiler:2.5.0' // або kapt, якщо використовуєте Kotlin
}

Крок 2: Створення сутності (Entity)

@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    public int id;

    @ColumnInfo(name = "name")
    public String name;

    @ColumnInfo(name = "email")
    public String email;

    // Конструктори, геттери та сеттери
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

Крок 3: Створення DAO

@Dao
public interface UserDao {

    // Використання Flowable для автоматичної підписки на зміни
    @Insert
    Completable insert(User user);

    @Query("SELECT * FROM users")
    Flowable<List<User>> getAllUsers();

    // Використання Single для отримання одного елемента
    @Query("SELECT * FROM users WHERE id = :userId LIMIT 1")
    Single<User> getUserById(int userId);
}

Крок 4: Репозиторій

public class UserRepository {

    private final UserDao userDao;

    public UserRepository(Application application) {
        UserDatabase db = UserDatabase.getDatabase(application);
        this.userDao = db.userDao();
    }

    // Отримання всіх користувачів як Flowable
    public Flowable<List<User>> getAllUsers() {
        return userDao.getAllUsers()
                .subscribeOn(Schedulers.io()) // Підписка на IO потоці
                .observeOn(AndroidSchedulers.mainThread()); // Оновлення UI на головному потоці
    }

    // Отримання користувача за id як Single
    public Single<User> getUserById(int id) {
        return userDao.getUserById(id)
                .subscribeOn(Schedulers.io()) 
                .observeOn(AndroidSchedulers.mainThread());
    }

    // Вставка користувача
    public Completable insertUser(User user) {
        return userDao.insert(user)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

Крок 5: ViewModel

public class UserViewModel extends AndroidViewModel {

    private final UserRepository repository;
    private final LiveData<List<User>> allUsers;

    public UserViewModel(Application application) {
        super(application);
        repository = new UserRepository(application);
        allUsers = new LiveData<List<User>>() {
            @Override
            protected void onActive() {
                super.onActive();
                repository.getAllUsers().subscribe(new Observer<List<User>>() {
                    @Override
                    public void onChanged(List<User> users) {
                        setValue(users); // оновлення LiveData з отриманими даними
                    }
                });
            }
        };
    }

    public LiveData<List<User>> getAllUsers() {
        return allUsers;
    }

    public void addUser(User user) {
        repository.insertUser(user).subscribe();
    }
}

Крок 6: Інтеграція з UI (Activity)

public class UserActivity extends AppCompatActivity {

    private UserViewModel userViewModel;

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

        // ініціалізація ViewModel
        userViewModel = new ViewModelProvider(this).get(UserViewModel.class);

        // Підписка на LiveData
        userViewModel.getAllUsers().observe(this, users -> {
            // Оновлення UI при зміні даних
            // Наприклад, оновлення RecyclerView
        });

        // Додавання користувача через ViewModel
        User newUser = new User("John Doe", "john.doe@example.com");
        userViewModel.addUser(newUser);
    }
}

Пояснення:

  1. Dao:
    • Використовуємо Flowable для отримання списку користувачів, щоб автоматично оновлювати UI при змінах.
    • Використовуємо Single для запиту одного елемента.
    • Використовуємо Completable для операцій, що не повертають результат, але можуть завершитись з помилкою (вставка).
  2. Репозиторій:
    • Зберігає доступ до Dao і надає методи для взаємодії з даними. Використовуємо RxJava для асинхронних операцій.
  3. ViewModel:
    • Отримує дані через репозиторій та підписується на зміни даних через RxJava (через LiveData).
  4. UI:
    • Використовуємо LiveData для оновлення UI при зміні даних.

Переваги:

  • RxJava дозволяє легко працювати з асинхронними операціями.
  • Flowable автоматично оновлює UI при зміні даних у базі.
  • Single для отримання одного результату.

Документація

https://developer.android.com/training/data-storage/room/async-queries#java