EasyAndroid基礎整合元件庫之:EasyBundle 最佳Bundle存取實踐

Haoge發表於2018-06-22

什麼是EasyBundle

EasyBundle是開源基礎元件整合庫EasyAndroid中的基礎元件之一。其作用是:優雅的進行Bundle資料存取

EasyAndroid作為一款整合元件庫,此庫中所整合的元件,均包含以下特點,你可以放心使用~~

  1. 精簡: 作為一款整合庫,我不希望有那種大元件,儘量控制好整合庫的大小。不要有冗餘程式碼
  2. 內聚: 儘量減少甚至避免單一元件對別的模組進行依賴。做到元件間獨立。

得益於編碼時的高內聚性,若你只需要使用EasyBundle. 那麼可以直接去copy EasyBundle原始碼檔案到你的專案中,直接進行使用,也是沒問題的。

特性

  1. 統一存取api
  2. 支援儲存任意型別資料,打破Bundle資料限制
  3. 自動型別轉換。讀取隨心
  4. Bundle與實體類之間的雙向資料注入

用法

用法概覽

我們先來與原生使用方式進行一下對比。以便讓大家能對EasyBundle的用法有個大概的概念

假設我們有以下一批資料,需要進行儲存

型別
Int age
String name
  • 原生儲存:需要根據儲存型別不同選擇不同的api
val bundle = getBundle()
bundle.putInt("age", age)
bundle.putString("name", name)
複製程式碼
  • 使用EasyBundle進行儲存:統一儲存api。直接儲存
val bundle:Bundle = EasyBundle.create(getBundle())
	.put("age", age)
	.put("name", name)
	.getBundle()
複製程式碼
  • 原生讀取:需要根據容器中的不同型別, 選擇不同api進行讀取
val bundle = getBundle()
val age:Int = bundle.getInt("age")
val name:String = bundle.getString("name")
複製程式碼
  • 使用EasyBundle進行讀取:統一讀取api。直接讀取
val easyBundle = EasyBundle.create(getBundle())
val age = easyBundle.get<Int>("age")
val name = easyBundle.get<String>("name")
複製程式碼
  • 原生方式頁面取值
class ExampleActivity:Activity() {
	var age:Int = 0
	var name:String = ""
	
	override fun onCreate(saveInstanceState:Bundle?) {
		super.onCreate(saveInstanceState)
		intent?.let{
			age = it.getIntExtra("age", 0)
			name = it.getStringExtra("name")
		}
	}
}
複製程式碼
  • 使用EasyBundle進行頁面取值
class BaseActivity() {
	override fun onCreate(saveInstanceState:Bundle?) {
		super.onCreate(saveInstanceState)
		// 在基類中直接配置注入入口,將intent中的資料注入到配置了BundleField註解的變數中去
		EasyBundle.toEntity(this, intent?.extras)
	}
}

class ExampleActivity:BaseActivity() {
	// 在對應的欄位上新增BundleField即可
	@BundleField
	var age:Int = 0
	@BundleField
	var name:String = ""
	...
}
複製程式碼
  • 原生方式進行現場保護
class ExampleActivity:Activity() {
	var age:Int = 0
	var name:String = ""
	
	// 原生方式。需要手動一個個的進行資料儲存
	override fun onSaveInstanceState(outState: Bundle?) {
		super.onSaveInstanceState(outState)
		outState?.let{
			it.putInt("age", age)
			it.putString("name", name)
		}
	}
	
	override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
		super.onRestoreInstanceState(savedInstanceState)
		saveInstanceState?.let {
			age = it.getIntExtra("age", 0)
			name = it.getStringExtra("name")
		}
	}
}
複製程式碼
  • 使用EasyBundle進行現場保護配置
// 直接在基類中進行基礎注入配置即可
class BaseActivity() {
	override fun onSaveInstanceState(outState: Bundle?) {
		super.onSaveInstanceState(outState)
		EasyBundle.toBundle(this, outState)
	}
	
	override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
		super.onRestoreInstanceState(savedInstanceState)
		EasyBundle.toEntity(this, savedInstanceState)
	}
}
複製程式碼

以上即是EasyBundle的各種主要使用方式。希望能讓大家對EasyBundle的主要功能先有個大致瞭解。

EasyBundle例項建立說明

EasyBundle是對Bundle的存取操作進行封裝的,那麼肯定我們會需要繫結一個Bundle對應進行操作

val easyBundle:EasyBundle = EasyBundle.create(bundle)
複製程式碼

