Встречайте RxJava: недостающая библиотека реактивного программирования для Android

Опубликовано: 2022-03-11

Если вы разработчик Android, скорее всего, вы слышали о RxJava. Это одна из самых обсуждаемых библиотек для включения реактивного программирования в Android-разработке. Его рекламируют как основу для упрощения параллельных/асинхронных задач, присущих мобильному программированию.

Но… что такое RxJava и как он «упрощает» вещи?

Функциональное реактивное программирование для Android: введение в RxJava

Избавьте свой Android от слишком большого количества потоков Java с помощью RxJava.
Твитнуть

Хотя в Интернете уже доступно множество ресурсов, объясняющих, что такое RxJava, в этой статье моя цель — дать вам общее представление о RxJava и, в частности, о том, как он вписывается в разработку для Android. Я также приведу несколько конкретных примеров и предложений о том, как вы можете интегрировать его в новый или существующий проект.

Зачем рассматривать RxJava?

По своей сути RxJava упрощает разработку, поскольку повышает уровень абстракции многопоточности. То есть как разработчику вам не нужно слишком беспокоиться о деталях того, как выполнять операции, которые должны выполняться в разных потоках. Это особенно привлекательно, поскольку многопоточность сложно правильно реализовать, и, если она реализована неправильно, это может привести к возникновению некоторых из самых сложных ошибок для отладки и исправления.

Конечно, это не означает, что RxJava является пуленепробиваемым, когда речь идет о многопоточности, и по-прежнему важно понимать, что происходит за кулисами; однако RxJava определенно может облегчить вашу жизнь.

Давайте посмотрим на пример.

Сетевой вызов — RxJava против AsyncTask

