创建RecyclerView Adapter

我们从REST API获取到数据后,我们需要把它绑定View上,并用一个适配器填充列表。我们的RecyclerView适配器是标准的。它继承于RecyclerView.Adapter并指定它自己的ViewHolder

  1. public static class ViewHolder extends RecyclerView.ViewHolder {
  2. @InjectView(R.id.name) TextView name;
  3. @InjectView(R.id.city) TextView city;
  4. @InjectView(R.id.reputation) TextView reputation;
  5. @InjectView(R.id.user_image) ImageView user_image;
  6. public ViewHolder(View view) {
  7. super(view);
  8. ButterKnife.inject(this, view);
  9. }
  10. }

我们一旦收到来自API管理器的数据,我们可以设置界面上所有的标签:name,cityreputation

为了展示用户的头像,我们将使用Sergey Tarasevich写的Universal Image Loader。实践证明,UIL是非常有名的好用的图片管理库。我们也可以使用Square公司的Picasso,Glide或者Facebook公司的Fresco。取决于你自己的喜好。最关键的是无需重复造轮子:库能够方便开发者并让他们更快速实现目标。

在我们的适配器中,我们可以这样:

  1. @Override
  2. public void onBindViewHolder(SoAdapter.ViewHolder holder, int position) {
  3. User user = mUsers.get(position);
  4. holder.setUser(user);
  5. }

ViewHolder,我们可以这样:

  1. public void setUser(User user) {
  2. name.setText(user.getDisplayName());
  3. city.setText(user.getLocation());
  4. reputation.setText(String.valueOf(user.getReputation()));
  5. ImageLoader.getInstance().displayImage(user.getProfileImage(), user_image);
  6. }

此时,我们可以允许代码获得一个用户列表,正如下图所示:

创建RecyclerView Adapter - 图1

检索天气预报

我们加大难度,将当地城市的天气加入列表中。OpenWeatherMap是一个灵活公共在线天气API,我们可以查询许多有用的预报信息。

和往常一样,我们将使用Retrofit映射到API然后通过RxJava来访问它。至于StackExchange API,我们将创建interfaceRestAdapter和一个灵活的管理器:

  1. public interface OpenWeatherMapService {
  2. @GET("data2.5/weather")
  3. Observable<WeatherResponse> getForecastByCity(@Query("q") String city);
  4. }

这个方法用城市名字作为参数提供当地的预报信息。我们像下面这样将接口和RestAdapter类绑定在一起:

  1. RestAdapter restAdapter = new RestAdapter.Builder()
  2. .setEndpoint("http://api.openweathermap.org")
  3. .setLogLevel(RestAdapter.LogLevel.BASIC)
  4. .build();
  5. mOpenWeatherMapService = restAdapter.create(OpenWeatherMapService.class);

像以前一样,我们只有两件事需要立马去做:设置API端口和log级别。

OpenWeatherMapApiManager类将提供下面的方法:

  1. public Observable<WeatherResponse> getForecastByCity(String city) {
  2. return mOpenWeatherMapService.getForecastByCity(city)
  3. .subscribeOn(Schedulers.io())
  4. .observeOn(AndroidSchedulers.mainThread());
  5. }

现在,我们有了用户列表,我们可以根据城市名来查询OpenWeatherMap获得天气预报信息。下一步是修改我们的ViewHolder类来为每位用户展示相应的天气图标。

我们使用这些工具方法先验证用户主页信息并获得一个合法的城市名字:

  1. private boolean isCityValid(String location) {
  2. int separatorPosition = getSeparatorPosition(location);
  3. return !"".equals(location) && separatorPosition > -1;
  4. }
  5. private int getSeparatorPosition(String location) {
  6. int separatorPosition = -1;
  7. if (location != null) {
  8. separatorPosition = location.indexOf(",");
  9. }
  10. return separatorPosition;
  11. }
  12. private String getCity(String location, int position) {
  13. if (location != null) {
  14. return location.substring(0, position);
  15. } else {
  16. return "";
  17. }
  18. }

借助一个有效的城市名,我们可以用下面命令来获得我们所需要天气的所有数据:

  1. OpenWeatherMapApiManager.getInstance().getForecastByCity(city)

