好久沒寫東西了,主要是 Compose 更新變化太快,官方文檔更新都跟不上代碼,寫相關的內容立刻就會過期。java
可是在 Compose Demo 項目中不是隻有 UI,數據層面逐步增長了 Room 和 DataStore,DataStore 已經迎來 beta 版本,想提早了解一下的話,如今正是時候。android
DataStore 的目標是成爲 SharedPreference 的上位替代,用於少許 key-value 數據的存儲,存儲方式是本地文件。這聽起來和 SharedPreference 並無本質區別,事實上也確實如此,由於兩者的區別在 API 的封裝上。SharedPreference 的實現至關簡單,存取值得時候經過類型消除和類型強轉保證 xml 的文件內容和 Java 的數據類型匹配,沒法保證安全性。在讀寫數據時主要提供同步的 API,在 UI 線程讀取較大內容的時候會有明顯卡頓,而異步的 apply 寫入又難以確認寫入完成的時間。git
DataStore 從設計之初就想避免上述問題,也所以變得相對複雜,提升了使用門檻,要不要換恐怕還得從業務角度考慮,如今僅作了解便可。程序員
首先,DataStore 提供了兩種不一樣數據結構的實現,分別是 Preferences 實現和 Proto 實現。其中 Preferences 的實現較爲簡單,不須要提早定義數據類型,也不能保證類型安全,這方面跟 SharedPreference 一致;Proto 實現則是使用 protobuf 預約義要保存的數據,能夠保證類型正確,代價是代碼更加複雜。github
其次,DataStore 深度綁定 Kotlin 協程,讀取數據和寫入數據的函數都是 suspend 函數,讀數據的返回值直接採用了 Flow,統一了單次獲取和持續監聽。web
DataStore 的兩種實現對應着不一樣的依賴,我以爲 Preferences 實現像是一種向易用性妥協的方案,真的想要簡單好用不如仍是用 SharedPreference,因此要學就一步到位,用 Proto 實現。json
Proto 實現的使用方式分爲如下幾步:安全
比起 SharedPreference 確實多了不少步驟,但流程還算清晰,真正的坑都潛伏在細節中。markdown
implementation("androidx.datastore:datastore:1.0.0-beta02") // 截止到20210626的最新版
複製代碼
protobuf 的全稱是 Protocal Buffers,詳細信息參考官網:developers.google.com/protocol-bu…數據結構
protobuf 已經有不少應用場景了,最廣爲人知的應該是 gRPC。比起 xml 或者 json,protobuf 更節省空間,速度更快。
具體的語法不在本文範圍內,按需學習便可。舉個例子,我用來保存用戶我的配置的一個 .proto 文件內容以下:
syntax = "proto3";
option java_package = "com.github.moqigit.timecount.datastore";
option java_multiple_files = true;
message UserPreferences {
int64 lastSessionTime = 1;
bool anti_anxious = 2;
bool check_update = 3;
bool cloud_storage = 4;
string primary_color = 5;
string light_color = 6;
string dark_color = 7;
}
複製代碼
protobuf 的使用方法是先定義數據結構,再編譯成指定語言供開發者使用。(暫不支持 Kotlin,咱們用 Java 便可)
按照官方文檔安裝後,咱們能夠在終端用 protoc 命令編譯 .proto 文件
Usage: protoc [OPTION] PROTO_FILES
每次改動都敲命令或者執行腳本編譯 proto 也是個比較麻煩的事,程序員應該向自動化努力。gradle 構建系統已經有 protobuf 的插件,"簡單"添加一下就能夠解放雙手。
簡單的前提,是別用 kts。
由於用 Kotlin 編寫 gradle 腳本實際上仍是比較冷門的,畢竟 groovy 的 DSL 至關簡潔了,徹底能夠知足平常開發工做的需求。不過用都用了,坑是自選的,只能建議各位現階段不要太勇,別給本身找麻煩。
過程主要是查文檔,看 kts 中的函數和 groovy 的區別,逐步解決構建中的錯誤,不贅述了,直接看結果:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("com.google.protobuf").version("0.8.12")
}
dependencies {
implementation("androidx.datastore:datastore:1.0.0-beta02")
implementation("com.google.protobuf:protobuf-javalite:3.10.0")
}
// protobuf 插件的配置函數跟 groovy 寫法上有必定的區別,千萬別直接複製
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
generateProtoTasks {
all().configureEach {
builtins {
id("java") { option("lite") }
}
}
}
}
複製代碼
.proto 文件放在 app/src/main/proto 文件夾中能夠省略配置 android sourceSets,make 成功後能夠在 generated java 文件夾下找到生成的代碼,大概是這樣
文檔參考:developer.android.com/topic/libra…
接入數據類是一個增長模板代碼的過程,分紅兩步,最終目的是提供一個 DataStore 對象的獲取方式。
Serializer<T>
,定義默認值DataStore<T>
對象供外部調用代碼以下:
object UserPreferenceSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (e: Exception) {
throw CorruptionException("Cannot read proto.", e)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
val Context.userPrefDataStore : DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb", // 保存文件名
serializer = UserPreferenceSerializer
)
複製代碼
Serializer 的代碼幾乎是固定的,建立 DataStore 的參數也只是多須要一個文件名,若是能經過帶參數的註解之類的方式自動生成對應代碼就行了,但願正式版能有優化。
fileName 對應的文件保存在 /data/data/com.github.moqigit.timecount/files/datastore 裏面,內容不是文本因此不能直接查看,安全性比 SharedPreference 高。protobuf 很是省空間,上面的7個鍵值對的測試數據只須要 40B 的空間,另外一個用 SharedPreference 存的2個鍵值對的文件就要 188B。
讀寫 API 就相對簡單了,會協程就沒什麼問題。
讀:
private fun printData() {
viewModelScope.launch {
val pref = this@MainActivity.userPrefDataStore.data.firstOrNull() ?: return@launch
Log.e("asdfg", "${pref.lastSessionTime}")
Log.e("asdfg", "${pref.toString()}")
}
}
複製代碼
打印數據只須要一次讀取,因此取 first 便可,若是須要監聽數據變化,則須要改用 collect。
寫:
private fun writeTestData() {
viewModelScope.launch {
pref.updateData {
it.toBuilder()
.setAntiAnxious(true)
.setCloudStorage(true)
.setCheckUpdate(true)
.setDarkColor("#333333")
.setLightColor("#262626")
.setLastSessionTime(System.currentTimeMillis())
.setPrimaryColor("#513f2d")
.build()
}
}
}
複製代碼
通過事先定義的數據結構自帶數據類型,讀寫過程就像 data class 同樣順滑,數據類型經由編譯器檢查也很難出錯,這應該是 protobuf 的大優點。
對應的頁面還沒弄好,只能在 Logcat 看效果了。
首次打開,只有一個默認值有數據
寫入以後,數據更新
默認值在 Serializer 的 defaultValue 中設置,若是自定義了完善的默認值數據,初次打開就不會一片空白了。
DataStore 的讀寫依賴 Context,使用時限制較多,解決方案也是老生常談了,能夠全局持有 applicationContext,用頂層函數或者單例提供 DataStore 對象。
但 Demo 項目已經接入 Hilt 了,DataStore 也很適合經過依賴注入建立對象,隨手加一下就行了:
@Module
@InstallIn(SingletonComponent::class)
object DSManager {
private val Context.userPrefDataStore : DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferenceSerializer
)
@Provides
fun provideUserPrefDS(@ApplicationContext context: Context) = context.userPrefDataStore
}
複製代碼
在 Activity 的使用:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var pref: DataStore<UserPreferences>
//……
}
複製代碼
在 ViewModel 的使用:
@HiltViewModel
class TimeCountViewModel @Inject constructor(
private val pref: DataStore<UserPreferences>,
) : ViewModel() {
// ……
}
複製代碼
DataStore 的上手體驗就是這樣了,初次使用比較複雜,優點須要複雜的業務場景才能體現,我的不是很推薦急着使用,畢竟兼容歷史版本是一個巨大的坑,。並且咱們也不是必定要在 SharedPreference 和 DataStore 二選一,不想用 Kotlin 協程還能夠試一下 MMKV。
Hilt 也比較穩定了,接下來準備寫 Compose 或者 Hilt,有什麼想法歡迎評論指出~