美文网首页Android Dev高级AndroidAndroidHot
使用Rx同步并缓存网络数据

使用Rx同步并缓存网络数据

作者: SpikeKing | 来源:发表于2016-01-19 08:54 被阅读2354次

    欢迎Follow我的GitHub, 关注我的简书. 其余参考Android目录.

    缓存模式

    本文的合集已经编著成书,高级Android开发强化实战,欢迎各位读友的建议和指导。在京东即可购买:https://item.jd.com/12385680.html

    Android

    RxJava是响应式编程, 在异步处理网络数据时, 使用广泛.
    我们也可以使用一些Rx的特性, 优雅地缓存网络数据.

    缓存模式: 读取数据库, 显示, 请求数据, 存储到数据库, 再更新页面.

    使用Dagger2+Retrofit+Rx的标准组合, 我来讲解一下如何使用.

    本文源码的GitHub下载地址

    动画

    1. 框架

    常规项目, 包含跳转缓存和非缓存页面, 为了模拟慢速环境, 延迟3秒加载数据.

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        // 跳转无缓存
        public void gotoNoCache(View view) {
            startActivity(new Intent(this, NocacheActivity.class));
        }
    
        // 跳转有缓存
        public void gotoCache(View view) {
            startActivity(new Intent(this, CacheActivity.class));
        }
    }
    

    2. 无缓存

    依赖注入三个关键部分, Application/Component/Module.

    public class RcApplication extends Application {
        private ApiComponent mApiComponent;
    
        @Override public void onCreate() {
            super.onCreate();
            mApiComponent = DaggerApiComponent.builder()
                    .apiModule(new ApiModule(this)).build();
        }
    
        public ApiComponent getApiComponent() {
            return mApiComponent;
        }
    }
    
    @Singleton
    @Component(modules = ApiModule.class)
    public interface ApiComponent {
        void inject(NocacheActivity activity);
    
        void inject(CacheActivity activity);
    }
    
    @Module
    public class ApiModule {
        private Application mApplication;
    
        public ApiModule(Application application) {
            mApplication = application;
        }
    
        @Provides
        @Singleton
        public Application provideApplication() {
            return mApplication;
        }
    
        @Provides
        @Singleton GitHubClient provideGitHubClient() {
            return new GitHubClient();
        }
    
        @Provides ObservableRepoDb provideObservableRepoDb() {
            return new ObservableRepoDb(mApplication);
        }
    }
    

    模块提供应用信息, GitHub的网络请求, 数据库.
    @Singleton表示单例模式, 全部注入拥有一个实例.

    页面, 使用RecyclerView显示列表信息, 在加载时显示ProgressBar.

    /**
     * 无缓存Activity
     * <p>
     * Created by wangchenlong on 16/1/18.
     */
    public class NocacheActivity extends Activity {
    
        @Bind(R.id.nocache_rv_list) RecyclerView mRvList;
        @Bind(R.id.nocache_pb_progress) ProgressBar mPbProgress;
    
        @Inject Application mApplication;
        @Inject GitHubClient mGitHubClient;
    
        private ListAdapter mListAdapter;
    
        @Override protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_nocache);
            ButterKnife.bind(this);
    
            ((RcApplication) getApplication()).getApiComponent().inject(this);
    
            LinearLayoutManager layoutManager = new LinearLayoutManager(mApplication);
            mRvList.setLayoutManager(layoutManager);
    
            mListAdapter = new ListAdapter();
            mRvList.setAdapter(mListAdapter);
        }
    
        @Override protected void onResume() {
            super.onResume();
    
            // 延迟3秒, 模拟效果
            mGitHubClient.getRepos("SpikeKing")
                    .delay(3, TimeUnit.SECONDS)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(this::onSuccess, this::onError);
    
            mPbProgress.setVisibility(View.VISIBLE);
        }
    
        private void onSuccess(ArrayList<Repo> repos) {
            mListAdapter.setRepos(repos);
            mPbProgress.setVisibility(View.INVISIBLE);
        }
    
        private void onError(Throwable throwable) {
            mPbProgress.setVisibility(View.INVISIBLE);
        }
    }
    

    通过观察可以发现, 长时间显示白屏会降低用户体验. 我来看看缓存模式.


    3. 缓存

    缓存模式: 读取数据库, 显示, 请求数据, 存储到数据库, 再更新页面.
    推荐使用脚本生成数据库处理类, 使用方式参考, 自动生成DbHelper的脚本.

    主页逻辑.

    public class CacheActivity extends Activity {
    
        @Bind(R.id.cache_rv_list) RecyclerView mRvList; // 列表
        @Bind(R.id.cache_srl_swipe) SwipeRefreshLayout mSrlSwipe; // 刷新
    
        @Inject Application mApplication;
        @Inject ObservableRepoDb mRepoDb;
        @Inject GitHubClient mGitHubClient;
    
        private ListAdapter mListAdapter; // RecyclerView适配器
    
        @Override protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_cache);
            ButterKnife.bind(this);
    
            // 注入类
            ((RcApplication) getApplication()).getApiComponent().inject(this);
    
            LinearLayoutManager layoutManager = new LinearLayoutManager(mApplication);
            mRvList.setLayoutManager(layoutManager);
    
            mListAdapter = new ListAdapter();
            mRvList.setAdapter(mListAdapter);
    
            mSrlSwipe.setOnRefreshListener(this::fetchUpdates);
        }
    
        @Override protected void onResume() {
            super.onResume();
            mRepoDb.getObservable()
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(this::setData);
    
            fetchUpdates();
            Toast.makeText(mApplication, "正在更新", Toast.LENGTH_SHORT).show();
        }
    
        // 设置数据, 更新完成会调用
        private void setData(ArrayList<Repo> repos) {
            mListAdapter.setRepos(repos);
            Toast.makeText(mApplication, "更新完成", Toast.LENGTH_SHORT).show();
        }
    
        private void fetchUpdates() {
            // 延迟3秒, 模拟效果
            mGitHubClient.getRepos("SpikeKing")
                    .delay(3, TimeUnit.SECONDS)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(mRepoDb::insertRepoList, this::fetchError, this::fetchComplete);
        }
    
        private void fetchError(Throwable throwable) {
            mSrlSwipe.setRefreshing(false);
        }
    
        private void fetchComplete() {
            mSrlSwipe.setRefreshing(false);
        }
    }
    

    数据库的观察者

    /**
     * Redo的观察者
     * <p>
     * Created by wangchenlong on 16/1/18.
     */
    public class ObservableRepoDb {
        private PublishSubject<ArrayList<Repo>> mPublishSubject; // 发表主题
        private RepoDbHelper mDbHelper; // 数据库
    
        public ObservableRepoDb(Context context) {
            mDbHelper = new RepoDbHelper(context);
            mPublishSubject = PublishSubject.create();
        }
    
        // 返回观察者
        public Observable<ArrayList<Repo>> getObservable() {
            Observable<ArrayList<Repo>> firstObservable = Observable.fromCallable(this::getRepoList);
            return firstObservable.concatWith(mPublishSubject); // 连接发表主题
        }
    
        // 从数据库获得数据
        private ArrayList<Repo> getRepoList() {
            mDbHelper.openForRead();
            ArrayList<Repo> repos = new ArrayList<>();
            Cursor c = mDbHelper.getAllRepo();
            if (!c.moveToFirst()) {
                return repos; // 返回空
            }
    
            do {
                // 添加数据
                repos.add(new Repo(
                        c.getString(RepoDbHelper.REPO_ID_COLUMN_POSITION),
                        c.getString(RepoDbHelper.REPO_NAME_COLUMN_POSITION),
                        c.getString(RepoDbHelper.REPO_DESCRIPTION_COLUMN_POSITION),
                        new Repo.Owner(c.getString(RepoDbHelper.REPO_OWNER_COLUMN_POSITION), "", "", "")));
            } while (c.moveToNext());
            c.close();
            mDbHelper.close();
            return repos;
        }
    
        // 插入Repo列表
        public void insertRepoList(ArrayList<Repo> repos) {
            mDbHelper.open();
            mDbHelper.removeAllRepo();
            for (Repo repo : repos) {
                mDbHelper.addRepo(
                        repo.getId(),
                        repo.getName(),
                        repo.getDescription(),
                        repo.getOwner().getLogin()
                );
            }
            mDbHelper.close();
            mPublishSubject.onNext(repos); // 会调用更新数据
        }
    }
    

    这一部分是关键, 实现网络请求同步插入数据库和更新页面.
    关联PublishSubject, 在插入数据完成后, 调用绑定观察者, 更新页面.
    .concatWith(mPublishSubject)mPublishSubject.onNext(repos).


    Rx在处理网络请求方面, 确实非常优雅, 值得喜欢完美的人使用.

    OK, that's all! Enjoy it.

    相关文章

      网友评论

      • xiaozhj:不知道我有没有理解出错,楼主代码那里网络请求成功后的插入db操作好像是在主线程中进行的吧?如果是的话,大数据量的时候会出问题的喔。
        xiaozhj:由于我是才开始接触rxjava,所以首先挺感谢楼主的无私分享,也让我在rx的使用上发现了新姿势,前几天都是停留在不停的new subcribe来接收订阅事件的阶段,今天早上看到楼主的代码,才知道用rx的新姿势。于是又去搜了相关资料再学习了一下。后面根据楼主的代码风格样式,依葫芦画瓢,做了下改动,把插入数据库那块操作改成在非主线程操作,如果有不恰当的地方或者有更好的处理方案,还请多多指点一下新姿势~~感激。代码如下:
        //首先在CacheActivity里添加这个方法,加这个方法真的就是纯粹为了能够像楼主的那样
        //简洁的调用三个方法就能免去手动new subcribe的操作。这里有些略无奈
        private void doNothing(ArrayList<Repo> repos){
        Log.i("RxTag", "onNext:"+Thread.currentThread().getName());
        }
        然后就是修改CacheActivity里的fetchUpdates方法
        private void fetchUpdates() {
        Log.i("RxTag", "update:" + Thread.currentThread().getName());
        // 延迟3秒, 模拟效果
        mGitHubClient.getRepos("SpikeKing")
        .delay(3, TimeUnit.SECONDS)
        //主要就是增加doOnNext操作,让其能够在非主线程中先执行插入数据库的操作
        .doOnNext(repos -> mRepoDb.insertRepoList(repos))
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(this::doNothing, this::fetchError, this::fetchComplete);
        }
      • haizhiyun:有没有一个方案链试请求,将缓存和网络结合起来通过一个观察者返回。网上有很多使用contact得方案来连接两者,不过好像未考虑一个问题:那就是网络出错的时候,很大概率上回先调用onError,而缓存数据没法返回(都说contact是顺序执行的,测试中并不如此,如果网络产生错误的速度较快,那么就会先回调onError,不知道那个顺序执行是怎么理解)
      • 天之大任:应该先来一个Dragger的教程,要不看不懂,嘿嘿
        SpikeKing:@天之大任 有呀, 看看目录, 里面有一个Dagger的教程.

      本文标题:使用Rx同步并缓存网络数据

      本文链接:https://www.haomeiwen.com/subject/lqikkttx.html