然後,通過easyBundle操作完資料後,取出操作後的bundle資料進行使用:

val bundle:Bundle = easyBundle.bundle
複製程式碼

若建立時傳遞進入的bundle為null。則將新建一個空的bundle容器進行資料儲存

fun create(source:Bundle? = null): EasyBundle {
    return EasyBundle(source?: Bundle())
}
複製程式碼

所以。我們再返回去看上面的儲存示例程式碼,就很清晰了:

val bundle:Bundle = EasyBundle.create(getBundle())
	.put("age", age)
	.put("name", name)
	.getBundle()
複製程式碼

統一存取api

從上面的示例中我們可以看得出來:相比於原生方式(需要針對不同型別資料使用不同的api進行資料存取), EasyBundle統一了存取的api:

統一儲存的三種方式

  1. 直接使用put(key:String, value:Any)方法一個個進行儲存:
easyBundle.put(key1, value1)
	.put(key2, value2)// 支援鏈式呼叫
複製程式碼
  1. 通過提供的帶可變引數的方法put(vararg params:Pair<String, Any>)進行多資料同時儲存
easyBundle.put(
	key1 to value1,
	key2 to value2
	...
)
複製程式碼
  1. 直接儲存別人傳過來的map資料put(params:Map<String, Any>)
val map:Map<String, Any> = getMap()
easyBundle.put(map)
複製程式碼

統一讀取

統一了資料的儲存入口。理所當然的,EasyBundle也統一了資料的讀取入口:

需要進行讀取時。可以通過行內函數get<T>(key:String)讀取指定資料.

比如讀取實現了Parcelable介面的User例項:

val user = easyBundle.get<User>("user")
複製程式碼

而在java環境下。因為沒有行內函數可用,所以你也可以使用get(key:String, type:Class<*>)方法進行讀取

User user = easyBundle.get("user", User.class)
複製程式碼

打破Bundle儲存資料限制

都知道,Bundle的存取api那麼複雜,主要是需要過濾掉不被系統允許的非序列化資料

所以經常性的。有時候我們在開發中,突然會需要將一個普通的實體類傳遞到下一個頁面。這個時候就會需要對這個類進行序列化修改。

雖然實際上對類進行實現序列化介面還是很簡單的。但是經常需要去實現,也是讓人神煩的。

解決辦法其實很簡單,參考經典的網路通訊模型即可:使用JSON作為中轉型別進行通訊

以下方的User為例:

class User() {
	val name:String? = null
}
複製程式碼

進行儲存

easyBundle.put("user", user)
複製程式碼

儲存時,自動對user進行型別檢查,發現此型別不被bundle所支援儲存,所以會將user通過fastjson或者gson進行json序列化轉碼後,再進行儲存.

核心原始碼展示

fun put(name:String, value:Any?) {
	...
	when (value) {
		// 首先,對於Bundle支援的資料型別。自動選擇正確的api進行儲存
		is Int -> bundle.putInt(name, value)
		is Long -> bundle.putLong(name, value)
		...
		// 對於Bundle不支援的資料型別。轉換為臨時中間JSON資料再進行儲存
		else -> bundle.putString(name, toJSON(value))
	}
}
複製程式碼

進行讀取

val user:User = easyBundle.get<User>("user")
複製程式碼

讀取時,從bundle中取出的是json字串。與指定型別User不匹配。則將通過fastjson或者gson進行json反序列化解析後。再進行返回:

除了此處所舉例的JSON資料自動轉換相容方案。還有一種是基本資料型別轉換相容:

比如當前bundle中放入了數字的字串:

easyBundle.put("number", "10086")
複製程式碼

雖然我們存入的時候是String型別資料。但是內容實際上是可以轉為int的。那麼我們也可以直接指定接受者型別為int來進行讀取:

val number:Int = easyBundle.get<Int>("number")
複製程式碼

基本型別相容的方式。在使用路由的專案下進行使用。非常好用:

因為路由框架中,url的引數部分,大部分都是直接以String的格式進行解析、傳遞的

核心原始碼展示:

fun <T> get(key:String, type:Class<T>):T? {
    var value = bundle.get(key) ?: return returnsValue(null, type) as T?
    // 當取出資料型別與指定型別匹配時。直接返回
    if (type.isInstance(value)) {
        return value as T
    }

    if (value !is String) {
        // 對於資料型別不為String的,先行轉換為json。
        value = toJSON(value)
    }

    // 處理兩種情況下的資料自動轉換:
    val result = when(type.canonicalName) {
    	// 第一種:基本資料型別資料自動轉換相容
		"byte", "java.lang.Byte" -> value.toByte()
		"short", "java.lang.Short" -> value.toShort()
		...
		// 第二種:JSON資料自動解析相容
		else -> parseJSON(value, type)
    }
    return result as T
}
複製程式碼