Допустим, мы хотим получить данные по сети и в результате обновить пользовательский интерфейс. Один из способов сделать это — (1) создать внутренний подкласс AsyncTask в нашей Activity / Fragment , (2) выполнить сетевую операцию в фоновом режиме и (3) получить результат этой операции и обновить пользовательский интерфейс в основном потоке. .

 public class NetworkRequestTask extends AsyncTask<Void, Void, User> { private final int userId; public NetworkRequestTask(int userId) { this.userId = userId; } @Override protected User doInBackground(Void... params) { return networkService.getUser(userId); } @Override protected void onPostExecute(User user) { nameTextView.setText(user.getName()); // ...set other views } } private void onButtonClicked(Button button) { new NetworkRequestTask(123).execute() }

Каким бы безобидным это ни казалось, этот подход имеет некоторые проблемы и ограничения. А именно, утечки памяти/контекста легко создаются, поскольку NetworkRequestTask является внутренним классом и, таким образом, содержит неявную ссылку на внешний класс. Кроме того, что, если мы хотим связать еще одну длинную операцию после сетевого вызова? Нам пришлось бы вложить два AsyncTask , что может значительно снизить читабельность.

Напротив, подход RxJava к выполнению сетевого вызова может выглядеть примерно так:

 private Subscription subscription; private void onButtonClicked(Button button) { subscription = networkService.getObservableUser(123) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<User>() { @Override public void call(User user) { nameTextView.setText(user.getName()); // ... set other views } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); }

Используя этот подход, мы решаем проблему (потенциальных утечек памяти, вызванных работающим потоком, содержащим ссылку на внешний контекст), сохраняя ссылку на возвращаемый объект Subscription . Затем этот объект Subscription привязывается к методу #onDestroy() объекта Activity / Fragment , чтобы гарантировать, что операция Action1#call не будет выполняться, когда Activity / Fragment необходимо уничтожить.

Также обратите внимание, что возвращаемый тип #getObservableUser(...) (т.е. Observable<User> ) связан с дальнейшими вызовами. С помощью этого гибкого API мы можем решить вторую проблему использования AsyncTask , которая заключается в том, что он допускает дальнейшую цепочку сетевых вызовов/длинных операций. Довольно аккуратно, да?

Давайте углубимся в некоторые концепции RxJava.

Наблюдаемый, наблюдатель и оператор — 3 «О» RxJava Core

В мире RxJava все можно смоделировать как потоки. Поток испускает предметы с течением времени, и каждое испускание можно потреблять/наблюдать.

Если подумать, поток — не новая концепция: события кликов могут быть потоком, обновления местоположения могут быть потоком, push-уведомления могут быть потоком и так далее.

В мире RxJava все можно смоделировать как потоки.

Абстракция потока реализована через 3 основные конструкции, которые мне нравится называть «3 O»; а именно: О -наблюдаемый, О -наблюдатель и О -оператор. Observable испускает элементы (поток); и Наблюдатель потребляет эти предметы. Выбросы от объектов Observable можно дополнительно модифицировать, преобразовывать и манипулировать с помощью цепочки вызовов оператора .

Наблюдаемый

Observable — это абстракция потока в RxJava. Он похож на итератор тем, что при заданной последовательности он выполняет итерацию и создает эти элементы упорядоченным образом. Затем потребитель может потреблять эти элементы через один и тот же интерфейс, независимо от базовой последовательности.

Скажем, мы хотели выдать числа 1, 2, 3 именно в таком порядке. Для этого мы можем использовать метод Observable<T>#create(OnSubscribe<T>) .

 Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> subscriber) { subscriber.onNext(1); subscriber.onNext(2); subscriber.onNext(3); subscriber.onCompleted(); } });

Вызов subscriber.onNext(Integer) создает элемент в потоке, и, когда поток завершает передачу, вызывается subscriber.onCompleted() .

Этот подход к созданию Observable довольно многословен. По этой причине существуют удобные методы создания экземпляров Observable, которым следует отдавать предпочтение практически во всех случаях.

Самый простой способ создать Observable — использовать Observable#just(...) . Как следует из названия метода, он просто выдает элементы, которые вы передаете ему в качестве аргументов метода.

 Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively

Наблюдатель

Следующим компонентом потока Observable является подписанный на него наблюдатель (или наблюдатели). Наблюдатели уведомляются всякий раз, когда в потоке происходит что-то «интересное». Наблюдатели уведомляются посредством следующих событий:

  • Observer#onNext(T) — вызывается, когда элемент испускается из потока
  • Observable#onError(Throwable) — вызывается при возникновении ошибки в потоке
  • Observable#onCompleted() — вызывается, когда поток завершает передачу элементов.

Чтобы подписаться на поток, просто вызовите Observable<T>#subscribe(...) и передайте экземпляр Observer.

 Observable<Integer> observable = Observable.just(1, 2, 3); observable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { Log.d("Test", "In onCompleted()"); } @Override public void onError(Throwable e) { Log.d("Test", "In onError()"); } @Override public void onNext(Integer integer) { Log.d("Test", "In onNext():" + integer); } });

Приведенный выше код выдаст следующее в Logcat:

 In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()

Также могут быть случаи, когда нас больше не интересуют выбросы Observable. Это особенно актуально в Android, когда, например, необходимо восстановить Activity / Fragment в памяти.

Чтобы прекратить наблюдение за элементами, нам просто нужно вызвать Subscription#unsubscribe() для возвращаемого объекта Subscription.

 Subscription subscription = someInfiniteObservable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } }); // Call unsubscribe when appropriate subscription.unsubscribe();

Как видно из приведенного выше фрагмента кода, при подписке на Observable мы сохраняем ссылку на возвращаемый объект Subscription, а затем, когда это необходимо, вызываем функцию subscribe subscription#unsubscribe() . В Android это лучше всего вызывать в Activity#onDestroy() или Fragment#onDestroy() .

Оператор

Элементы, испускаемые Observable, могут быть преобразованы, изменены и отфильтрованы с помощью операторов до уведомления подписанных объектов Observer. Некоторые из наиболее распространенных операций функционального программирования (такие как отображение, фильтрация, уменьшение и т. д.) также могут быть применены к наблюдаемому потоку. Давайте посмотрим на карту в качестве примера:

 Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });

