认识RxJava:缺少的Android反应式编程库

本文概述

如果你是Android开发人员, 那么你可能听说过RxJava。它是在Android开发中启用反应式编程的讨论最多的库之一。它被认为是简化移动编程中固有的并发/异步任务的入门框架。

但是……什么是RxJava, 它如何”简化”事情?

Android的函数式反应式编程:RxJava简介

使用RxJava可以使Android与太多的Java线程脱钩。

鸣叫

尽管网上已经有很多资源可以解释RxJava是什么, 但在本文中, 我的目标是向你提供RxJava的基本介绍, 特别是它如何适合Android开发。我还将给出一些具体示例和建议, 说明如何将其集成到新项目或现有项目中。

为什么要考虑RxJava?

RxJava的核心是简化开发, 因为它提高了围绕线程的抽象级别。也就是说, 作为开发人员, 你不必担心如何执行应在不同线程上进行的操作的细节。这是特别有吸引力的, 因为线程难以正确实现, 如果执行不正确, 可能会导致一些最困难的错误进行调试和修复。

当然, 这并不意味着RxJava在线程化方面是防弹的, 了解幕后发生的事情仍然很重要;但是, RxJava绝对可以使你的生活更轻松。

让我们看一个例子。

网络通话-RxJava与AsyncTask

假设我们要通过网络获取数据并更新UI。一种方法是(1)在我们的Activity / Fragment中创建一个内部AsyncTask子类, (2)在后台执行网络操作, 并且(3)获取该操作的结果并更新主线程中的UI 。

public class NetworkRequestTask extends AsyncTask<Void, Void, User> {

    private final int userId;

    public NetworkRequestTask(int userId) {
        this.userId = userId;
    }

    @Override protected User doInBackground(Void... params) {
        return networkService.getUser(userId);
    }

    @Override protected void onPostExecute(User user) {
        nameTextView.setText(user.getName());
        // ...set other views
    }
}
   
private void onButtonClicked(Button button) {
   new NetworkRequestTask(123).execute()
}

这种方法似乎无害, 但存在一些问题和局限性。即, 由于NetworkRequestTask是内部类, 因此很容易创建内存/上下文泄漏, 因此对外部类具有隐式引用。另外, 如果我们要在网络通话后链接另一个长时间的操作, 该怎么办?我们必须嵌套两个AsyncTask, 这会大大降低可读性。

相比之下, 执行网络调用的RxJava方法可能看起来像这样:

private Subscription subscription;

private void onButtonClicked(Button button) {
   subscription = networkService.getObservableUser(123)
                      .subscribeOn(Schedulers.io())
                      .observeOn(AndroidSchedulers.mainThread())
                      .subscribe(new Action1<User>() {
                          @Override public void call(User user) {
                              nameTextView.setText(user.getName());
                              // ... set other views
                          }
                      });
}

@Override protected void onDestroy() {
   if (subscription != null && !subscription.isUnsubscribed()) {
       subscription.unsubscribe();
   }
   super.onDestroy();
}

使用此方法, 我们通过保留对返回的Subscription对象的引用来解决(由于正在运行的线程持有对外部上下文的引用而导致的潜在内存泄漏)问题。然后, 将此订阅对象绑定到活动/片段对象的#onDestroy()方法, 以确保在需要销毁活动/片段时不会执行Action1#call操作。

另外, 请注意, #getObservableUser(…)的返回类型(即Observable <User>)与对其的进一步调用链接在一起。通过这种流畅的API, 我们能够解决使用AsyncTask的第二个问题, 即它允许进一步的网络调用/长操作链接。很整洁吧?

让我们深入研究一些RxJava概念。

可观察, 可观察和可操作-RxJava Core的3个

在RxJava世界中, 所有内容都可以建模为流。流随时间发射项目, 并且每次发射都可以消耗/观察。

如果你考虑一下, 流不是一个新概念:单击事件可以是流, 位置更新可以是流, 推送通知可以是流, 依此类推。

在RxJava世界中,所有内容都可以建模为流。