關於EasyBundle中,json中轉資料的說明

在EasyBundle中。並沒有直接依賴fastjsongson解析庫。而是通過在執行時進行json庫匹配。使用當前的執行環境所支援的json解析庫

// 當前執行環境下。是否存在fastjson
private val FASTJSON by lazy { return@lazy exist("com.alibaba.fastjson.JSON") }
// 當前執行環境下,是否存在gson
private val GSON by lazy { return@lazy exist("com.google.gson.Gson") }

// 進行json庫判斷。優先使用gson
private fun toJSON(value:Any) = when {
    GSON -> Gson().toJson(value)
    FASTJSON -> JSON.toJSONString(value)
    else -> throw RuntimeException("Please make sure your project support [FASTJSON] or [GSON] to be used")
}

private fun parseJSON(json:String, clazz: Class<*>) = when {
    GSON -> Gson().fromJson(json, clazz)
    FASTJSON -> JSON.parseObject(json, clazz)
    else -> throw RuntimeException("Please make sure your project support [FASTJSON] or [GSON] to be used")
}
複製程式碼

所以,完全不用擔心會引入新的不需要的庫進來。而且,相信大部分的專案中也肯定有fastjsongson至少其中一種解析庫。

雙向資料注入

EasyBundle提供了BundleField註解。用於提供雙向資料注入功能。

雙向注入的意思即是:即可以將資料從實體類中注入到bundle容器中,也可以從bundle容器中注入到實體類中:

舉個栗子,這是個普通bean類,儲存著使用者資訊:

class User(var name:String, var arg:Int, var address:String)
複製程式碼

然後。正常模式下。當我們需要將這些資料儲存到bundle中去時:

val user = getUser()
bundle.putString("name", user.name)
bundle.putInt("age", user.age)
bundle.putString("address", user.address)
複製程式碼

或者,需要從bundle中將對應的資料取出來並賦值給user:

user.name = bundle.getString("name")
user.age = bundle.getInt("age")
user.address = bundle.getString("address")
複製程式碼

但是,如果你使用EasyBundle提供的雙向資料注入功能,就很簡單了:

1. 為需要進行注入的欄位。新增註解:

class User(@BundleField var name:String, 
	@BundleField var arg:Int, 
	@BundleField var address:String)
複製程式碼

2. 將資料從User中注入到bundle中進行儲存

EasyBundle.toBundle(user, bundle)
複製程式碼

3. 將資料從bundle中,讀取並注入到User例項中去:

EasyBundle.toEntity(user, bundle)
複製程式碼

效果與上方的原始寫法一致。且更加方便、更加簡潔、更加強大

重新指定key值

一般來說。直接使用@BundleField時。預設使用的key值是欄位名

但是有時候,我們會需要對key值進行重設:

class Entity(@BundleField("reset_name") var name:String)
複製程式碼

防crash開關

在進行資料存取的過程中,很難避免不會出現存取異常。比如說。你存的是"Hello,World", 但是取的時候你卻取成了Int。或者存的是json。但是讀取的時候,進行json解析錯誤時。這些情況下都會導致丟擲不可期的異常

所以BundleField提供了throwable引數:

@BundleField(throwable = false)
var user:User
複製程式碼

throwable型別為Boolean。代表當存取時發生異常時。是否將此異常向上丟擲。(預設為false)

資料注入的使用場景

上面雖然說了那麼長一截,但是如果沒有具體的使用場景示例的支撐。可能會有部分朋友不太理解: 你說了那麼多,然而又有什麼卵用?

下面我就舉例一些使用場景。進行一些具體的說明:

1. 頁面跳轉Intent傳值

這其實可以說是主要的使用場景。在Activity中進行使用,獲取啟動時傳遞的資料:

class UserActivity:Activity() {
	@BundleField
	lateinit var name:String
	@BundleField
	lateinit var uid:String
	
	override fun onCreate(saveInstanceState:Bundle?) {
		// 將intent中的資料。注入到當前類中
		EasyBundle.toEntity(this, intent?.extras)
	}
}	
複製程式碼

當然。其實每次有個新頁面。都去寫一次EasyBundle.toEntity也是挺蛋疼的