用天气响应的结果,我们可以获得天气图标的URL:

  1. getWeatherIconUrl(weatherResponse);

用图标URL,我们可以检索到图标本身:

  1. private Observable<Bitmap> loadBitmap(String url) {
  2. return Observable.create(subscriber -> {
  3. ImageLoader.getInstance().displayImage(url,city_image, new ImageLoadingListener() {
  4. @Override
  5. public void onLoadingStarted(String imageUri, View view) {
  6. }
  7. @Override
  8. public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
  9. subscriber.onError(failReason.getCause());
  10. }
  11. @Override
  12. public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
  13. subscriber.onNext(loadedImage);
  14. subscriber.onCompleted();
  15. }
  16. @Override
  17. public void onLoadingCancelled(String imageUri, View view) {
  18. subscriber.onError(new Throwable("Image loading cancelled"));
  19. }
  20. });
  21. });
  22. }

这个loadBitmap()返回的Observable可以链接前面一个,并且最后我们可以为这个任务返回一个单独的Observable:

  1. if (isCityValid(location)) {
  2. String city = getCity(location, separatorPosition);
  3. OpenWeatherMapApiManager.getInstance().getForecastByCity(city)
  4. .filter(response -> response != null)
  5. .filter(response -> response.getWeather().size() > 0)
  6. .flatMap(response -> {
  7. String url = getWeatherIconUrl(response);
  8. return loadBitmap(url);
  9. })
  10. .subscribeOn(Schedulers.io())
  11. .observeOn(AndroidSchedulers.mainThread())
  12. .subscribe(new Observer<Bitmap>() {
  13. @Override
  14. public void onCompleted() {
  15. }
  16. @Override
  17. public void onError(Throwable e) {
  18. App.L.error(e.toString());
  19. }
  20. @Override
  21. public void onNext(Bitmap icon) {
  22. city_image.setImageBitmap(icon);
  23. }
  24. });
  25. }

运行代码,我们可以在下面列表中为每个用户获得新的天气图标: 创建RecyclerView Adapter - 图2

打开网站

使用用户主页包含的信息,我们将会创建一个onClick监听器来导航到用户web页面,如果有的话,否则打开在Stack Overflow上的个人主页。

为了实现它,我们简单实现Activity类的接口,用来在适配器触发Android的onClick事件。

我们的Adapter ViewHolder指定这个接口:

  1. public interface OpenProfileListener {
  2. public void open(String url);
  3. }

Activity实现它:

  1. [...] implements SoAdapter.ViewHolder.OpenProfileListener { [...]
  2. mAdapter.setOpenProfileListener(this);
  3. [...]
  4. @Override
  5. public void open(String url) {
  6. Intent i = new Intent(Intent.ACTION_VIEW);
  7. i.setData(Uri.parse(url));
  8. startActivity(i);
  9. }

Activity收到URL并用外部Android浏览器打开它。我们的ViewHolder负责在用户列表的每个卡片上创建OnClickListener并检查我们是打开Stack Overflow用户主页还是外部个人站:

  1. mView.setOnClickListener(view -> {
  2. if (mProfileListener != null) {
  3. String url = user.getWebsiteUrl();
  4. if (url != null && !url.equals("") && !url.contains("search")) {
  5. mProfileListener.open(url);
  6. } else {
  7. mProfileListener.open(user.getLink());
  8. }
  9. }
  10. )};

一旦我们点击了,我们将直接重定向到预期的网站。在Android上,我们可以用RxAndroid的一种特殊形式(ViewObservable)以更加响应式的方式实现同样的结果。

  1. ViewObservable.clicks(mView)
  2. .subscribe(onClickEvent -> {
  3. if (mProfileListener != null) {
  4. String url = user.getWebsiteUrl();
  5. if (url != null && !url.equals("") && !url.contains("search")) {
  6. mProfileListener.open(url);
  7. } else {
  8. mProfileListener.open(user.getLink());
  9. }
  10. }
  11. });

上面两块代码片段是等价的,你可以选择最喜欢的方式来实现。