Когда следует использовать RxJava Observable и когда просто обратный вызов на Android?

Я работаю над сетью для своего приложения. Поэтому я решил попробовать Square Retrofit. Я вижу, что они поддерживают простой Callback

@GET("/user/{id}/photo")
void getUserPhoto(@Path("id") int id, Callback<Photo> cb);

и RxJava Observable

@GET("/user/{id}/photo")
Observable<Photo> getUserPhoto(@Path("id") int id);

Оба выглядят довольно похожими на первый взгляд, но когда он добирается до реализации, он становится интересным...

Пока простая реализация обратного вызова будет выглядеть примерно так:

api.getUserPhoto(photoId, new Callback<Photo>() {
    @Override
    public void onSuccess() {
    }
});

что довольно просто и просто. И с Observable он быстро становится многословным и довольно сложным.

public Observable<Photo> getUserPhoto(final int photoId) {
    return Observable.create(new Observable.OnSubscribeFunc<Photo>() {
        @Override
        public Subscription onSubscribe(Observer<? super Photo> observer) {
            try {
                observer.onNext(api.getUserPhoto(photoId));
                observer.onCompleted();
            } catch (Exception e) {
                observer.onError(e);
            }

            return Subscriptions.empty();
        }
    }).subscribeOn(Schedulers.threadPoolForIO());
}

И это не так. Вам все равно нужно сделать что-то вроде этого:

Observable.from(photoIdArray)
        .mapMany(new Func1<String, Observable<Photo>>() {
            @Override
            public Observable<Photo> call(Integer s) {
                return getUserPhoto(s);
            }
        })
        .subscribeOn(Schedulers.threadPoolForIO())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Photo>() {
            @Override
            public void call(Photo photo) {
                //save photo?
            }
        });

Я что-то упустил? Или это неправильный случай для использования Observable s? Когда нужно/следует предпочесть Observable по простому обратному вызову?

Update

Использование модификации намного проще, чем пример выше, как @Niels показал в своем ответе или в примере проекта Джейка Уортона U2020. Но в сущности вопрос остается прежним - когда нужно использовать один или другой способ?

Ответ 1

Для простых сетевых вещей преимущества RxJava над Callback очень ограничены. Простой пример getUserPhoto:

RxJava:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
            @Override
            public void call(Photo photo) {
               // do some stuff with your photo 
            }
     });

Перезвоните:

api.getUserPhoto(photoId, new Callback<Photo>() {
    @Override
    public void onSuccess(Photo photo, Response response) {
    }
});

Вариант RxJava не намного лучше, чем вариант Callback. А пока давайте проигнорируем обработку ошибок. Давайте возьмем список фотографий:

RxJava:

api.getUserPhotos(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<List<Photo>, Observable<Photo>>() {
    @Override
    public Observable<Photo> call(List<Photo> photos) {
         return Observable.from(photos);
    }
})
.filter(new Func1<Photo, Boolean>() {
    @Override
    public Boolean call(Photo photo) {
         return photo.isPNG();
    }
})
.subscribe(
    new Action1<Photo>() {
    @Override
        public void call(Photo photo) {
            list.add(photo)
        }
    });

Перезвоните:

api.getUserPhotos(userId, new Callback<List<Photo>>() {
    @Override
    public void onSuccess(List<Photo> photos, Response response) {
        List<Photo> filteredPhotos = new ArrayList<Photo>();
        for(Photo photo: photos) {
            if(photo.isPNG()) {
                filteredList.add(photo);
            }
        }
    }
});

Теперь вариант RxJava по-прежнему не меньше, хотя с Lambdas он будет ближе к варианту Callback. Кроме того, если у вас есть доступ к каналу JSON, было бы странно получать все фотографии, когда вы только отображаете PNG. Просто настройте подачу, чтобы на ней отображались только PNG.

Первый вывод

Это не делает вашу кодовую базу меньше, когда вы загружаете простой JSON, который вы подготовили в нужном формате.

Теперь давайте сделаем вещи немного интереснее. Допустим, вы хотите не только получить userPhoto, но у вас есть клон Instagram, и вы хотите получить 2 JSON: 1. getUserDetails() 2. getUserPhotos()

Вы хотите загрузить эти два JSON параллельно, и когда оба будут загружены, страница должна отображаться. Вариант обратного вызова станет немного сложнее: вам нужно создать 2 обратных вызова, сохранить данные в упражнении и, если все данные загружены, отобразить страницу:

Перезвоните:

api.getUserDetails(userId, new Callback<UserDetails>() {
    @Override
    public void onSuccess(UserDetails details, Response response) {
        this.details = details;
        if(this.photos != null) {
            displayPage();
        }
    }
});

api.getUserPhotos(userId, new Callback<List<Photo>>() {
    @Override
    public void onSuccess(List<Photo> photos, Response response) {
        this.photos = photos;
        if(this.details != null) {
            displayPage();
        }
    }
});

RxJava:

private class Combined {
    UserDetails details;
    List<Photo> photos;
}


Observable.zip(api.getUserDetails(userId), api.getUserPhotos(userId), new Func2<UserDetails, List<Photo>, Combined>() {
            @Override
            public Combined call(UserDetails details, List<Photo> photos) {
                Combined r = new Combined();
                r.details = details;
                r.photos = photos;
                return r;
            }
        }).subscribe(new Action1<Combined>() {
            @Override
            public void call(Combined combined) {
            }
        });

Мы куда-то добираемся! Код RxJava теперь такой же большой, как и опция обратного вызова. Код RxJava является более надежным; Подумайте, что произойдет, если нам понадобится загрузить третий JSON (например, последние видео)? RxJava потребуется лишь небольшая настройка, в то время как вариант Callback должен быть настроен в нескольких местах (при каждом обратном вызове мы должны проверять, все ли данные получены).