Приведенный выше фрагмент кода будет брать каждое излучение от Observable и умножать каждое на 3, создавая поток 3, 6, 9, 12, 15 соответственно. Применение оператора обычно возвращает в результате другой Observable, что удобно, поскольку позволяет нам связать несколько операций для получения желаемого результата.

Предположим, что для приведенного выше потока мы хотели получать только четные числа. Этого можно добиться, объединив операцию фильтра .

 Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).filter(new Func1<Integer, Boolean>() { @Override public Boolean call(Integer integer) { return integer % 2 == 0; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });

В набор инструментов RxJava встроено множество операторов, которые изменяют поток Observable; если вы можете придумать способ изменить поток, скорее всего, для этого есть оператор. В отличие от большей части технической документации, читать документы RxJava/ReactiveX довольно просто и по делу. Каждый оператор в документации сопровождается визуализацией того, как оператор влияет на поток. Эти визуализации называются «мраморными диаграммами».

Вот как гипотетический оператор под названием flip может быть смоделирован с помощью мраморной диаграммы:

Пример того, как гипотетический оператор под названием флип может быть смоделирован с помощью мраморной диаграммы.

Многопоточность с RxJava

Управление потоком, в котором выполняются операции в цепочке Observable, осуществляется путем указания планировщика, в котором должен выполняться оператор. По сути, вы можете думать о планировщике как о пуле потоков, который, если он указан, будет использоваться и выполняться оператором. По умолчанию, если такой планировщик не предоставлен, цепочка Observable будет работать в том же потоке, где Observable#subscribe(...) . В противном случае планировщик может быть указан через Observable#subscribeOn(Scheduler) и/или Observable#observeOn(Scheduler) , при этом запланированная операция будет выполняться в потоке, выбранном планировщиком.

Ключевое различие между этими двумя методами заключается в том, что Observable#subscribeOn(Scheduler) указывает исходному Observable, на каком планировщике он должен работать. Цепочка будет продолжать работать в потоке из планировщика, указанного в Observable#subscribeOn(Scheduler) , пока не будет сделан вызов Observable#observeOn(Scheduler) с другим планировщиком. Когда такой вызов сделан, все наблюдатели оттуда (т. е. последующие операции по цепочке) будут получать уведомления в потоке, observeOn из планировщикаObservOn.

Вот мраморная диаграмма, которая демонстрирует, как эти методы влияют на то, где выполняются операции:

Мраморная диаграмма, демонстрирующая, как эти методы влияют на выполнение операций.

В контексте Android, если операция пользовательского интерфейса должна выполняться в результате длительной операции, мы бы хотели, чтобы эта операция выполнялась в потоке пользовательского интерфейса. Для этой цели мы можем использовать AndroidScheduler#mainThread() , один из планировщиков, предоставляемых в библиотеке RxAndroid.

RxJava на Android

Теперь, когда у нас есть некоторые основы, вам может быть интересно — как лучше всего интегрировать RxJava в приложение для Android? Как вы можете себе представить, существует множество вариантов использования RxJava, но в этом примере давайте рассмотрим один конкретный случай: использование объектов Observable как части сетевого стека.

В этом примере мы рассмотрим Retrofit, HTTP-клиент с открытым исходным кодом Square, который имеет встроенные привязки с RxJava для взаимодействия с API GitHub. В частности, мы создадим простое приложение, которое представляет все помеченные звездочкой репозитории для пользователя, которому присвоено имя пользователя GitHub. Если вы хотите забежать вперед, исходный код доступен здесь.

Создать новый Android-проект

  • Начните с создания нового проекта Android и назовите его GitHubRxJava .

Снимок экрана: создание нового проекта Android

  • На экране « Целевые устройства Android » оставьте выбранными « Телефон» и «Планшет » и установите минимальный уровень SDK 17. Не стесняйтесь устанавливать более низкий или более высокий уровень API, но для этого примера уровня API 17 будет достаточно.

Снимок экрана: Экран целевых устройств Android

  • Выберите « Пустая активность» в следующем запросе.

Снимок экрана: добавление действия на экран мобильного устройства

  • На последнем шаге сохраните имя действия как MainActivity и сгенерируйте файл макета activity_main .

Снимок экрана: настройка экрана «Активность»

Настройка проекта

Включите RxJava, RxAndroid и библиотеку Retrofit в app/build.gradle . Обратите внимание, что включение RxAndroid неявно также включает RxJava. Однако рекомендуется всегда явно включать эти две библиотеки, поскольку RxAndroid не всегда содержит самую последнюю версию RxJava. Явное включение последней версии RxJava гарантирует использование самой последней версии.

 dependencies { compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxjava:1.1.8' // ...other dependencies }

Создать объект данных

Создайте класс объекта данных GitHubRepo . Этот класс инкапсулирует репозиторий в GitHub (сетевой ответ содержит больше данных, но нас интересует только его подмножество).

 public class GitHubRepo { public final int id; public final String name; public final String htmlUrl; public final String description; public final String language; public final int stargazersCount; public GitHubRepo(int id, String name, String htmlUrl, String description, String language, int stargazersCount) { this.id = id; this.name = name; this.htmlUrl = htmlUrl; this.description = description; this.language = language; this.stargazersCount = stargazersCount; } }

Настройка

  • Создайте интерфейс GitHubService . Мы передадим этот интерфейс в Retrofit, а Retrofit создаст реализацию GitHubService .
 public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
  • Создайте класс GitHubClient . Это будет объект, с которым мы будем взаимодействовать, чтобы совершать сетевые вызовы с уровня пользовательского интерфейса.

    • При создании реализации GitHubService с помощью Retrofit нам нужно передать RxJavaCallAdapterFactory в качестве адаптера вызова, чтобы сетевые вызовы могли возвращать объекты Observable (передача адаптера вызова необходима для любого сетевого вызова, который возвращает результат, отличный от Call ).

    • Нам также нужно передать GsonConverterFactory , чтобы мы могли использовать Gson как способ маршалинга объектов JSON в объекты Java.

 public class GitHubClient { private static final String GITHUB_BASE_URL = "https://api.github.com/"; private static GitHubClient instance; private GitHubService gitHubService; private GitHubClient() { final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); gitHubService = retrofit.create(GitHubService.class); } public static GitHubClient getInstance() { if (instance == null) { instance = new GitHubClient(); } return instance; } public Observable<List<GitHubRepo>> getStarredRepos(@NonNull String userName) { return gitHubService.getStarredRepositories(userName); } }

Макеты настройки

Затем создайте простой пользовательский интерфейс, который отображает полученные репозитории с заданным именем пользователя GitHub. Создайте activity_home.xml — макет для нашей активности — примерно так:

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android: android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android: android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/username"/> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/search"/> </LinearLayout> </LinearLayout>

Создайте item_github_repo.xml — макет элемента ListView для объекта репозитория GitHub — примерно так:

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp"> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:text tools:text="Cropper"/> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:lines="2" android:ellipsize="end" android:textSize="16sp" android:layout_below="@+id/text_repo_name" tools:text="Android widget for cropping and rotating an image."/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentLeft="true" android:textColor="?attr/colorPrimary" android:textSize="14sp" android:text tools:text="Language: Java"/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentRight="true" android:textColor="?attr/colorAccent" android:textSize="14sp" android:text tools:text="Stars: 1953"/> </RelativeLayout>

Склеить все вместе

Создайте ListAdapter , отвечающий за привязку объектов GitHubRepo к ListView . Процесс по существу включает в себя item_github_repo.xml в View , если не предоставлено переработанное View ; в противном случае переработанное View используется повторно, чтобы предотвратить чрезмерное наполнение слишком большого количества объектов View .

 public class GitHubRepoAdapter extends BaseAdapter { private List<GitHubRepo> gitHubRepos = new ArrayList<>(); @Override public int getCount() { return gitHubRepos.size(); } @Override public GitHubRepo getItem(int position) { if (position < 0 || position >= gitHubRepos.size()) { return null; } else { return gitHubRepos.get(position); } } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final View view = (convertView != null ? convertView : createView(parent)); final GitHubRepoViewHolder viewHolder = (GitHubRepoViewHolder) view.getTag(); viewHolder.setGitHubRepo(getItem(position)); return view; } public void setGitHubRepos(@Nullable List<GitHubRepo> repos) { if (repos == null) { return; } gitHubRepos.clear(); gitHubRepos.addAll(repos); notifyDataSetChanged(); } private View createView(ViewGroup parent) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View view = inflater.inflate(R.layout.item_github_repo, parent, false); final GitHubRepoViewHolder viewHolder = new GitHubRepoViewHolder(view); view.setTag(viewHolder); return view; } private static class GitHubRepoViewHolder { private TextView textRepoName; private TextView textRepoDescription; private TextView textLanguage; private TextView textStars; public GitHubRepoViewHolder(View view) { textRepoName = (TextView) view.findViewById(R.id.text_repo_name); textRepoDescription = (TextView) view.findViewById(R.id.text_repo_description); textLanguage = (TextView) view.findViewById(R.id.text_language); textStars = (TextView) view.findViewById(R.id.text_stars); } public void setGitHubRepo(GitHubRepo gitHubRepo) { textRepoName.setText(gitHubRepo.name); textRepoDescription.setText(gitHubRepo.description); textLanguage.setText("Language: " + gitHubRepo.language); textStars.setText("Stars: " + gitHubRepo.stargazersCount); } } }