流抽象是通过3个核心结构来实现的, 我将其称为” 3 O”。即:可观察者, 观察者和操作者。 Observable发出项目(流);观察者消耗这些物品。可观察对象的发射可以通过链接操作员调用来进一步修改, 转换和操纵。

可观察的

一个Observable是RxJava中的流抽象。它与Iterator相似, 在给定序列的情况下, 它会按顺序进行迭代并生成这些项。然后, 消费者可以通过相同的界面消费这些物品, 而无需考虑其基本顺序。

假设我们要按此顺序发出数字1、2、3。为此, 我们可以使用Observable <T> #create(OnSubscribe <T>)方法。

Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() {
   @Override public void call(Subscriber<? super Integer> subscriber) {
       subscriber.onNext(1);
       subscriber.onNext(2);
       subscriber.onNext(3);
       subscriber.onCompleted();
   }
});

调用Subscriber.onNext(Integer)会在流中发射一个项目, 并且当流发射完成时, 然后会调用Subscriber.onCompleted()。

这种创建Observable的方法非常冗长。由于这个原因, 有一些方便的方法可以创建Observable实例, 几乎在所有情况下都应该使用这种方法。

创建Observable的最简单方法是使用Observable#just(…)。就像方法名称所暗示的那样, 它只是发出你传递给它的项目作为方法参数。

Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively

观察者

Observable流的下一个组件是预订给它的Observer(一个或多个Observer)。每当流中发生”有趣”的事情时, 便会通知观察者。通过以下事件通知观察者:

  • Observer#onNext(T)-从流中发出项目时调用
  • Observable#onError(Throwable)-当流中发生错误时调用
  • Observable#onCompleted()-当流完成发射项目时调用。

要订阅流, 只需调用Observable <T> #subscribe(…)并传入一个Observer实例即可。

Observable<Integer> observable = Observable.just(1, 2, 3);
observable.subscribe(new Observer<Integer>() {
   @Override public void onCompleted() {
       Log.d("Test", "In onCompleted()");
   }

   @Override public void onError(Throwable e) {
       Log.d("Test", "In onError()");
   }

   @Override public void onNext(Integer integer) {
       Log.d("Test", "In onNext():" + integer);
   }
});

上面的代码将在Logcat中发出以下内容:

In onNext(): 1
In onNext(): 2
In onNext(): 3
In onNext(): 4
In onCompleted()

在某些情况下, 我们对可观测对象的发射不再感兴趣。例如, 在需要将”活动/片段”回收到内存中的情况下, 这在Android中尤其重要。

要停止观察项目, 我们只需要在返回的Subscription对象上调用Subscription#unsubscribe()。

Subscription subscription = someInfiniteObservable.subscribe(new Observer<Integer>() {
   @Override public void onCompleted() {
       // ...
   }

   @Override public void onError(Throwable e) {
       // ...
   }

   @Override public void onNext(Integer integer) {
       // ...
   }
});

// Call unsubscribe when appropriate
subscription.unsubscribe();

如上面的代码片段所示, 订阅了Observable后, 我们保留对返回的Subscription对象的引用, 并在必要时稍后调用subscription#unsubscribe()。在Android中, 最好在Activity#onDestroy()或Fragment#onDestroy()中调用它。

操作符

Observable发出的项目可以在通知订阅的Observer对象之前通过运算符进行转换, 修改和过滤。在函数式编程中发现的一些最常见的操作(例如, 映射, 过滤, 缩小等)也可以应用于可观察流。让我们以地图为例:

Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() {
   @Override public Integer call(Integer integer) {
       return integer * 3;
   }
}).subscribe(new Observer<Integer>() {
   @Override public void onCompleted() {
       // ...
   }

   @Override public void onError(Throwable e) {
       // ...
   }

   @Override public void onNext(Integer integer) {
       // ...
   }
});

上面的代码段将获取来自Observable的每个发射, 并将每个发射乘以3, 分别生成流3、6、9、12、15。应用运算符通常会返回另一个Observable结果, 这很方便, 因为这使我们可以链接多个操作以获得所需的结果。

