《安卓-深入淺出MVVM教程》應(yīng)用篇-03 Cache (本地緩存)

簡(jiǎn)介

背景

這幾年 MVP 架構(gòu)在安卓界非常流行,幾乎已經(jīng)成為主流框架,它讓業(yè)務(wù)邏輯 和 UI操作相對(duì)獨(dú)立,使得代碼結(jié)構(gòu)更清晰。


MVVM 在前端火得一塌糊涂,而在安卓這邊卻基本沒(méi)見(jiàn)到幾個(gè)人在用,看到介紹 MVVM 也最多是講 DataBinding 或 介紹思想的。偶爾看到幾篇提到應(yīng)用的,還是對(duì)谷歌官網(wǎng)的Architecture Components 文章的翻譯。

相信大家看別人博客或官方文檔的時(shí)候,總會(huì)碰到一些坑。要么入門教程寫(xiě)得太復(fù)雜(無(wú)力吐槽,前面寫(xiě)一堆原理,各種高大上的圖,然并卵,到實(shí)踐部分一筆帶過(guò),你確定真的是入門教程嗎)。要么就是簡(jiǎn)單得就是一個(gè) hello world,然后就沒(méi)有下文了(看了想罵人)。


實(shí)在看不下去的我,決定插手你的人生。

目錄

《安卓-深入淺出MVVM教程》大致分兩部分:應(yīng)用篇、原理篇。
采用循序漸進(jìn)方式,內(nèi)容深入淺出,符合人類學(xué)習(xí)規(guī)律,希望大家用最少時(shí)間掌握 MVVM。

應(yīng)用篇:

01 Hello MVVM (快速入門)
02 Repository (數(shù)據(jù)倉(cāng)庫(kù))
03 Cache (本地緩存)
04 State Lcee (加載/空/錯(cuò)誤/內(nèi)容視圖)
05 Simple Data Source (簡(jiǎn)單的數(shù)據(jù)源)
06 Load More (加載更多)
07 DataBinding (數(shù)據(jù)與視圖綁定)
08 RxJava2
09 Dragger2
10 Abstract (抽象)
11 Demo (例子)
12-n 待定(歡迎 github 提建議)

原理篇

01 MyLiveData(最簡(jiǎn)單的LiveData)
02-n 待定(并不是解讀源碼,那樣太無(wú)聊了,打算帶你從0擼一個(gè) Architecture)

關(guān)于提問(wèn)

本人水平和精力有限,如果有大佬發(fā)現(xiàn)哪里寫(xiě)錯(cuò)了或有好的建議,歡迎在本教程附帶的 github倉(cāng)庫(kù) 提issue。
What?為什么不在博客留言?考慮到國(guó)內(nèi)轉(zhuǎn)載基本無(wú)視版權(quán)的情況,一般來(lái)說(shuō)你都不是在源出處看到這篇文章,所以留言我也一般是看不到的。

教程附帶代碼

https://github.com/ittianyu/MVVM

應(yīng)用篇放在 app 模塊下,原理篇放在 implementation 模塊下。
每一節(jié)代碼采用不同包名,相互獨(dú)立。

前言

上一節(jié)我們加入了遠(yuǎn)程數(shù)據(jù)源,那么本地?cái)?shù)據(jù)源(緩存)呢?。
一般來(lái)說(shuō),緩存可以是直接存文件,也可以用數(shù)據(jù)庫(kù)。因?yàn)楣雀枞彝爸袔Я艘粋€(gè) ROOM 數(shù)據(jù)庫(kù),所以這一節(jié)我們用 ROOM 來(lái)實(shí)現(xiàn)緩存。

環(huán)境配置

為了使用 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 是一個(gè) ORM 數(shù)據(jù)庫(kù)框架,支持返回 LiveData 數(shù)據(jù)。

我們可以直接通過(guò)注解來(lái)定義表

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

Dao

然后定義 Dao 來(lái)操作表

@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);
}

可以看到完全是基于注解的,我們甚至都不需要自己來(lái)實(shí)現(xiàn)類,所以還是比較方便的。而且還直接查詢返回 LiveData,這就省去我們自己轉(zhuǎn)換了。

數(shù)據(jù)庫(kù)

表和操作都寫(xiě)好了,那么怎么使用呢?