其實注入方法是可以放入基類的。做到一次基類配置,所有子類共用

class BaseActivity:Activity() {
	override fun onCreate(saveInstanceState:Bundle?) {
		// 將intent中的資料。注入到當前類中
		EasyBundle.toEntity(this, intent?.extras)
		...
	}
}
複製程式碼

而且。使用此種方式,有個很顯著的優點:比如對於上方所示的UserActivity頁面來說。此頁面需要的資料就是nameuid,一目瞭然~

2. 現場狀態保護

照原生的方式。我們在進行現場保護時,會需要自己去將關鍵狀態資料一個個的手動存入saveInstanceState中去,需要恢復資料時,又需要一個個的去手動讀取資料.

比如像下方的頁面:

class PersonalActivity:Activity() {
	// 此類中含有部分的關鍵狀態變數
	lateinit var name:String
	var isSelf:Boolean = false
	...
		
	// 然後需要進行現場狀態保護。儲存關鍵資料:
	override fun onSaveInstanceState(outState: Bundle?) {
	    super.onSaveInstanceState(outState)
	    outState.putString("name", name)
	    outState.putBoolean("isSelf", isSelf)
	}
	// 頁面待恢復時,將資料讀取出來進行恢復
	override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
	    super.onRestoreInstanceState(savedInstanceState)
	    if (saveInstanceState == null) return
	    name = saveInstanceState.getString("name")
	    isSelf = saveInstanceState.getBoolean("isSelf")
	}
}
複製程式碼

這只是兩個變數需要儲存。如果資料量較多的環境下。這塊就得把人寫瘋。。。

EasyBundle的雙向資料注入功能,在此處就能得到非常良好的表現:

class PersonalActivity:Activity() {
	// 此類中含有部分的關鍵狀態變數
	@BundleField
	lateinit var name:String
	@BundleField
	var isSelf:Boolean = false
	...
		
	// 然後需要進行現場狀態保護。儲存關鍵資料:
	override fun onSaveInstanceState(outState: Bundle?) {
	    super.onSaveInstanceState(outState)
	    EasyBundle.toBundle(this, outState)
	}
	// 頁面待恢復時,將資料讀取出來進行恢復
	override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
	    super.onRestoreInstanceState(savedInstanceState)
	    EasyBundle.toEntity(this, savedInstanceState)
	}
}
複製程式碼

當然,推薦的做法還是將此配置到基類. 使上層的程式碼更加簡潔:

class BaseActivity:Activity() {
	override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)
        EasyBundle.toBundle(this, outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
        super.onRestoreInstanceState(savedInstanceState)
        EasyBundle.toEntity(this, savedInstanceState)
    }
}
複製程式碼

當然,你也可以擴充到任意你需要使用到的地方。

3. 相容路由跳轉引數傳遞

上面說了,EasyBundle支援了基本型別的相容邏輯。此相容邏輯,主要其實就是用來出來路由引數傳遞的問題

比如我們有以下一個路由跳轉連結:

val url = "Haoge://page/user?name=Haoge&age=18"
複製程式碼

從連結可以看出來,其實我們需要傳遞的引數有兩個:String型別的name, Int型別的age

但是路由框架可沒此目測功能,所以基本來說。解析後放入intent中傳遞的資料,都是String型別的nameage

所以照正常邏輯:我們在目標頁面。對age的取值。會需要將資料先讀取出來再進行一次轉碼後方可使用

class UserActivity:BaseActivity() {
	lateinit var name:String
	lateinit var age:Int
	
	override fun onCreate(saveInstanceState:Bundle?) {
		// 從intent中進行讀取
		name = intent.getStringExtra("name")
		age = intent.getStringExtra("age").toInt()// 需要再進行一次轉碼
	}
}
複製程式碼

而使用注入功能,則不用考慮那麼多,直接懟啊!!!

class UserActivity:BaseActivity() {
	@BundleField
	lateinit var name:String
	@BundleField // 讀取時,會進行自動轉碼
	lateinit var age:Int
}
複製程式碼

4. 指定預設值

@BundleField
var age:Int = 18 // 直接對變數指定預設資料即可
複製程式碼

混淆配置

因為自動注入操作使用了反射進行操作。所以如果需要對專案進行混淆的。記得新增上以下混淆規則:

-keep class com.haoge.easyandroid.easy.BundleField
-keepclasseswithmembernames class * {
    @com.haoge.easyandroid.easy.BundleField <fields>;
}
複製程式碼

更多使用場景。期待你的發掘~~~

相關文章