鉴于上述信息流, 我们只想接收偶数。这可以通过链接过滤器操作来实现。

Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() {
   @Override public Integer call(Integer integer) {
       return integer * 3;
   }
}).filter(new Func1<Integer, Boolean>() {
   @Override public Boolean call(Integer integer) {
       return integer % 2 == 0;
   }
}).subscribe(new Observer<Integer>() {
   @Override public void onCompleted() {
       // ...
   }

   @Override public void onError(Throwable e) {
       // ...
   }

   @Override public void onNext(Integer integer) {
       // ...
   }
});

RxJava工具集中内置有许多运算符, 用于修改Observable流。如果你能想到一种修改流的方法, 那么很可能有一个运营商。与大多数技术文档不同, 阅读RxJava / ReactiveX文档非常简单且切合实际。文档中的每个操作员都附带了有关操作员如何影响流的可视化效果。这些可视化称为”大理石图”。

通过大理石图可以对称为翻转的假设算子进行建模:

如何通过大理石图对称为翻转的假设算子进行建模的示例。

使用RxJava进行多线程

通过指定应该在其中发生操作符的调度程序, 来控制可观察链中发生操作的线程。本质上, 你可以将调度程序视为线程池, 在指定线程池后, 操作员将使用该线程池并在其上运行。默认情况下, 如果未提供此类Scheduler, 则Observable链将在调用Observable#subscribe(…)的同一线程上运行。否则, 可以通过Observable#subscribeOn(Scheduler)和/或Observable#observeOn(Scheduler)指定调度程序, 其中调度的操作将在调度程序选择的线程上发生。

两种方法之间的主要区别在于Observable#subscribeOn(Scheduler)指示源Observable应该在其上运行哪个Scheduler。该链将继续在Observable#subscribeOn(Scheduler)中指定的Scheduler的线程上运行, 直到使用其他Scheduler调用Observable#observeOn(Scheduler)。进行这样的调用时, 从那里开始的所有观察者(即, 链中的后续操作)将在来自observeOn Scheduler的线程中接收通知。

这是一张大理石图, 演示了这些方法如何影响运行位置:

一个大理石图,演示了这些方法如何影响操作的运行位置。

在Android上下文中, 如果由于长时间操作而需要进行UI操作, 我们希望该操作在UI线程上进行。为此, 我们可以使用RxAndroid库中提供的调度程序之一AndroidScheduler#mainThread()。

Android上的RxJava

现在我们掌握了一些基础知识, 你可能想知道-将RxJava集成到Android应用程序中的最佳方法是什么?你可能会想到, RxJava有许多用例, 但在此示例中, 让我们看一个特定的案例:将Observable对象用作网络堆栈的一部分。

在此示例中, 我们将研究Retrofit, 这是Square开源的HTTP客户端, 具有与RxJava的内置绑定以与GitHub的API交互。具体来说, 我们将创建一个简单的应用, 为使用GitHub用户名的用户显示所有已加星标的存储库。如果你想继续前进, 可以在这里找到源代码。

创建一个新的Android项目

  • 首先创建一个新的Android项目并将其命名为GitHubRxJava。
屏幕快照:创建一个新的Android项目
  • 在”目标Android设备”屏幕中, 保持选中”电话”和”平板电脑”, 并将最低SDK级别设置为17。可以随意将其设置为较低/较高的API级别, 但是在此示例中, API级别17就足够了。
屏幕截图:"目标Android设备"屏幕
  • 在下一个提示中选择清空活动。
屏幕截图:向移动设备屏幕添加活动
  • 在最后一步中, 将”活动名称”保留为MainActivity并生成布局文件activity_main。
屏幕快照:自定义"活动"屏幕

项目设置

在app / build.gradle中包括RxJava, RxAndroid和Retrofit库。请注意, 隐式包括RxAndroid也包括RxJava。但是, 最佳实践是始终明确包含这两个库, 因为RxAndroid并不总是包含RxJava的最新版本。明确包括最新版本的RxJava保证了使用最新版本。

dependencies {
    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'io.reactivex:rxandroid:1.2.0'
    compile 'io.reactivex:rxjava:1.1.8'
    // ...other dependencies
}

创建数据对象