Склейте все вместе в MainActivity . По сути, это Activity , которое отображается при первом запуске приложения. Здесь мы просим пользователя ввести свое имя пользователя GitHub и, наконец, отображаем все отмеченные звездочкой репозитории под этим именем пользователя.

 public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GitHubRepoAdapter adapter = new GitHubRepoAdapter(); private Subscription subscription; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ListView listView = (ListView) findViewById(R.id.list_view_repos); listView.setAdapter(adapter); final EditText editTextUsername = (EditText) findViewById(R.id.edit_text_username); final Button buttonSearch = (Button) findViewById(R.id.button_search); buttonSearch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String username = editTextUsername.getText().toString(); if (!TextUtils.isEmpty(username)) { getStarredRepos(username); } } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); } private void getStarredRepos(String username) { subscription = GitHubClient.getInstance() .getStarredRepos(username) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<List<GitHubRepo>>() { @Override public void onCompleted() { Log.d(TAG, "In onCompleted()"); } @Override public void onError(Throwable e) { e.printStackTrace(); Log.d(TAG, "In onError()"); } @Override public void onNext(List<GitHubRepo> gitHubRepos) { Log.d(TAG, "In onNext()"); adapter.setGitHubRepos(gitHubRepos); } }); } }

Запустите приложение

Запуск приложения должен представить экран с полем ввода для ввода имени пользователя GitHub. Затем поиск должен представить список всех помеченных репозиториев.

Снимок экрана приложения со списком всех помеченных репозиториев.

Заключение

Я надеюсь, что это послужит полезным введением в RxJava и обзором его основных возможностей. В RxJava есть множество мощных концепций, и я призываю вас изучить их, углубившись в хорошо документированную вики RxJava.

Не стесняйтесь оставлять любые вопросы или комментарии в поле для комментариев ниже. Вы также можете следить за мной в Твиттере по адресу @arriolachris, где я много пишу о RxJava и обо всем, что связано с Android.

Если вам нужен всеобъемлющий учебный ресурс по RxJava, вы можете ознакомиться с электронной книгой, которую я написал с Ангусом Хуангом, на Leanpub.

Связанный: Десять функций Kotlin для ускорения разработки Android