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


260

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

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

и RxJava's 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?
            }
        });

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

Обновить

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


Можете
ли

Это все еще работает ...
Мартинас Юркус

4
Человек, у которого были те же мысли, когда я читал RxJava, это было новым. Я прочитал пример модификации (потому что я очень хорошо знаком с ним) простого запроса, в котором было десять или пятнадцать строк кода, и моей первой реакцией было то, что ты, должно быть, шутишь = /. Я также не могу понять, как это заменяет шину событий, так как шина событий отделяет вас от наблюдаемого, и rxjava повторно вводит связь, если я не ошибаюсь.
Ло-Тан

Ответы:


348

Для простых сетевых вещей преимущества 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 помещает все элементы в поток одновременно. Это не особенно полезно само по себе по сравнению с Callback. Но когда в поток помещается несколько элементов и в разное время, и вам нужно делать вещи, связанные с синхронизацией, RxJava делает код более понятным.


6
Какой класс вы использовали для inputObservable? У меня много проблем с разоблачением, и я хотел бы узнать немного больше об этом решении.
Мигоре

Проект @Migore RxBinding имеет модуль «Привязка платформы», который предоставляет такие классы, как RxViewи RxTextViewт. Д., Которые можно использовать для inputObservable.
метель

@Niels, можете ли вы объяснить, как добавить обработку ошибок? Вы можете создать подписчика с помощью onCompleted, onError и onNext, если вы используете flatMap, Filter и затем подписываетесь? Большое вам спасибо за ваше большое объяснение.
Николя Жафель

5
Это потрясающий пример!
Е. Лин Аунг

3
Лучшее объяснение!
Денис Нек

66

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

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

5
Да, но вопрос в том, когда предпочесть одно другому?
Кристофер Перри

7
Так как же это лучше, чем простой обратный вызов?
Мартинас Юркус

14
Наблюдаемые @MartynasJurkus могут быть полезны, если вы хотите связать несколько функций в цепочку. Например, звонящий хочет получить фотографию, обрезанную до размеров 100x100. API может вернуть фотографию любого размера, так что вы можете сопоставить наблюдаемую getUserPhoto с другим ResizedPhotoObservable - вызывающий абонент получает уведомление только после завершения изменения размера. Если вам не нужно его использовать, не форсируйте его.
ataulm

2
Один небольшой отзыв. Нет необходимости звонить .subscribeOn (Schedulers.io ()), так как RetroFit уже позаботится об этом - github.com/square/retrofit/issues/430 (см. Ответ Джейка)
hiBrianLee

4
@MartynasJurkus в дополнение к материалам, сказанным ataulm, в отличие от того, от которого Callbackвы можете отказаться Observeable, поэтому избегаете распространенных проблем жизненного цикла.
Дмитрий Зайцев

35

В случае 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's loaded, time to show the list or something.
        }
    });

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

TLDR Точка здесь существо; обратный вызов только возвращает вам обратный вызов для успеха и неудачи; RxJava Observable позволяет вам делать карту, уменьшать, фильтровать и многое другое.


Прежде всего, подписка и наблюдение не нужны при модернизации. Он будет выполнять сетевой вызов асинхронно и уведомлять в том же потоке, что и вызывающий. Когда вы добавляете RetroLambda, чтобы получить лямбда-выражения, становится намного приятнее.

но я могу сделать это (изображение фильтра PNG) в .subscribe () Почему я должен использовать фильтр? и нет необходимости в flatmap
SERG

3
3 ответа от @Niels. Первый отвечает на вопрос. Никакой выгоды для 2-го
Рэймонд Ченон


27

С 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 фрагмента или действия вы подписываетесь на Observable, который возвращает photoModel.subscribeToPhoto (), он возвращает Observable, который всегда испускает элементы, испускаемые последним Observable (запросом).

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 выдает последний ответ сервера, независимо от того, когда вы подписались.

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


Не кажется ли вам, что создание класса PhotoModel в качестве Singleton само по себе является ограничением? Представьте, что это не профиль пользователя, а набор фотографий для нескольких пользователей. Разве мы не получим только последнюю запрошенную фотографию пользователя? Плюс запрос данных о каждом изменении ориентации звучит для меня немного странно, что вы думаете?
Фарид

2

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

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

1

По примерам и выводам в других ответах, я думаю, нет большой разницы для простых одно или двухэтапных задач. Однако Обратный звонок прост и понятен. 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's 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
});

4
ОП не просила предложений по новой библиотеке
Форрестхопкинса

1

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


0

Похоже, что вы изобретаете велосипед, то, что вы делаете, уже реализовано в модернизации.

Например, вы можете взглянуть на RestAdapterTest.java , где они определяют интерфейс с Observable в качестве возвращаемого типа, а затем использовать его .


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

@ Yehosef кто-то удалил свой комментарий или вы притворялись OP? ))
Фарид

Вы можете назначить награду за чужой вопрос. Когда я спросил об этом, мне действительно был нужен ответ на вопрос - поэтому я предложил вознаграждение. Если вы наведите курсор на награду, вы увидите, что она была вручена мной.
Yehosef

0

Когда вы создаете приложение для развлечения, проект питомца, POC или первый прототип, вы используете простые базовые классы android / java, такие как обратные вызовы, асинхронные задачи, циклы, потоки и т. Д. Они просты в использовании и не требуют каких-либо Интеграция сторонних библиотек. Интеграция большой библиотеки просто для создания небольшого неизменного проекта нелогична, когда нечто подобное можно сделать прямо из рук вон.

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

С другой стороны, библиотеки параллелизма, такие как RxJava, Co-рутины и т. Д., Проверяются и проверяются более миллиарда раз, чтобы помочь в написании готового параллельного кода. Опять же, дело не в том, что используя эти библиотеки, вы не пишете параллельный код или абстрагируете всю логику параллелизма. Вы все еще. Но теперь он видим и обеспечивает четкую схему написания параллельного кода для всей кодовой базы и, что более важно, для всей вашей команды разработчиков.

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


TL'DR

Если вы уже используете RxJava для одновременного кодирования по всей базе кода, просто используйте RxJava Observable / Flowable. Мудрый вопрос, который нужно задать, - использовать ли Observables для Flowables. Если нет, то иди и используй вызываемый.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.