Другой пример; мы хотим создать поле автозаполнения, которое загружает данные с помощью Retrofit. Мы не хотим делать веб-вызовы каждый раз, когда EditText имеет TextChangedEvent. При быстрой печати только последний элемент должен инициировать вызов. На RxJava мы можем использовать оператор debounce:

inputObservable.debounce(1, TimeUnit.SECONDS).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                // use Retrofit to create autocompletedata
            }
        });

Я не буду создавать вариант обратного вызова, но вы поймете, что это гораздо больше работы.

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

Ответ 2

Наблюдаемый материал уже выполнен в "Дооснащении", поэтому код может быть следующим:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });

Ответ 3

В случае getUserPhoto() преимущества для RxJava невелики. Но давайте возьмем еще один пример, когда вы получите все фотографии для пользователя, но только тогда, когда изображение будет PNG, и у вас нет доступа к JSON для фильтрации на сервере.

api.getUserPhotos(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<List<Photo>, Observable<Photo>>() {
    @Override
    public Observable<Photo> call(List<Photo> photos) {
         return Observable.from(photos);
    }
})
.filter(new Func1<Photo, Boolean>() {
    @Override
    public Boolean call(Photo photo) {
         return photo.isPNG();
    }
})
.subscribe(
    new Action1<Photo>() {
    @Override
        public void call(Photo photo) {
            // on main thread; callback for each photo, add them to a list or something.
            list.add(photo)
        }
    }, 
    new Action1<Throwable>() {
    @Override
        public void call(Throwable throwable) {
            // on main thread; something went wrong
            System.out.println("Error! " + throwable);
        }
    }, 
    new Action0() {
        @Override
        public void call() {
            // on main thread; all photo loaded, time to show the list or something.
        }
    });

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

TL;DR Укажите здесь; обратный вызов возвращает только обратный вызов для успешной работы и отказа; RxJava Observable позволяет вам делать карту, уменьшать, фильтровать и многое другое.

Ответ 4

С помощью rxjava вы можете делать больше вещей с меньшим количеством кода.

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

С rxjava очень просто.

public class PhotoModel{
  BehaviorSubject<Observable<Photo>> subject = BehaviorSubject.create(...);

  public void setUserId(String id){
   subject.onNext(Api.getUserPhoto(photoId));
  }

  public Observable<Photo> subscribeToPhoto(){
    return Observable.switchOnNext(subject);
  }
}

если вы хотите реализовать мгновенный поиск, вам нужно только прослушать TextChangeListener и вызвать photoModel.setUserId(EditText.getText());

В методе onCreate Fragment или activity вы подписываетесь на Observable, который возвращает photoModel.subscribeToPhoto(), он возвращает Observable, который всегда излучает элементы, испускаемые последним Observable (request).

AndroidObservable.bindFragment(this, photoModel.subscribeToPhoto())
                 .subscribe(new Action1<Photo>(Photo photo){
      //Here you always receive the response of the latest query to the server.
                  });

Кроме того, если, например, PhotoModel является Singleton, вам не нужно беспокоиться об изменении ориентации, потому что BehaviorSubject испускает последний ответ сервера, независимо от того, когда вы подписываетесь.

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

Ответ 5

Обычно мы используем следующую логику:

  1. Если это простой вызов с одним ответом, то Callback или Future лучше.
  2. Если это вызов с помощью нескольких ответов (поток), или когда существует сложное взаимодействие между различными вызовами (см @Niels' ответ), то Наблюдаемые лучше.

Ответ 7

По образцам и выводам в других ответах, я думаю, что нет большой разницы для простых одно- или двухэтапных задач. Однако обратный вызов является простым и понятным. RxJava более сложный и слишком большой для простой задачи. Существует третье решение: AbacusUtil. Позвольте мне реализовать вышеприведенные варианты использования со всеми тремя решениями: Callback, RxJava, CompletableFuture (AbacusUtil) с Retrolambda:

Извлечь фотографию из сети и сохранить/отобразить на устройстве:

// By Callback
api.getUserPhoto(userId, new Callback<Photo>() {
    @Override
    public void onResponse(Call<Photo> call, Response<Photo> response) {
        save(response.body()); // or update view on UI thread.
    }

    @Override
    public void onFailure(Call<Photo> call, Throwable t) {
        // show error message on UI or do something else.
    }
});

// By RxJava
api.getUserPhoto2(userId) //
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(photo -> {
            save(photo); // or update view on UI thread.
        }, error -> {
            // show error message on UI or do something else.
        });

// By Thread pool executor and CompletableFuture.
TPExecutor.execute(() -> api.getUserPhoto(userId))
        .thenRunOnUI((photo, error) -> {
            if (error != null) {
                // show error message on UI or do something else.
            } else {
                save(photo); // or update view on UI thread.
            }
        });

Загружать детали пользователя и фотографию параллельно

// By Callback
// ignored because it little complicated

// By RxJava
Observable.zip(api.getUserDetails2(userId), api.getUserPhoto2(userId), (details, photo) -> Pair.of(details, photo))
        .subscribe(p -> {
            // Do your task.
        });

// By Thread pool executor and CompletableFuture.
TPExecutor.execute(() -> api.getUserDetails(userId))
          .runOnUIAfterBoth(TPExecutor.execute(() -> api.getUserPhoto(userId)), p -> {
    // Do your task
});

Ответ 8

Лично я предпочитаю использовать Rx для получения ответов API в тех случаях, когда мне нужно выполнить фильтрацию, сопоставление или что-то подобное с данными или в случаях, когда мне нужно выполнить другие вызовы API на основе предыдущих ответов на вызовы.