美文网首页架构与框架
《安卓-深入浅出MVVM教程》应用篇-03 Cache (本地缓

《安卓-深入浅出MVVM教程》应用篇-03 Cache (本地缓

作者: IT天宇 | 来源:发表于2017-10-23 23:59 被阅读841次

    简介

    背景

    这几年 MVP 架构在安卓界非常流行,几乎已经成为主流框架,它让业务逻辑 和 UI操作相对独立,使得代码结构更清晰。


    MVVM 在前端火得一塌糊涂,而在安卓这边却基本没见到几个人在用,看到介绍 MVVM 也最多是讲 DataBinding 或 介绍思想的。偶尔看到几篇提到应用的,还是对谷歌官网的Architecture Components 文章的翻译。

    相信大家看别人博客或官方文档的时候,总会碰到一些坑。要么入门教程写得太复杂(无力吐槽,前面写一堆原理,各种高大上的图,然并卵,到实践部分一笔带过,你确定真的是入门教程吗)。要么就是简单得就是一个 hello world,然后就没有下文了(看了想骂人)。


    实在看不下去的我,决定插手你的人生。

    目录

    《安卓-深入浅出MVVM教程》大致分两部分:应用篇、原理篇。
    采用循序渐进方式,内容深入浅出,符合人类学习规律,希望大家用最少时间掌握 MVVM。

    应用篇:

    01 Hello MVVM (快速入门)
    02 Repository (数据仓库)
    03 Cache (本地缓存)
    04 State Lcee (加载/空/错误/内容视图)
    05 Simple Data Source (简单的数据源)
    06 Load More (加载更多)
    07 DataBinding (数据与视图绑定)
    08 RxJava2
    09 Dragger2
    10 Abstract (抽象)
    11 Demo (例子)
    12-n 待定(欢迎 github 提建议)

    原理篇

    01 MyLiveData(最简单的LiveData)
    02-n 待定(并不是解读源码,那样太无聊了,打算带你从0撸一个 Architecture)

    关于提问

    本人水平和精力有限,如果有大佬发现哪里写错了或有好的建议,欢迎在本教程附带的 github仓库 提issue。
    What?为什么不在博客留言?考虑到国内转载基本无视版权的情况,一般来说你都不是在源出处看到这篇文章,所以留言我也一般是看不到的。

    教程附带代码

    https://github.com/ittianyu/MVVM

    应用篇放在 app 模块下,原理篇放在 implementation 模块下。
    每一节代码采用不同包名,相互独立。

    前言

    上一节我们加入了远程数据源,那么本地数据源(缓存)呢?。
    一般来说,缓存可以是直接存文件,也可以用数据库。因为谷歌全家桶中带了一个 ROOM 数据库,所以这一节我们用 ROOM 来实现缓存。

    环境配置

    为了使用 ROOM,你需要引入

    // room
    compile "android.arch.persistence.room:runtime:$rootProject.room"
    annotationProcessor "android.arch.persistence.room:compiler:$rootProject.room"
    
    ext {
        ...
        room = '1.0.0-rc1'
        ...
    }
    

    ROOM

    ROOM 是一个 ORM 数据库框架,支持返回 LiveData 数据。

    我们可以直接通过注解来定义表

    @Entity(tableName = "user")
    public class User implements Serializable {
        @PrimaryKey
        private int id;
    ...
    }
    

    Dao

    然后定义 Dao 来操作表

    @Dao
    public interface UserDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)// cache need update
        Long add(User user);
    
        @Query("select * from user where login = :username")
        LiveData<User> queryByUsername(String username);
    }
    

    可以看到完全是基于注解的,我们甚至都不需要自己来实现类,所以还是比较方便的。而且还直接查询返回 LiveData,这就省去我们自己转换了。

    数据库

    表和操作都写好了,那么怎么使用呢?

    首先得有库

    @Database(entities = {User.class}, version = 1, exportSchema = false)
    public abstract class DB extends RoomDatabase {
        public abstract UserDao getUserDao();
    
    }
    

    定义好库之后,我们需要创建这个库
    为了以后方便使用,所以写了一个工具类

    public class DBHelper {
        private static final DBHelper instance = new DBHelper();
        private static final String DATABASE_NAME = "c_cache";
    
        private DBHelper() {
    
        }
    
        public static DBHelper getInstance() {
            return instance;
        }
    
        private DB db;
    
        public void init(Context context) {
            db = Room.databaseBuilder(context.getApplicationContext(), DB.class, DATABASE_NAME).build();
        }
    
        public DB getDb() {
            return db;
        }
    }
    

    一般来说,可以在 Application 或 Activity 中初始化

    Service

    既然已经能直接通过 dao 获取到数据了,为什么还要加一层 service? 硬搬后端那套?
    实际上直接使用 Dao 还是有点问题的,ROOM 不允许在主线程中进行操作,查询返回 LiveData 是没问题的,但是别忘了我们还有 Long add(User user); 这么一个方法,直接在 ViewModel 中调用是会抛异常的。
    所以 Service 这里可以起到适配器的作用。

    public interface UserService {
        LiveData<Long> add(User user);
    
        LiveData<User> queryByUsername(String username);
    }
    

    没错,add 也返回 LiveData

    public class UserServiceImpl implements UserService {
        private static final UserServiceImpl instance = new UserServiceImpl();
    
        private UserServiceImpl() {
        }
    
        public static UserServiceImpl getInstance() {
            return instance;
        }
    
    
        private UserDao userDao = DBHelper.getInstance().getDb().getUserDao();
    
        @Override
        public LiveData<Long> add(final User user) {
            // transfer long to LiveData<Long>
            final MutableLiveData<Long> data = new MutableLiveData<>();
            new AsyncTask<Void, Void, Long>() {
                @Override
                protected Long doInBackground(Void... voids) {
                    return userDao.add(user);
                }
    
                @Override
                protected void onPostExecute(Long rowId) {
                    data.setValue(rowId);
                }
            }.execute();
            return data;
        }
    
        @Override
        public LiveData<User> queryByUsername(String username) {
            return userDao.queryByUsername(username);
        }
    
    }
    

    转换过程其实就是用 AsyncTask 来实现其他线程执行,然后切换回主线程。当然你也可以使用其他切换线程的方法。你甚至可以用 LiveData 自带的 postValue 来切换线程,也就是你只要 new 一个新线程执行完成后 postValue 来设置值就可以。

    DataSource

    包结构整理

    到现在为止,你会发现,有两个数据源了。
    是不是结构有点混乱了?
    我们整理一下包结构

    repository

    • local
      • dao
      • db
      • service
    • remote

    统一数据源

    为了方便对外提供统一接口,我们定义一个 DataSource 接口

    public interface UserDataSource {
        LiveData<User> queryUserByUsername(String username);
    }
    

    分别在定义Local 和 Remote 数据源实现类

    public class LocalUserDataSource implements UserDataSource {
        private static final LocalUserDataSource instance = new LocalUserDataSource();
        private LocalUserDataSource() {
        }
        public static LocalUserDataSource getInstance() {
            return instance;
        }
    
    
        private UserService userService = UserServiceImpl.getInstance();
    
        @Override
        public LiveData<User> queryUserByUsername(String username) {
            return userService.queryByUsername(username);
        }
    
        public LiveData<Long> addUser(User user) {
            return userService.add(user);
        }
    }
    

    因为上一节已经写过 LiveData<User> getUser(String username) 这样一个方法,其实这里就是把这方法复制过来,改了个名。

    但有一点需要注意,在远程访问数据成功之后,别忘了给本地源加入数据。

    public class RemoteUserDataSource implements UserDataSource {
        private static final RemoteUserDataSource instance = new RemoteUserDataSource();
        private RemoteUserDataSource() {
        }
        public static RemoteUserDataSource getInstance() {
            return instance;
        }
    
    
        private UserApi userApi = RetrofitFactory.getInstance().create(UserApi.class);
    
        @Override
        public LiveData<User> queryUserByUsername(String username) {
            final MutableLiveData<User> data = new MutableLiveData<>();
            userApi.queryUserByUsername(username)
                    .enqueue(new Callback<User>() {
                        @Override
                        public void onResponse(Call<User> call, Response<User> response) {
                            User user = response.body();
                            if (null == user)
                                return;
                            data.setValue(user);
                            // update cache
                            LocalUserDataSource.getInstance().addUser(user);
                        }
    
                        @Override
                        public void onFailure(Call<User> call, Throwable t) {
                            t.printStackTrace();
                        }
                    });
            return data;
        }
    }
    

    Repository

    然后要修改 Repository,用统一的 DataSource 来获取数据。

    这就涉及到什么时候用本地和远程的问题了。
    秉着简单点的原则,我们假设没网时用本地源,有网用远程源。

    所以我们需要一个工具来检测是否有网。

    注意要加上查看网络状态的权限。

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    

    这个工具类网上一大把,不再解释。

    public class NetworkUtils {
    
        public static boolean isConnected(Context context) {
            if (context != null) {
                ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                        .getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
                if (mNetworkInfo != null) {
                    return mNetworkInfo.isAvailable();
                }
            }
            return false;
        }
    
    }
    

    为了检查网络状况,我们需要 Context,所以加入了一个 init 方法,而 getUser 则直接调用数据源。

    public class UserRepository {
        private static final UserRepository instance = new UserRepository();
        private UserRepository() {
        }
        public static UserRepository getInstance() {
            return instance;
        }
    
    
        private Context context;
        private UserDataSource remoteUserDataSource = RemoteUserDataSource.getInstance();
        private UserDataSource localUserDataSource = LocalUserDataSource.getInstance();
    
        public void init(Context context) {
            this.context = context.getApplicationContext();
        }
    
        public LiveData<User> getUser(String username) {
            if (NetworkUtils.isConnected(context)) {
                return remoteUserDataSource.queryUserByUsername(username);
            } else {
                return localUserDataSource.queryUserByUsername(username);
            }
        }
    
    }
    

    初始化

    因为加入了 DB 和 网络检测,所以我们需要传入 context,所以需要在 Application 或 Activity 中初始化一次。

    private void initData() {
        DBHelper.getInstance().init(this);
        UserRepository.getInstance().init(this);
    ...
    }
    

    总结

    上面这般折腾之后,你会发现在 View 和 ViewModel 基本不用做修改,这就是职责分离的好处。

    博主:大家再见,后会有期!
    读者:博主,且慢,我还有几个问题。怎么重新请求数据,我想查其他 username 的信息,我还想在请求失败 或 用户不存在的时候显示其他视图,怎么破?
    博主:欲知后事如何,请听下回分解。2333333

    相关文章

      网友评论

        本文标题:《安卓-深入浅出MVVM教程》应用篇-03 Cache (本地缓

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