首先得有庫(kù)

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

}

定義好庫(kù)之后,我們需要?jiǎng)?chuàng)建這個(gè)庫(kù)
為了以后方便使用,所以寫(xiě)了一個(gè)工具類

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;
    }
}

一般來(lái)說(shuō),可以在 Application 或 Activity 中初始化

Service

既然已經(jīng)能直接通過(guò) dao 獲取到數(shù)據(jù)了,為什么還要加一層 service? 硬搬后端那套?
實(shí)際上直接使用 Dao 還是有點(diǎn)問(wèn)題的,ROOM 不允許在主線程中進(jìn)行操作,查詢返回 LiveData 是沒(méi)問(wèn)題的,但是別忘了我們還有 Long add(User user); 這么一個(gè)方法,直接在 ViewModel 中調(diào)用是會(huì)拋異常的。
所以 Service 這里可以起到適配器的作用。

public interface UserService {
    LiveData<Long> add(User user);

    LiveData<User> queryByUsername(String username);
}

沒(méi)錯(cuò),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);
    }

}

轉(zhuǎn)換過(guò)程其實(shí)就是用 AsyncTask 來(lái)實(shí)現(xiàn)其他線程執(zhí)行,然后切換回主線程。當(dāng)然你也可以使用其他切換線程的方法。你甚至可以用 LiveData 自帶的 postValue 來(lái)切換線程,也就是你只要 new 一個(gè)新線程執(zhí)行完成后 postValue 來(lái)設(shè)置值就可以。

DataSource

包結(jié)構(gòu)整理

到現(xiàn)在為止,你會(huì)發(fā)現(xiàn),有兩個(gè)數(shù)據(jù)源了。
是不是結(jié)構(gòu)有點(diǎn)混亂了?
我們整理一下包結(jié)構(gòu)

repository

  • local
    • dao
    • db
    • service
  • remote

統(tǒng)一數(shù)據(jù)源

為了方便對(duì)外提供統(tǒng)一接口,我們定義一個(gè) DataSource 接口

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

分別在定義Local 和 Remote 數(shù)據(jù)源實(shí)現(xiàn)類

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);
    }
}

因?yàn)樯弦还?jié)已經(jīng)寫(xiě)過(guò) LiveData<User> getUser(String username) 這樣一個(gè)方法,其實(shí)這里就是把這方法復(fù)制過(guò)來(lái),改了個(gè)名。

但有一點(diǎn)需要注意,在遠(yuǎn)程訪問(wèn)數(shù)據(jù)成功之后,別忘了給本地源加入數(shù)據(jù)。

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,用統(tǒng)一的 DataSource 來(lái)獲取數(shù)據(jù)。

這就涉及到什么時(shí)候用本地和遠(yuǎn)程的問(wèn)題了。
秉著簡(jiǎn)單點(diǎn)的原則,我們假設(shè)沒(méi)網(wǎng)時(shí)用本地源,有網(wǎng)用遠(yuǎn)程源。

所以我們需要一個(gè)工具來(lái)檢測(cè)是否有網(wǎng)。

注意要加上查看網(wǎng)絡(luò)狀態(tài)的權(quán)限。

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

這個(gè)工具類網(wǎng)上一大把,不再解釋。

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;
    }

}

為了檢查網(wǎng)絡(luò)狀況,我們需要 Context,所以加入了一個(gè) init 方法,而 getUser 則直接調(diào)用數(shù)據(jù)源。

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);
        }
    }

}

初始化

因?yàn)榧尤肓?DB 和 網(wǎng)絡(luò)檢測(cè),所以我們需要傳入 context,所以需要在 Application 或 Activity 中初始化一次。

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

總結(jié)

上面這般折騰之后,你會(huì)發(fā)現(xiàn)在 View 和 ViewModel 基本不用做修改,這就是職責(zé)分離的好處。

博主:大家再見(jiàn),后會(huì)有期!
讀者:博主,且慢,我還有幾個(gè)問(wèn)題。怎么重新請(qǐng)求數(shù)據(jù),我想查其他 username 的信息,我還想在請(qǐng)求失敗 或 用戶不存在的時(shí)候顯示其他視圖,怎么破?
博主:欲知后事如何,請(qǐng)聽(tīng)下回分解。2333333

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容