什麼是EasyBundle
EasyBundle是開源基礎元件整合庫EasyAndroid中的基礎元件之一。其作用是:優雅的進行Bundle資料存取
EasyAndroid作為一款整合元件庫,此庫中所整合的元件,均包含以下特點,你可以放心使用~~
- 精簡: 作為一款整合庫,我不希望有那種大元件,儘量控制好整合庫的大小。不要有冗餘程式碼
- 內聚: 儘量減少甚至避免單一元件對別的模組進行依賴。做到元件間獨立。
得益於編碼時的高內聚性
,若你只需要使用EasyBundle. 那麼可以直接去copy EasyBundle原始碼檔案到你的專案中,直接進行使用,也是沒問題的。
特性
- 統一存取api
- 支援儲存任意型別資料,打破Bundle資料限制
- 自動型別轉換。讀取隨心
- 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:
統一儲存的三種方式
- 直接使用
put(key:String, value:Any)
方法一個個進行儲存:
easyBundle.put(key1, value1)
.put(key2, value2)// 支援鏈式呼叫
複製程式碼
- 通過提供的帶可變引數的方法
put(vararg params:Pair<String, Any>)
進行多資料同時儲存
easyBundle.put(
key1 to value1,
key2 to value2
...
)
複製程式碼
- 直接儲存別人傳過來的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中。並沒有直接依賴fastjson
與gson
解析庫。而是通過在執行時進行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")
}
複製程式碼
所以,完全不用擔心會引入新的不需要的庫進來。而且,相信大部分的專案中也肯定有fastjson
與gson
至少其中一種解析庫。
雙向資料注入
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
頁面來說。此頁面需要的資料就是name
與uid
,一目瞭然~
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
型別的name
與age
所以照正常邏輯:我們在目標頁面。對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>;
}
複製程式碼
更多使用場景。期待你的發掘~~~