1. 程式人生 > >Kotlin入門(25)共享引數模板

Kotlin入門(25)共享引數模板

共享引數SharedPreferences是Android最簡單的資料儲存方式,常用於存取“Key-Value”鍵值對資料。在使用共享引數之前,要先呼叫getSharedPreferences方法宣告檔名與操作模式,示例程式碼如下:

    SharedPreferences sps = getSharedPreferences("share", Context.MODE_PRIVATE);

該方法的第一個引數是檔名,例子中的share表示當前的共享引數檔案是share.xml;第二個引數是操作模式,一般填MODE_PRIVATE表示私有模式。
共享引數若要儲存資料則需藉助於Editor類,示例的Java程式碼如下:

    SharedPreferences.Editor editor = sps.edit();
    editor.putString("name", "阿四");
    editor.putInt("age", 25);
    editor.putBoolean("married", false);
    editor.putFloat("weight", 50f);
    editor.commit(); 

使用共享引數讀取資料則相對簡單,直接呼叫其物件的get方法即可獲取資料,注意get方法的第二個引數表示預設值,示例的Java程式碼如下:

    String name = sps.getString("name", "");
    int age = sps.getInt("age", 0);
    boolean married = sps.getBoolean("married", false);
    float weight = sps.getFloat("weight", 0);

從上述資料讀寫的程式碼可以看出,共享引數的存取操作有些繁瑣,因此實際開發常將共享引數相關操作提取到一個工具類,在新的工具類中封裝SharedPreferences的常用操作,下面便是一個共享引數工具類的Java程式碼例子:

public class SharedUtil {
    private static SharedUtil mUtil;
    private static SharedPreferences mShared;
    
    public static SharedUtil getIntance(Context ctx) {
        if (mUtil == null) {
            mUtil = new SharedUtil();
        }
        mShared = ctx.getSharedPreferences("share", Context.MODE_PRIVATE);
        return mUtil;
    }

    public void writeShared(String key, String value) {
        SharedPreferences.Editor editor = mShared.edit();
        editor.putString(key, value);
        editor.commit(); 
    }

    public String readShared(String key, String defaultValue) {
        return mShared.getString(key, defaultValue);
    }
}

有了共享引數工具類,外部讀寫SharedPreferences就比較方便了,比如下面的Java程式碼,無論是往共享引數寫資料還是從共享引數讀資料,均只要一行程式碼:

    //呼叫工具類寫入共享引數
    SharedUtil.getIntance(this).writeShared("name", "阿四");
    //呼叫工具類讀取共享引數
    String name = SharedUtil.getIntance(this).readShared("name", "");

不過這個工具類並不完善,因為它只支援字串String型別的資料讀寫,並不支援整型、浮點數、布林型等其它型別的資料讀寫。另外,如果外部需要先讀取某個欄位的數值,等處理完了再寫回共享引數,則使用工具類也要兩行程式碼(一行讀資料、一行寫資料),依舊有欠簡潔。找毛病其實都是容易的,如果仍然使用Java編碼,能完善的就完善,不能完善的也不必苛求了。
之所以挑Java實現方式的毛病,倒不是因為看它不順眼整天吹毛求疵,而是因為Kotlin有更好的解決辦法。為了趁熱打鐵方便比較兩種方式的優劣,下面開門見山直接給出Kotlin封裝共享引數的實現程式碼例子:

class Preference<T>(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> {

    val prefs: SharedPreferences by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(name, default)
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }

    private fun <T> findPreference(name: String, default: T): T = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }
        return res as T
    }

    private fun <T> putPreference(name: String, value: T) = with(prefs.edit()) {
        //putInt、putString等方法返回Editor物件
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }.apply() //commit方法和apply方法都表示提交修改
    }
}

外部在使用該工具類之時,可在Activity程式碼中宣告來自於Preference的委託屬性,委託屬性一旦宣告,則它的初始值便是從共享引數讀取的數值;後續程式碼若給委託屬性賦值,則立即觸發寫入動作,把該屬性的最新值儲存到共享引數中。於是外部操作共享引數的某個欄位,真正要書寫的僅僅是下面的一行委託屬性宣告程式碼:

    //宣告字串型別的委託屬性
    private var name: String by Preference(this, "name", "")
    //宣告整型數型別的委託屬性
    private var age: Int by Preference(this, "age", 0)

 

