EasyAndroid基礎集成組件庫之:EasyBundle 最佳Bundle存取實踐

什麼是EasyBundle

EasyBundle是開源基礎組件集成庫EasyAndroid中的基礎組件之一。其做用是:優雅的進行Bundle數據存取java

EasyAndroid做爲一款集成組件庫,此庫中所集成的組件,均包含如下特色,你能夠放心使用~~android

  1. 精簡: 做爲一款集成庫,我不但願有那種大組件,儘可能控制好集成庫的大小。不要有冗餘代碼
  2. 內聚: 儘可能減小甚至避免單一組件對別的模塊進行依賴。作到組件間獨立。

得益於編碼時的高內聚性,若你只須要使用EasyBundle. 那麼能夠直接去copy EasyBundle源碼文件到你的項目中,直接進行使用,也是沒問題的。git

特性

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

用法

用法概覽

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

假設咱們有如下一批數據,須要進行存儲json

類型
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的主要功能先有個大體瞭解。api

EasyBundle實例建立說明

EasyBundle是對Bundle的存取操做進行封裝的,那麼確定咱們會須要綁定一個Bundle對應進行操做bash

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())
}
複製代碼

因此。咱們再返回去看上面的存儲示例代碼,就很清晰了:ide

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>;
}
複製代碼

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

相關文章
相關標籤/搜索