什么时候应该使用RxJava Observable,什么时候应该在Android上使用简单的Callback?


260

我正在为我的应用程序联网。因此,我决定尝试使用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?
            }
        });

我在这里想念什么吗?还是使用Observables 是错误的情况?什么时候/应该Observable比简单的回调更喜欢?

更新资料

@Niels在他的回答或Jake Wharton的示例项目U2020中显示,使用改造比上面的示例简单得多。但是从本质上讲,问题仍然是一样的-什么时候应该使用一种方法?


您可以更新您在U2020中正在讨论的文件的链接吗
letroll 2014年

它仍在工作...
Martynas Jurkus 2014年

4
伙计,我在阅读RxJava时完全有相同的想法,这是新事物。我读了一个简单请求的改造示例(因为我对此非常熟悉),它是10或15行代码,而我的第一个反应是,你一定是在跟我开玩笑= /。我也无法弄清楚它如何替换事件总线,因为事件总线使您与可观察的对象分离,而rxjava重新引入了耦合,除非我弄错了。
-Tan)

Answers:


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 feed,那么在仅显示PNG时检索所有照片会有些奇怪。只需将Feed调整为仅显示PNG。

第一个结论

加载准备以正确格式显示的简单JSON时,它不会使代码库变小。

现在,让我们做些有趣的事情。假设您不仅要检索userPhoto,而且有一个Instagram克隆,并且要检索2个JSON:1. getUserDetails()2. getUserPhotos()

您要并行加载这两个JSON,并且在同时加载这两个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
            }
        });

我不会创建Callback变体,但是您会了解这还需要做很多工作。

结论:当数据作为流发送时,RxJava非常好。Retrofit Observable可将所有元素同时推送到流中。与回调相比,它本身并不是特别有用。但是,当有多个元素推送到流上且时间不同时,并且您需要执行与时序相关的工作时,RxJava使代码更具可维护性。


6
您使用的是哪一类inputObservable?我在反跳方面遇到很多问题,想了解更多有关此解决方案的信息。
Migore

@Migore RxBinding项目具有“平台结合”模块,它提供等的类RxViewRxTextView等等,可用于inputObservable
暴雪2015年

@Niels您能解释一下如何添加错误处理吗?如果您先进行flatMap,筛选和订阅,就可以使用onCompleted,onError和onNext创建订阅者吗?非常感谢您的解释。
Nicolas Jafelle 2015年

4
这是一个了不起的例子!
Ye Lin Aung

3
有史以来最好的解释!
Denis Nek

66

Observable的内容已经在Retrofit中完成,因此代码可能是这样的:

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

5
是的,但是问题是什么时候比另一个更喜欢?
Christopher Perry 2014年

7
那么,这比简单的回调更好吗?
Martynas Jurkus 2014年

14
如果要链接多个功能,@ MartynasJurkus的可观察对象会很有用。例如,呼叫者希望获得一张裁剪为100x100尺寸的照片。该api可能返回任何大小的照片,因此您可以将可观察到的getUserPhoto映射到另一个ResizedPhotoObservable-调用者仅在调整大小后得到通知。如果您不需要使用它,请不要强行使用它。
ataulm 2014年

2
一小部分反馈。无需调用.subscribeOn(Schedulers.io()),因为RetroFit已经解决了这个问题-github.com/square/retrofit/issues/430(请参阅Jake的回复)
hiBrianLee14年

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以获得lambda表达式时,它会变得更好。

但是我可以在.subscribe()中做到这一点(过滤器图像为PNG),为什么要使用过滤器?并不需要平面图
SERG

3
来自@Niels的3个答案。第一个回答了这个问题。对于第二个没有好处
Raymond Chenon

与第一个相同的答案
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());

在Fragment或活动的onCreate方法中,您预订了返回photoModel.subscribeToPhoto()的Observable,它返回一个Observable,该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会发出最后的服务器响应,而无论您何时订阅。

通过这些代码行,我们实现了即时搜索并处理方向更改。您认为可以用更少的代码实现回调吗?我对此表示怀疑。


您不认为使PhotoModel类成为Singleton本身是一种限制吗?想象一下,它不是用户个人资料,而是多个用户的照片集,难道我们不会仅获得最后请求的用户照片吗?再加上要求有关每个方向变化的数据对我来说听起来有点不对劲,您认为呢?
Farid

2

我们通常遵循以下逻辑:

  1. 如果这是一个简单的单响应呼叫,则Callback或Future更好。
  2. 如果是具有多个响应(流)的呼叫,或者不同呼叫之间存在复杂的交互(请参阅@Niels的答案),那么Observables更好。

1

通过其他答案中的样本和结论,我认为简单的一两步任务没有太大区别。但是,回调很简单明了。RxJava更复杂,太大,无法完成简单的任务。第三种解决方案是:AbacusUtil。让我用所有三个解决方案实现上述用例:带有Retrolambda的 Callback,RxJava,CompletableFuture(AbacusUtil):

从网络获取照片并保存/显示在设备上:

// 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,他们在其中定义一个将Observable作为返回类型的接口,然后使用它


感谢你的回答。我知道您在说什么,但我不确定如何实现此目的。您能否使用现有的改造实施方案提供一个简单的示例,以便向您颁发赏金?
Yehosef 2014年

@Yehosef是否有人删除了他们的评论,或者您假装自己是OP?))
Farid

您可以奖励别人的问题。当我问这个问题时,我真的需要这个问题的答案-所以我提供了赏金。如果您悬停在赏金奖上,您会看到它是我授予的。
Yehosef

0

当您创建用于娱乐,宠物项目,POC或第一个原型的应用程序时,可以使用简单的核心android / java类,例如回调,异步任务,循环程序,线程等。它们易于使用,不需要任何操作第三方库集成。如果可以立即进行类似的操作,那么仅仅为了建立一个小型的,不变的项目而进行的大型图书馆集成是不合逻辑的。

但是,这些就像一把非常锋利的刀。在生产环境中使用它们总是很酷,但也会带来后果。如果您不熟悉Clean编码和SOLID原理,则很难编写安全的并发代码。您必须维护适当的体系结构以促进将来的更改并提高团队生产力。

另一方面,并​​发库(如RxJava,Co-routines等)经过十亿次尝试和测试,以帮助编写可用于生产环境的并发代码。再一次,这并不是说您没有在编写并发代码或没有抽象掉所有并发逻辑。你还是。但是现在,它是可见的,并为在整个代码库(更重要的是在整个开发团队中)编写并发代码提供了清晰的模式。

使用并发框架而不是处理原始并发的普通旧核心类,这是一个主要好处。但是,请不要误解我。我坚信限制外部库的依赖关系,但是在这种特定情况下,您将必须为代码库构建自定义框架,这是一项耗时的任务,只有经过高级经验才能完成。因此,并发框架比使用普通类(如回调等)更受青睐。


TL'DR

如果您已经在整个代码库中使用RxJava进行并发编码,则只需使用RxJava Observable / Flowable。然后要问的一个明智的问题是,我应该使用Observables到Flowables。如果不是,请继续使用可调用对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.