既然Kotlin對共享引數的處理也如此傳神,那麼大家肯定很好奇,這個高大上的Preference究竟運用了哪些黑科技呢?且待筆者下面細細道來:
一、模板類
因為共享引數允許儲存的資料型別包括整型、浮點數、字串等等,所以Preference定義成模板類,具體的引數型別在呼叫之時再指定。
除卻代表模板類泛型的T,該類中還有兩個與之相似的元素,分別是Any和*,各自表示不同的涵義。下面簡單說明一下T、Any和*三者之間的區別:
1、T是抽象的泛型,在模板類中用來佔位子,外部呼叫模板類時才能確定T的具體型別;
2、Any是Kotlin的基本型別,所有Kotlin類都從Any派生而來,故而它相當於Java裡面的Object;
3、*星號表示一個不確定的型別,同樣也是在外部呼叫時才能確定,這點跟T比較像,但T出現在模板類的定義中,而*與模板類無關,它出現在單個函式定義的引數列表中,因此星號相當於Java裡面的問號?;

二、委託屬性/屬性代理
注意到外部利用Preference宣告引數欄位時,後面跟著表示式“by Preference(...)”,這個by表示代理動作,早在第五章的“5.3.4 介面代理”就介紹瞭如何讓類通過關鍵字by實現指定介面的代理,當時舉例說明給不同的鳥類賦予不同的動作。第五章的例子是介面代理或稱類代理,而這裡則為屬性代理,所謂屬性代理,是說該屬性的型別不變,但是屬性的讀寫行為被後面的類接管了。
為什麼需要接管屬性的讀寫行為呢?舉個例子,市民每個月都要交電費,自己每月跑去電力營業廳交錢顯然夠嗆,於是後來支援在電力網站上自助繳費,然而上網繳費仍顯麻煩,因為需要使用者主動上網付費,要是使用者忘記就不好辦了。所以很多銀行都推出了“委託代扣”的業務,只要使用者跟銀行簽約並指定委託扣費的電力賬戶,那麼在每個月指定時間,銀行會自動從使用者銀行卡中扣費並繳納給指定的電力賬戶,如此省卻了使用者的人工操作。
現實生活中的委託扣費場景,對應到共享引數這裡,開發者的人工操作指的是手工編碼從SharedPreferences類讀取資料和儲存資料,而自動操作指的是約定代理的屬性自動通過模板類Preference<T>完成資料的讀取和儲存,也就是說,Preference<T>接管了這些屬性的讀寫行為,接管後的操作則是模板類的getValue和setValue方法。屬性被接管的行為叫做屬性代理,而被代理的屬性稱作委託屬性。

三、關鍵字lazy
模板類Preference<T>聲明瞭一個共享引數的prefs物件,其中用到了關鍵字lazy,lazy的意思是懶惰,表示只在該屬性第一次使用時執行初始化。聯想到Kotlin還有類似的關鍵字名叫lateinit,意思是延遲初始化,加上lazy可以歸納出Kotlin變數的三種初始化操作,具體說明如下:
1、宣告時賦值:這是最常見的變數初始化,在宣告某個變數時,立即在後面通過等號“=”給它賦予具體的值。
2、lateinit延遲初始化:變數宣告時沒有馬上賦值,但該變數仍是個非空變數,何時初始化由開發者編碼決定。
3、lazy首次使用時初始化:宣告變數時指定初始化動作,但該動作要等到變數第一次使用時才進行初始化。
此處的prefs物件使用lazy規定了屬性值在首次使用時初始化,且初始化動作通過by後面的表示式來指定,即“{ context.getSharedPreferences("default", Context.MODE_PRIVATE) }”。連同大括號在內的這個表示式,其實是個匿名例項,它內部定義了prefs物件的初始化語句,並返回SharedPreferences型別的變數值。

四、with函式
with函式的書寫格式形如“with(函式頭語句) { 函式體語句 }”,看這架勢,with方法的函式語句分為兩部分,詳述如下:
1、函式頭語句:頭部語句位於緊跟with的圓括號內部。它先於函式體語句執行,並且頭部語句返回一個物件,函式體語句在該物件的名稱空間中執行;即體語句可以直接呼叫該物件的方法,而無需顯式指定該物件的例項名稱。
2、函式體語句:體語句位於常規的大括號內部。它要等頭部語句執行完畢才會執行,同時體語句在頭部語句返回物件的名稱空間中執行;即體語句允許直接呼叫頭部物件的方法,而無需顯式指定該物件的例項名稱。
綜上所述,在模板類Preference<T>的編碼過程中,聯合運用了Kotlin的多項黑科技,方才實現了優於Java的共享引數操作方式。