创建GitHubRepo数据对象类。此类在GitHub中封装了一个存储库(网络响应包含更多数据, 但我们只对其中的一部分感兴趣)。

public class GitHubRepo {

    public final int id;
    public final String name;
    public final String htmlUrl;
    public final String description;
    public final String language;
    public final int stargazersCount;

    public GitHubRepo(int id, String name, String htmlUrl, String description, String language, int stargazersCount) {
        this.id = id;
        this.name = name;
        this.htmlUrl = htmlUrl;
        this.description = description;
        this.language = language;
        this.stargazersCount = stargazersCount;
    }
}

设置改造

  • 创建GitHubService接口。我们将此接口传递给Retrofit, Retrofit将创建GitHubService的实现。
    public interface GitHubService {
        @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName);
    }

创建GitHubClient类。这将是我们将与之交互以从UI级别进行网络调用的对象。

  • 当通过Retrofit构建GitHubService的实现时, 我们需要传入RxJavaCallAdapterFactory作为呼叫适配器, 以便网络呼叫可以返回Observable对象(任何返回除Call之外的结果的网络呼叫都需要传递呼叫适配器)。

  • 我们还需要传入GsonConverterFactory, 以便我们可以使用Gson作为将JSON对象编组为Java对象的方法。

    public class GitHubClient {

        private static final String GITHUB_BASE_URL = "https://api.github.com/";

        private static GitHubClient instance;
        private GitHubService gitHubService;

        private GitHubClient() {
            final Gson gson =
                new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
            final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL)
                                                            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                                                            .addConverterFactory(GsonConverterFactory.create(gson))
                                                            .build();
            gitHubService = retrofit.create(GitHubService.class);
        }

        public static GitHubClient getInstance() {
            if (instance == null) {
                instance = new GitHubClient();
            }
            return instance;
        }

        public Observable<List<GitHubRepo>> getStarredRepos(@NonNull String userName) {
            return gitHubService.getStarredRepositories(userName);
        }
    }

设置布局

接下来, 创建一个简单的UI, 以给定的GitHub用户名显示检索到的存储库。创建activity_home.xml-我们的活动的布局-如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/list_view_repos"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/edit_text_username"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="@string/username"/>

        <Button
            android:id="@+id/button_search"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/search"/>

    </LinearLayout>

</LinearLayout>

创建item_github_repo.xml-GitHub存储库对象的ListView项布局-类似于以下内容:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="6dp">

    <TextView
        android:id="@+id/text_repo_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textStyle="bold"
        tools:text="Cropper"/>

    <TextView
        android:id="@+id/text_repo_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:lines="2"
        android:ellipsize="end"
        android:textSize="16sp"
        android:layout_below="@+id/text_repo_name"
        tools:text="Android widget for cropping and rotating an image."/>

    <TextView
        android:id="@+id/text_language"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_repo_description"
        android:layout_alignParentLeft="true"
        android:textColor="?attr/colorPrimary"
        android:textSize="14sp"
        android:textStyle="bold"
        tools:text="Language: Java"/>

    <TextView
        android:id="@+id/text_stars"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_repo_description"
        android:layout_alignParentRight="true"
        android:textColor="?attr/colorAccent"
        android:textSize="14sp"
        android:textStyle="bold"
        tools:text="Stars: 1953"/>

</RelativeLayout>

粘合在一起

创建一个ListAdapter, 负责将GitHubRepo对象绑定到ListView项目中。如果没有提供回收的View, 则该过程实质上涉及将item_github_repo.xml扩展为View。否则, 将重复使用回收的View, 以防止膨胀过多的View对象。

public class GitHubRepoAdapter extends BaseAdapter {

    private List<GitHubRepo> gitHubRepos = new ArrayList<>();

    @Override public int getCount() {
        return gitHubRepos.size();
    }

    @Override public GitHubRepo getItem(int position) {
        if (position < 0 || position >= gitHubRepos.size()) {
            return null;
        } else {
            return gitHubRepos.get(position);
        }
    }

    @Override public long getItemId(int position) {
        return position;
    }

