在Android开发中,何时应该使用RxJava Observable,何时应该使用简单的回调函数?

271

我正在为我的应用程序开发网络功能。因此,我决定尝试使用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);

Both look pretty similar at first glance, but when it gets to implementation it gets interesting...
在第一眼看上去,两者看起来非常相似,但是当涉及到实现时就变得有趣了...
而使用简单的回调实现将类似于这样:
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是错误的情况? 何时应该优先选择Observable而不是简单的回调函数?

更新

如@Niels在他的答案中所示或在Jake Wharton的示例项目U2020中,使用retrofit比上面的示例要简单得多。 但是基本问题仍然是相同的-何时应该使用其中一种方法?


你能否更新一下你在U2020中提到的文件链接? - letroll
它仍在运行中... - Martynas Jurkus
6
当我读到 RxJava 是新玩意时,我也有同样的想法。我阅读了一个 retrofit 的示例(因为我对它非常熟悉),一个简单的请求需要十到十五行代码,我的第一反应是你一定在开玩笑 =/。我也不明白它如何取代事件总线,因为事件总线将你与可观察对象解耦,而 RxJava 又重新引入了耦合,除非我错了。 - Lo-Tan
9个回答

359

对于简单的网络操作,RxJava相比Callback的优势非常有限。以获取用户照片为例:

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的变体并不比回调函数的变体好多少。暂时先不考虑错误处理。 假设我们有一组照片列表:

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的变体虽然使用Lambda表达式可以更接近回调函数的变体,但仍然不会更小。

此外,如果您可以访问JSON源,那么当您只显示PNG时检索所有照片可能有些奇怪。只需调整源以仅显示PNG即可。

第一个结论

当您加载一个简单的JSON并将其准备为正确格式时,不会使代码库更小。

现在,让我们稍微有些有趣。假设您不仅想检索用户照片,而且您还有一个类似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加载数据。 我们不希望在每次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可以使代码更易于维护。


6
你在 inputObservable 中使用了哪个类?我在去抖方面遇到了很多问题,想学习一些有关这个解决方案的知识。 - Migore
@Migore RxBinding 项目具有“平台绑定”模块,该模块提供了类似 RxViewRxTextView 等类,可用于 inputObservable - blizzard
@Niels,你能解释一下如何添加错误处理吗?如果你使用flatMap、Filter然后订阅,你能创建一个带有onCompleted、onError和onNext的订阅者吗?非常感谢你的详细解释。 - Nicolas Jafelle
6
这是一个很棒的例子! - Ye Lin Aung
4
最好的解释! - Denis Nek
显示剩余10条评论

68

在 Retrofit 中,Observable 部分已经完成了,因此代码可能如下:

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

6
是的,但问题是何时优先选择其中的一个? - Christopher Perry
8
这种方法比简单的回调函数更好在哪里? - Martynas Jurkus
17
如果您想链接多个函数,观察者模式可能很有用。例如,调用者想要获取一个裁剪为100x100尺寸的照片。API可能会返回任何尺寸的照片,因此您可以将getUserPhoto观察对象映射到另一个ResizedPhotoObservable - 只有在调整大小完成时才会通知调用者。如果您不需要使用它,请不要强制使用。 - ataulm
3
一点小反馈。没有必要调用.subscribeOn(Schedulers.io()),因为RetroFit已经处理了这个问题 - https://github.com/square/retrofit/issues/430(请参见Jake的回复)。 - hiBrianLee
4
与 ataulm 提到的内容不同,除了 Callback,你还可以取消订阅 Observeable,从而避免常见的生命周期问题。 - Dmitry Zaytsev
显示剩余2条评论

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返回一个Photo列表,我们将使用flatMap将它们拆分为单独的项目。通过这样做,我们将能够使用filter方法忽略不是PNG格式的照片。之后,我们会订阅并针对每个单独的照片获得回调,一个错误处理程序以及一个在所有行完成时的回调。

简而言之 重点在于:回调仅返回成功和失败的回调,而RxJava Observable允许您执行映射、缩小、过滤等许多其他操作。


首先,使用Retrofit时不需要订阅subscribeOn和观察observeOn。它会异步执行网络调用,并在与调用者相同的线程上通知。当您添加RetroLambda以获取lambda表达式时,它变得更加美好。 - user486646
但我可以在.subscribe()中完成它(过滤图像为PNG)。为什么应该使用filter?而且在flatMap中没有必要。 - SERG
3
从 @Niels 的三个回答中,第一个回答回答了问题。 第二个回答没有益处。 - Raymond Chenon
same answer as first - Mayank Sharma

28

使用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或Activity的onCreate方法中订阅返回photoModel.subscribeToPhoto()的Observable,它返回一个总是发射最新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类设置为单例本身就是一种限制吗?想象一下,如果它不是用户个人资料而是多个用户的照片集合,我们只会得到最后一个请求的用户照片吗?此外,在每次方向更改时请求数据听起来有点奇怪,你觉得呢? - Farid

2

我们通常采用以下逻辑:

  1. 如果是简单的单响应调用,则使用回调或Future更好。
  2. 如果是多个响应(流)的调用,或者不同调用之间存在复杂的交互(参见@Niels'的答案),则使用Observables更好。

1
根据其他答案中的示例和结论,我认为对于简单的一两步任务没有太大差异。然而,回调函数简单明了。RxJava对于简单任务来说过于复杂且庞大。第三种解决方案是:abacus-common。让我使用以上三种解决方案实现以下用例:使用回调函数、RxJava和CompletableFuture(abacus-common)与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 调用的情况下。请保留 HTML 标签。

0

谢谢你的回答。我理解你的意思,但我还不确定如何实现它。你能否提供一个使用现有的 Retrofit 实现的简单示例,以便我可以授予你赏金? - Yehosef
@Yehosef,是有人删除了他们的评论还是你在假装成OP?)) - Farid
你可以对别人的问题设置悬赏奖励。当我提问时,真正需要这个问题的答案 - 所以我设置了一个悬赏。如果你将鼠标悬停在悬赏奖励上,你会看到是我颁发的。 - Yehosef

0

当你为了娱乐、宠物项目、POC或第一个原型创建应用程序时,你可以使用简单的核心Android/Java类,如回调、异步任务、循环器、线程等。它们易于使用,不需要任何第三方库集成。如果可以通过简单的方式完成类似的小型不可变项目,那么为了构建这样的项目而进行大型库集成是不合理的。

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

另一方面,并发库如RxJava、Co-routines等已经经过数十亿次尝试和测试,以帮助编写生产就绪的并发代码。现在再次强调,使用这些库并不意味着您不编写并发代码或将所有并发逻辑抽象化。您仍然是在编写并发代码。但现在,它是可见的,并且强制执行编写并发代码的明确模式贯穿整个代码库,更重要的是贯穿整个开发团队。

使用并发框架而不是处理原始并发的普通核心类,这是一个重要的好处。然而,请不要误解我。我非常相信限制外部库的依赖性,但在这种特定情况下,您将不得不为您的代码库构建自定义框架,这是一项耗时的任务,只能在有经验的情况下完成。因此,并发框架比使用回调等简单类更受欢迎。


简而言之

如果您已经在整个代码库中使用RxJava进行并发编码,则只需使用RxJava Observable/Flowable。然后要问的一个明智的问题是,我应该使用Observables还是Flowables。 如果不是这样,那就继续使用callable。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接