Коли слід використовувати RxJava Observable, а коли простий зворотний виклик на Android?


260

Я працюю над мережею для свого додатка. Тому я вирішив спробувати Retrofit Square . Я бачу, що вони підтримують просте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?
            }
        });

Я щось тут пропускаю? Або це неправильний випадок використання Observables? Коли б / слід віддати перевагу Observableпростому зворотному виклику?

Оновлення

Використовувати модернізацію набагато простіше, ніж приклад вище, як показав @Niels у своїй відповіді або в прикладі проекту U2020 Джейка Уортона . Але по суті питання залишається однаковим - коли слід використовувати той чи інший спосіб?


чи можете ви оновити своє посилання на файл, про який ви говорите в U2020
перерахуйте

Це все ще працює ...
Мартінас Юркус

4
Людина У мене були такі ж думки саме тоді, коли я читав RxJava - це нова річ. Я прочитав приклад доопрацювання (тому що я надзвичайно знайомий з ним) простого запиту, і це було десять-п’ятнадцять рядків коду, і моя перша реакція полягала в тому, що ти мусиш жартувати мене = /. Я також не можу зрозуміти, як це замінює шину подій, оскільки шина події від'єднує вас від спостережуваного і rxjava знову вводить з'єднання, якщо я не помиляюся.
Ло-Тан

Відповіді:


348

Для простих мережних матеріалів переваги RxJava перед зворотним зворотом дуже обмежені. Простий приклад 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 потребує лише невеликого регулювання, тоді як варіант зворотного виклику повинен бути відрегульований у декількох місцях (для кожного зворотного дзвінка нам потрібно перевірити, чи отримано всі дані).

Ще один приклад; ми хочемо створити поле автозаповнення, яке завантажує дані за допомогою Retrofit. Ми не хочемо робити веб-дзвінок щоразу, коли редактор тексту має TextChangedEvent. Коли ви швидко набираєте текст, лише останній елемент повинен викликати дзвінок. На RxJava ми можемо використовувати оператор дебютування:

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

Я не буду створювати варіант зворотного виклику, але ви зрозумієте, що це набагато більше роботи.

Висновок: RxJava надзвичайно хороший, коли дані надсилаються як потік. Модуль модернізації одночасно виштовхує всі елементи на потоці. Це само по собі не є корисним порівняно з зворотним викликом. Але коли в потоці є декілька елементів, що висуваються в різні часи, і вам потрібно робити речі, пов’язані з тимчасовим часом, RxJava робить код набагато більш ретельним.


6
Який клас ви використовували для inputObservable? У мене дуже багато проблем з дебютацією і я хотів би дізнатися трохи більше про це рішення.
Мігор

@Migore RxBinding проект має «Платформа прив'язки» модуль , який забезпечує класи , як RxView, і RxTextViewт.д. , які можуть бути використані для inputObservable.
хуртовина

@Niels чи можете ви пояснити, як додати поводження з помилками? чи можете ви створити абонента за допомогою onCompleted, onError та onNext, якщо ви flatMap, Filter і потім підписуєтеся? Дуже дякую за велике пояснення.
Ніколя Джафель

4
Це приголомшливий приклад!
Ye Lin Aung

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 спостереження можуть бути корисними, якщо ви хочете пов'язати кілька функцій. Наприклад, абонент хоче отримати фотографію, обрізану розмірами 100х100. Api може повернути фотографії будь-якого розміру, тому ви можете зіставити файл getUserPhoto, який можна побачити, на інший ResizedPhotoObservable - абонент отримує сповіщення лише тоді, коли зміниться розмір. Якщо вам не потрібно використовувати його, не змушуйте його.
ataulm

2
Один незначний відгук. Там немає необхідності викликати .subscribeOn (Schedulers.io ()) , як ДООСНАСТКЕ вже доглядає цього - 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. Після цього ми підпишемося та отримаємо зворотний виклик для кожної окремої фотографії, повідомлення про помилку та зворотний виклик, коли всі рядки будуть завершені.

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


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

але я можу це зробити (зображення фільтра - це PNG) у .subscribe () Чому я повинен використовувати фільтр? і в плановій карті немає потреби
SERG

3
3 відповіді від @Niels. Перший відповідає на питання. Немає користі для 2-го
Реймонд Шенон

така ж відповідь, що і перша
Mayank Sharma

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, який завжди випромінює елементи, випущені останнім спостережуваним (запитом).

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. Якщо це простий дзвінок з одним відгуком, тоді краще зворотний виклик або майбутнє.
  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
OP не просила пропозицій щодо нової бібліотеки
forresthopkinsa

1

Я особисто вважаю за краще використовувати Rx для отримання відповідей api у випадках, коли мені доведеться фільтрувати, картографувати чи щось подібне на даних або у випадках, коли мені доведеться робити інші дзвінки api на основі попередніх відповідей на дзвінки.


0

Схоже, ви винаходите колесо, те, що ви робите, вже реалізовано в доопрацюванні.

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


Дякую за відповідь. Я бачу, що ви говорите, але я ще не впевнений, як би це здійснив. Чи можете ви надати простий приклад, використовуючи існуючу реалізацію дооснащення, щоб я міг нагородити вас винагородою?
Йозеф

@Yehosef хтось видалив їх коментар чи ви робили вигляд, що ви є ОП? ))
Фарид

Ви можете вручити винагороду за чуже питання. Коли я це запитав, мені дуже потрібна відповідь на питання - тому я запропонував щедроту. Якщо ви надінетесь на нагороду за нагороду, ви побачите, що вона була присуджена мною.
Йозеф

0

Коли ви створюєте додаток для розваг, проект для домашніх тварин, або POC або 1-й прототип, ви використовуєте прості класи на андроїд / java, наприклад, зворотні дзвінки, завдання асинхронізації, петлі, потоки тощо. Вони прості у використанні і не вимагають ніяких Інтеграція сторонніх лібер. Велика інтеграція бібліотеки просто для побудови незмінного для невеликого часу проекту є нелогічною, коли щось подібне може бути зроблено прямо з кажана.

Однак це як дуже гострий ніж. Використовувати їх у виробничих умовах завжди здорово, але вони також матимуть наслідки. Важко написати безпечний паралельний код, якщо ви не добре знаєте принципи чистого кодування та принципи SOLID. Вам доведеться підтримувати належну архітектуру, щоб полегшити майбутні зміни та підвищити продуктивність команди.

З іншого боку, паралельні бібліотеки, такі як RxJava, Co-rutines тощо, перевіряються і перевіряються понад мільярд разів, щоб допомогти написати готовий до виробництва одночасний код. Знову ж таки, це не те, що користуючись цими бібліотеками, ви не пишете одночасний код або не абстрагуєте всю логіку одночасності. Ти все-таки є. Але тепер це помітно і застосовує чітку схему для написання одночасного коду на всій кодовій базі та, що ще важливіше, у всій вашій команді розробників.

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


TL'DR

Якщо ви вже використовуєте RxJava для одночасного кодування у всій базі коду, просто використовуйте RxJava Observable / Flowable. Мудрий питання, щоб задати тоді, чи слід використовувати спостереження для Flowables. Якщо ні, то вперед і використовуйте дзвінок.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.