    @Override public View getView(int position, View convertView, ViewGroup parent) {
        final View view = (convertView != null ? convertView : createView(parent));
        final GitHubRepoViewHolder viewHolder = (GitHubRepoViewHolder) view.getTag();
        viewHolder.setGitHubRepo(getItem(position));
        return view;
    }

    public void setGitHubRepos(@Nullable List<GitHubRepo> repos) {
        if (repos == null) {
            return;
        }
        gitHubRepos.clear();
        gitHubRepos.addAll(repos);
        notifyDataSetChanged();
    }

    private View createView(ViewGroup parent) {
        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        final View view = inflater.inflate(R.layout.item_github_repo, parent, false);
        final GitHubRepoViewHolder viewHolder = new GitHubRepoViewHolder(view);
        view.setTag(viewHolder);
        return view;
    }

    private static class GitHubRepoViewHolder {

        private TextView textRepoName;
        private TextView textRepoDescription;
        private TextView textLanguage;
        private TextView textStars;

        public GitHubRepoViewHolder(View view) {
            textRepoName = (TextView) view.findViewById(R.id.text_repo_name);
            textRepoDescription = (TextView) view.findViewById(R.id.text_repo_description);
            textLanguage = (TextView) view.findViewById(R.id.text_language);
            textStars = (TextView) view.findViewById(R.id.text_stars);
        }

        public void setGitHubRepo(GitHubRepo gitHubRepo) {
            textRepoName.setText(gitHubRepo.name);
            textRepoDescription.setText(gitHubRepo.description);
            textLanguage.setText("Language: " + gitHubRepo.language);
            textStars.setText("Stars: " + gitHubRepo.stargazersCount);
        }
    }
}

在MainActivity中将所有内容粘合在一起。本质上, 这是我们首次启动该应用程序时显示的活动。在这里, 我们要求用户输入他们的GitHub用户名, 最后, 通过该用户名显示所有已加星标的存储库。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private GitHubRepoAdapter adapter = new GitHubRepoAdapter();
    private Subscription subscription;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ListView listView = (ListView) findViewById(R.id.list_view_repos);
        listView.setAdapter(adapter);

        final EditText editTextUsername = (EditText) findViewById(R.id.edit_text_username);
        final Button buttonSearch = (Button) findViewById(R.id.button_search);
        buttonSearch.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                final String username = editTextUsername.getText().toString();
                if (!TextUtils.isEmpty(username)) {
                    getStarredRepos(username);
                }
            }
        });
    }

    @Override protected void onDestroy() {
        if (subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
        super.onDestroy();
    }

    private void getStarredRepos(String username) {
        subscription = GitHubClient.getInstance()
                                   .getStarredRepos(username)
                                   .subscribeOn(Schedulers.io())
                                   .observeOn(AndroidSchedulers.mainThread())
                                   .subscribe(new Observer<List<GitHubRepo>>() {
                                       @Override public void onCompleted() {
                                           Log.d(TAG, "In onCompleted()");
                                       }

                                       @Override public void onError(Throwable e) {
                                           e.printStackTrace();
                                           Log.d(TAG, "In onError()");
                                       }

                                       @Override public void onNext(List<GitHubRepo> gitHubRepos) {
                                           Log.d(TAG, "In onNext()");
                                           adapter.setGitHubRepos(gitHubRepos);
                                       }
                                   });
    }
}

运行应用

运行该应用程序应显示一个带有输入框的屏幕, 以输入GitHub用户名。然后搜索应显示所有已加星标存储库的列表。

该应用程序的屏幕快照显示了所有已加星标的存储库的列表。

总结

我希望这对RxJava有所帮助, 并概述其基本功能。 RxJava中有许多强大的概念, 我敦促你通过更深入地研究有据可查的RxJava Wiki来探索它们。

随时在下面的评论框中留下任何问题或评论。你也可以在@arriolachris的Twitter上关注我, 在这里我对RxJava和Android的所有内容进行了很多推文。

如果你需要有关RxJava的全面学习资源, 可以在Leanpub上查看我与Angus Huang编写的电子书。

相关:十项Kotlin功能可促进Android开发

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?