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

简介

背景

这几年 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容