前言
最近兩年碴倾,MVVM的呼聲越來越高逗噩,說實話,在經(jīng)歷了MVP的臃腫影斑,MVP的繁瑣,我有點怕了机打。但是這次Google官方帶來的一系列為MVVM架構(gòu)設(shè)計的武器—Jetpack
矫户,真的讓我驚喜到了。
也許你還沒有使用這個新的武器残邀,那么我真的建議你去使用一下皆辽,感受下這個新武器的快準狠,感受下這個新架構(gòu)的精妙解耦芥挣。
介紹
2018年谷歌I/O驱闷,Jetpack
橫空出世,官方介紹如下:
Jetpack 是一套庫空免、工具和指南空另,可幫助開發(fā)者更輕松地編寫優(yōu)質(zhì)應用。這些組件可幫助您遵循最佳做法蹋砚、讓您擺脫編寫樣板代碼的工作并簡化復雜任務(wù)扼菠,以便您將精力集中放在所需的代碼上。
一直以來坝咐,Android開發(fā)
都充斥了大量的不規(guī)范的操作和重復代碼循榆,比如生命周期的管理,開發(fā)過程的重復墨坚,項目架構(gòu)的選擇等等秧饮。所以Google
為了規(guī)范開發(fā)行為,就推出這套指南泽篮,旨在讓開發(fā)者們能夠更好盗尸,更快,更規(guī)范
地開發(fā)出優(yōu)質(zhì)應用帽撑。
當然振劳,這兩年的實踐也確實證明了Jetpack
做到了它介紹的那樣,便捷油狂,快速历恐,優(yōu)質(zhì)寸癌。所以我們作為開發(fā)者還是應該早點應用到這些工具裙犹,提高自己的開發(fā)效率
忍坷,也規(guī)范我們自己的開發(fā)行為蜻懦。
今天給大家?guī)淼氖荍etpack中的架構(gòu)組件摹察,這個模塊的組件可以說就是為MVVM框架服務(wù)的最冰,每個庫也都是可以單獨使用的征候。
Jetpack-架構(gòu)組件
先簡單說下MVVM
县昂,Model—View—ViewModel闷叉。
-
Model層
主要指數(shù)據(jù)庇勃,比如服務(wù)器數(shù)據(jù)檬嘀,本地數(shù)據(jù)庫數(shù)據(jù),所以網(wǎng)絡(luò)操作和數(shù)據(jù)庫讀取就是這一層责嚷,只保存數(shù)據(jù)鸳兽。 -
View層
主要指UI相關(guān),比如xml布局文件罕拂,Activity界面顯示 -
ViewModel層
是MVVM的核心揍异,連接view和model,需要將model的數(shù)據(jù)展示到view上爆班,以及view上的操作數(shù)據(jù)反映轉(zhuǎn)化到model層衷掷,所以就相當于一個雙向綁定。
所以就需要柿菩,databinding進行數(shù)據(jù)的綁定戚嗅,單向或者雙向。viewmodel進行數(shù)據(jù)管理枢舶,綁定view和數(shù)據(jù)渡处。lifecycle進行生命周期管理。LiveData進行數(shù)據(jù)的及時反饋祟辟。 迫不及待了吧医瘫,跟隨我一起看看每個庫的神奇之處。
數(shù)據(jù)綁定
數(shù)據(jù)綁定庫是一種支持庫旧困,借助該庫醇份,您可以使用聲明性格式(而非程序化地)將布局中的界面組件綁定到應用中的數(shù)據(jù)源。
主要指的就是數(shù)據(jù)綁定庫DataBinding
吼具,下面從六個方面具體介紹下
配置應用使用數(shù)據(jù)綁定:
android {
...
dataBinding {
enabled = true
}
}
復制代碼
1)布局和綁定表達式
通過數(shù)據(jù)綁定僚纷,我們可以讓xml布局文件中的view與數(shù)據(jù)對象進行綁定和賦值,并且可以借助表達式語言編寫表達式來處理視圖分派的事件拗盒。舉個??:
//布局 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"/>
</layout>
//實體類User
data class User(val name: String)
//Activity賦值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Bob")
}
復制代碼
通過@{}
符號怖竭,可以在布局中使用數(shù)據(jù)對象,并且可以通過DataBindingUtil獲取賦值對象陡蝇。并且@{}
里面的表達式語言支持多種運算符痊臭,包括算術(shù)運算符哮肚,邏輯運算符等等。
2)可觀察的數(shù)據(jù)對象
可觀察性是指一個對象將其數(shù)據(jù)變化告知其他對象的能力广匙。通過數(shù)據(jù)綁定庫允趟,您可以讓對象、字段或集合變?yōu)榭捎^察鸦致。
比如上文剛說到的User類潮剪,我們將name屬性改成可觀察對象,
data class User(val name: ObservableField<String>)
val userName = ObservableField<String>()
userName.set("Bob")
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User(userName)
復制代碼
然后綁定到布局中分唾,這時候這個User的name
屬性就是被觀察對象了抗碰,如果userName
改變,布局里面的TextView
顯示數(shù)據(jù)也會跟著改變绽乔,這就是可觀察數(shù)據(jù)對象弧蝇。
3)生成的綁定類
剛才我們獲取綁定布局是通過DataBindingUtil.setContentView
方法生成ActivityMainBinding對象并綁定布局。那么ActivityMainBinding類是怎么生成的呢迄汛?只要你的布局用layout
屬性包圍捍壤,編譯后就會自動生成綁定類骤视,類名稱基于布局文件的名稱鞍爱,它會轉(zhuǎn)換為 Pascal
大小寫形式并在末尾添加 Binding 后綴。
正常創(chuàng)建綁定對象是通過如下寫法:
//Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
}
//Fragment
@Nullable
fun onCreateView( inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_layout, container, false)
return mDataBinding.getRoot()
}
復制代碼
4)綁定適配器
適配器這里指的是布局中的屬性設(shè)置专酗,android:text="@{user.name}"
表達式為例睹逃,庫會查找接受user.getName()
所返回類型的setText(arg)
方法。 重要的是祷肯,我們可以自定義這個適配器了沉填,也就是布局里面的屬性我們可以隨便定義它的名字和作用。來個??
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String) {
Picasso.get().load(url).into(view)
}
<ImageView app:imageUrl="@{venue.imageUrl}" />
復制代碼
在類中定義一個外部可以訪問的方法loadImage
佑笋,注釋@BindingAdapter
里面的屬性為你需要定義的屬性名稱翼闹,這里設(shè)置的是imageUrl。所以在布局中就可以使用app:imageUrl
蒋纬,并傳值為String類型猎荠,系統(tǒng)就會找到這個適配器方法并執(zhí)行。
5)將布局視圖綁定到架構(gòu)組件
這一塊就是實際應用了蜀备,和jetpack其他組件相結(jié)合使用关摇,形成完整的MVVM
分層架構(gòu)。
// Obtain the ViewModel component.
val userModel: UserViewModel by viewModels()
// Inflate view and obtain an instance of the binding class.
val binding: ActivityDatabindingMvvmBinding =
DataBindingUtil.setContentView(this, R.layout.activity_databinding_mvvm)
// Assign the component to a property in the binding class.
binding.viewmodel = userModel
<data>
<variable
name="viewmodel"
type="com.panda.jetpackdemo.dataBinding.UserViewModel" />
</data>
class UserViewModel : ViewModel() {
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
init {
currentName.value="zzz"
}
}
復制代碼
6)雙向數(shù)據(jù)綁定
剛才我們介紹的都是單向綁定碾阁,也就是布局中view綁定了數(shù)據(jù)對象输虱,那么如何讓數(shù)據(jù)對象也對view產(chǎn)生綁定呢?也就是view改變
的時候數(shù)據(jù)對象也能接收到訊息脂凶,形成雙向綁定
宪睹。
很簡單愁茁,比如一個EditText,需求是EditText改變的時候横堡,user對象name數(shù)據(jù)也會跟著改變埋市,只需要把之前的"@{}"改成"@={}"
//布局 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.name}"/>
</layout>
復制代碼
很簡單吧,同樣命贴,這個雙向綁定功能也是支持自定義的道宅。來個??
object SwipeRefreshLayoutBinding {
//方法1,數(shù)據(jù)綁定到view
@JvmStatic
@BindingAdapter("app:bind_refreshing")
fun setSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout,newValue: Boolean) {
if (swipeRefreshLayout.isRefreshing != newValue)
swipeRefreshLayout.isRefreshing = newValue
}
//方法1胸蛛,view改變會通知bind_refreshingChanged污茵,并且從該方法獲取view的數(shù)據(jù)
@JvmStatic
@InverseBindingAdapter(attribute = "app:bind_refreshing",event = "app:bind_refreshingChanged")
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =swipeRefreshLayout.isRefreshing
//方法3,view如何改變來影響數(shù)據(jù)內(nèi)容
@JvmStatic
@BindingAdapter("app:bind_refreshingChanged",requireAll = false)
fun setOnRefreshListener(swipeRefreshLayout: SwipeRefreshLayout,bindingListener: InverseBindingListener?) {
if (bindingListener != null)
swipeRefreshLayout.setOnRefreshListener {
bindingListener.onChange()
}
}
}
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_refreshing="@={viewModel.refreshing }">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
復制代碼
簡單說明下葬项,首先通過bind_refreshing
屬性泞当,將數(shù)據(jù)viewModel.refreshing
綁定到view上,這樣數(shù)據(jù)變化民珍,view也會跟著變化襟士。然后view變化的時候嚷量,通過InverseBindingAdapter
注釋,會調(diào)用bind_refreshingChanged
事件嗜历,而bind_refreshingChanged事件告訴了我們view什么時候會進行數(shù)據(jù)的修改,在這個案例中也就是swipeRefreshLayout下滑的時候會導致數(shù)據(jù)進行改變抖所,于是數(shù)據(jù)對象會從isSwipeRefreshLayoutRefreshing
方法獲取到最新的數(shù)值田轧,也就是從view更新過來的數(shù)據(jù)暴匠。
這里要注意的一個點是,雙向綁定要考慮到死循環(huán)問題傻粘,當View被改變抹腿,數(shù)據(jù)對象對應發(fā)生更新警绩,同時,這個更新又回通知View層去刷新UI后室,然后view被改變又會導致數(shù)據(jù)對象更新岸霹,無限循環(huán)下去了。所以防止死循環(huán)的做法就是判斷view的數(shù)據(jù)狀態(tài)痛黎,當發(fā)生改變的時候才去更新view湖饱。
Lifecycles
生命周期感知型組件可執(zhí)行操作來響應另一個組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化杀捻。這些組件有助于您寫出更有條理且往往更精簡的代碼致讥,這樣的代碼更易于維護垢袱。
Lifecycles
,稱為生命周期感知型組件撮弧,可以感知和響應另一個組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化。
可能有人會疑惑了救恨,生命周期就那幾個肠槽,我為啥還要導入一個庫呢秸仙?有了庫難道就不用寫生命周期了嗎寂纪,有什么好處呢? 舉個??孝冒,讓你感受下庄涡。
首先導入庫穴店,可以根據(jù)實際項目情況導入
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
//.......
復制代碼
現(xiàn)在有一個定位監(jiān)聽器迹鹅,需要在Activity
啟動的時候開啟斜棚,銷毀的時候關(guān)閉弟蚀。正常代碼如下:
class BindingActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
myLocationListener.start()
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
}
internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit
) {
fun start() {
// connect to system location service
}
fun stop() {
// disconnect from system location service
}
}
}
復制代碼
乍一看也沒什么問題是吧,但是如果需要管理生命周期的類一多捶闸,是不是就不好管理了删壮。所有的類都要在Activity里面管理央碟,還容易漏掉亿虽。 所以解決辦法就是實現(xiàn)解耦
洛勉,讓需要管理生命周期的類自己管理
如迟,這樣Activity也不會遺漏和臃腫了。上代碼:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
lifecycle.addObserver(myLocationListener)
}
internal class MyLocationListener (
private val context: Context,
private val callback: (Location) -> Unit
): LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
// disconnect if connected
}
}
復制代碼
很簡單吧,只要實現(xiàn)LifecycleObserver
接口巩趁,就可以用注釋的方式執(zhí)行每個生命周期要執(zhí)行的方法议慰。然后在Activity里面addObserver
綁定即可别凹。
同樣的洽糟,Lifecycle
也支持自定義生命周期坤溃,只要繼承LifecycleOwner即可薪介,然后通過markState
方法設(shè)定自己類的生命周期汁政,舉個??
class BindingActivity : AppCompatActivity(), LifecycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.markState(Lifecycle.State.CREATED)
}
public override fun onStart() {
super.onStart()
lifecycleRegistry.markState(Lifecycle.State.STARTED)
}
}
復制代碼
LiveData
LiveData 是一種可觀察的數(shù)據(jù)存儲器類勺鸦。與常規(guī)的可觀察類不同抠蚣,LiveData 具有生命周期感知能力嘶窄,意指它遵循其他應用組件(如 Activity柄冲、Fragment 或 Service)的生命周期现横。這種感知能力可確保 LiveData 僅更新處于活躍生命周期狀態(tài)的應用組件觀察者戒祠。
LiveData
是一種可觀察的數(shù)據(jù)存儲器類姜盈。 等等馏颂,這個介紹好像似曾相識救拉?對亿絮,前面說數(shù)據(jù)綁定的時候就有一個可觀察的數(shù)據(jù)對象ObservableField
派昧。那兩者有什么區(qū)別呢斗锭?
1)LiveData
具有生命周期感知能力岖是,可以感知到Activity等的生命周期豺撑。這樣有什么好處呢聪轿?很常見的一點就是可以減少內(nèi)存泄漏和崩潰情況了呀陆错,想想以前你的項目中針對網(wǎng)絡(luò)接口返回數(shù)據(jù)的時候都要判斷當前界面是否銷毀音瓷,現(xiàn)在LiveData就幫你解決了這個問題绳慎。
具體為什么能解決崩潰和泄漏問題呢?
-
不會發(fā)生內(nèi)存泄漏
觀察者會綁定到 Lifecycle 對象已脓,并在其關(guān)聯(lián)的生命周期遭到銷毀后進行自我清理摆舟。 -
不會因 Activity 停止而導致崩潰
如果觀察者的生命周期處于非活躍狀態(tài)(如返回棧中的 Activity)恨诱,則它不會接收任何 LiveData 事件照宝。 -
自動判斷生命周期并回調(diào)方法
如果觀察者的生命周期處于 STARTED 或 RESUMED狀態(tài)厕鹃,則 LiveData 會認為該觀察者處于活躍狀態(tài)剂碴,就會調(diào)用onActive方法忆矛,否則催训,如果 LiveData 對象沒有任何活躍觀察者時漫拭,會調(diào)用 onInactive()方法采驻。
2) LiveData更新數(shù)據(jù)更靈活,不一定是改變數(shù)據(jù)各淀,而是調(diào)用方法(postValue或者setValue)
的方式進行UI更新或者其他操作碎浇。
好了奴璃。還是舉個??更直觀的看看吧:
//導入庫:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
public class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val myPriceListener: LiveData<BigDecimal> = StockLiveData("")
myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// 監(jiān)聽livedata的數(shù)據(jù)變化苟穆,如果調(diào)用了setValue或者postValue會調(diào)用該onChanged方法
//更新UI數(shù)據(jù)或者其他處理
})
}
}
復制代碼
這是一個股票數(shù)據(jù)對象,StockManager
為股票管理器攒盈,如果該對象有活躍觀察者時型豁,就去監(jiān)聽股票市場的情況迎变,如果沒有活躍觀察者時衣形,就可以斷開監(jiān)聽泵喘。 當監(jiān)聽到股票信息變化纪铺,該股票數(shù)據(jù)對象就會通過setValue
方法進行數(shù)據(jù)更新鲜锚,反應到觀察者的onChanged方法芜繁。這里要注意的是setValue
方法只能在主線程調(diào)用,而postValue
則是在其他線程調(diào)用榔袋。 當Fragment
這個觀察者生命周期發(fā)生變化時凰兑,LiveData
就會移除這個觀察者吏够,不再發(fā)送消息锅知,所以也就避免崩潰問題售睹。
Navigation
導航 Navigation 組件旨在用于具有一個主 Activity 和多個 Fragment 目的地的應用生真。主 Activity 與導航圖相關(guān)聯(lián)柱蟀,且包含一個負責根據(jù)需要交換目的地的 NavHostFragment长已。在具有多個 Activity 目的地的應用中术瓮,每個 Activity 均擁有其自己的導航圖胞四。
所以說白了辜伟,Navigation
就是一個Fragment
的管理框架。 怎么實現(xiàn)旱捧?創(chuàng)建Activity枚赡,F(xiàn)ragment标锄,進行連接。
1)導入庫
def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
復制代碼
2)創(chuàng)建3個Fragment和一個Activity
3)創(chuàng)建res/navigation/my_nav.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/myFragment1"
tools:ignore="UnusedNavigation">
<fragment
android:id="@+id/myFragment1"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/myFragment2" />
</fragment>
<fragment
android:id="@+id/myFragment2"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/myFragment3" />
</fragment>
<fragment
android:id="@+id/myFragment3"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
</fragment>
</navigation>
復制代碼
在res文件夾下新建navigation
目錄,并新建my_nav.xml
文件逊脯。配置好每個Fragment军洼,其中:
-
app:startDestination
屬性代表一開始顯示的fragment -
android:name
屬性代表對應的Fragment路徑 -
action
代表該Fragment存在的跳轉(zhuǎn)事件,比如myFragment1可以跳轉(zhuǎn)myFragment2甘桑。
- 修改Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/my_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
復制代碼
可以看到,Activity的布局文件就是一個fragment控件德谅,name為NavHostFragment窄做,navGraph
為剛才新建的mynavigation文件。
5)配置完了之后庸汗,就可以設(shè)置具體的跳轉(zhuǎn)邏輯了蚯舱。
override fun onClick(v: View) {
//不帶參數(shù)
v.findNavController().navigate(R.id.action_blankFragment_to_blankFragment2)
//帶參數(shù)
var bundle = bundleOf("amount" to amount)
v.findNavController().navigate(R.id.confirmationAction, bundle)
}
//接收數(shù)據(jù)
tv.text = arguments?.getString("amount")
復制代碼
需要注意的是枉昏,跳轉(zhuǎn)這塊官方建議用Safe Args
的Gradle 插件句旱,該插件可以生成簡單的 object 和 builder
類谈撒,以便以類型安全的方式瀏覽和訪問任何關(guān)聯(lián)的參數(shù)啃匿。這里就不細說了,感興趣的可以去官網(wǎng)看看
Room
Room 持久性庫在 SQLite 的基礎(chǔ)上提供了一個抽象層裆悄,讓用戶能夠在充分利用 SQLite 的強大功能的同時灯帮,獲享更強健的數(shù)據(jù)庫訪問機制。
所以Room
就是一個數(shù)據(jù)庫框架逻住。問題來了钟哥,市面上那么多數(shù)據(jù)庫組件,比如ormLite瞎访,greendao
等等腻贰,為什么google還要出一個room,有什么優(yōu)勢呢扒秸?
-
性能優(yōu)勢,一次數(shù)據(jù)庫操作主要包括:構(gòu)造sql語句—編譯語句—傳入?yún)?shù)—執(zhí)行操作伴奥。
ORMLite
主要在獲取參數(shù)屬性值的時候写烤,是通過反射獲取的,所以速度較慢拾徙。GreenDao
在構(gòu)造sql語句的時候是通過代碼拼接洲炊,所以較慢。Room
是通過接口方法的注解生成sql語句,也就是編譯成字節(jié)碼的時候就生成了sql語句暂衡,所以運行起來較快询微。 -
支持jetpack其他組件(比如LiveData,Paging)以及RxJava狂巢,這就好比借助了當前所在的優(yōu)勢環(huán)境撑毛,就能給你帶來一些得天獨厚的優(yōu)勢。當然實際使用起來也確實要方便很多唧领,比如
liveData
結(jié)合藻雌,就能在數(shù)據(jù)查詢后進行自動UI更新。
既然Room這么優(yōu)秀疹吃,那就用起來吧蹦疑。 Room的接入主要有三大點:DataBase西雀、Entity萨驶、Dao
。分別對應數(shù)據(jù)庫艇肴,表和數(shù)據(jù)訪問腔呜。
1)首先導入庫:
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
}
復制代碼
2)建立數(shù)據(jù)庫類,聲明數(shù)據(jù)庫表成員再悼,數(shù)據(jù)庫名稱核畴,數(shù)據(jù)庫版本,單例等等
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDb : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDb? = null
@Synchronized
fun get(context: Context): UserDb {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
UserDb::class.java, "StudentDatabase").build()
}
return instance!!
}
}
}
復制代碼
3)建表冲九,可以設(shè)置主鍵谤草,外鍵,索引莺奸,自增等等
@Entity
data class User(@PrimaryKey(autoGenerate = true) val id: Int,
val name: String)
復制代碼
4)Dao丑孩,數(shù)據(jù)操作
@Dao
interface UserDao {
@Query("SELECT * FROM User")
fun getAllUser(): DataSource.Factory<Int, User>
@Query("SELECT * FROM User")
fun getAllUser2(): LiveData<List<User>>
@Query("SELECT * from user")
fun getAllUser3(): Flowable<List<User>>
@Insert
fun insert(users: List<User>)
}
復制代碼
然后就可以進行數(shù)據(jù)庫操作了,很簡單吧灭贷。
官方文檔
Demo代碼地址
Paging
分頁庫可幫助您一次加載和顯示一小塊數(shù)據(jù)温学。按需載入部分數(shù)據(jù)會減少網(wǎng)絡(luò)帶寬和系統(tǒng)資源的使用量。
所以Paging
就是一個分頁庫甚疟,主要用于Recycleview列表展示仗岖。下面我就結(jié)合Room說說Paging的用法。 使用Paging主要注意兩個類:PagedList和PagedListAdapter
览妖。
1)PagedList
用于加載應用數(shù)據(jù)塊轧拄,綁定數(shù)據(jù)列表,設(shè)置數(shù)據(jù)頁等讽膏。結(jié)合上述Room
的Demo我繼續(xù)寫了一個UserModel
進行數(shù)據(jù)管理:
class UserModel(app: Application) : AndroidViewModel(app) {
val dao = UserDb.get(app).userDao()
var idNum = 1
companion object {
private const val PAGE_SIZE = 10
}
//初始化PagedList
val users = LivePagedListBuilder(
dao.getAllUser(), PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setEnablePlaceholders(true)
.build()
).build()
//插入用戶
fun insert() = ioThread {
dao.insert(newTenUser())
}
//獲取新的10個用戶
fun newTenUser(): ArrayList<User> {
var newUsers = ArrayList<User>()
for (index in 1..10) {
newUsers.add(User(0, "bob${++idNum}"))
}
return newUsers
}
}
復制代碼
2)PagedListAdapter
使用Recycleview必要要用到adatper檩电,所以這里需要綁定一個繼承自PagedListAdapter
的adapter:
class UserAdapter : PagedListAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bindTo(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder =
UserViewHolder(parent)
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
oldItem == newItem
}
}
class UserViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)) {
private val tv1 = itemView.findViewById<TextView>(R.id.name)
var user: User? = null
fun bindTo(user: User?) {
this.user = user
tv1.text = user?.name
}
}
}
復制代碼
這里還用到了DiffUtil.ItemCallback
類,用于比較數(shù)據(jù),進行數(shù)據(jù)更新用是嗜。
ok愈案,數(shù)據(jù)源,adapter都設(shè)置好了鹅搪,接下來就是監(jiān)聽數(shù)據(jù)站绪,刷新數(shù)據(jù)就可以了
// 監(jiān)聽users數(shù)據(jù),數(shù)據(jù)改變調(diào)用submitList方法
viewModel.users.observe(this, Observer(adapter::submitList))
復制代碼
對丽柿,就是這么一句恢准,監(jiān)聽PagedList
,并且在它改變的時候調(diào)用PagedListAdapter的submitList
方法甫题。 這分層夠爽吧馁筐,其實這也就是paging或者說jetpack給我們項目帶來的優(yōu)勢,層層解耦坠非,adapter都不用維護list數(shù)據(jù)源了敏沉。
ViewModel
ViewModel 類旨在以注重生命周期的方式存儲和管理界面相關(guān)的數(shù)據(jù)。ViewModel 類讓數(shù)據(jù)可在發(fā)生屏幕旋轉(zhuǎn)等配置更改后繼續(xù)留存炎码。
終于說到ViewModel
了盟迟,其實之前的demo都用了好多遍了,ViewModel
主要是從界面控制器邏輯中分離出視圖數(shù)據(jù)潦闲,為什么要這么做呢攒菠?主要為了解決兩大問題:
- 以前Activity中如果被系統(tǒng)銷毀或者需要重新創(chuàng)建的時候,頁面臨時性數(shù)據(jù)都會丟失歉闰,需要通過
onSaveInstanceState()
方法保存辖众,onCreate方法中讀取。而且數(shù)據(jù)量一大就更加不方便了和敬。 - 在Activity中凹炸,難免有些異步調(diào)用,所以就會容易導致界面銷毀時候概龄,這些調(diào)用還存在还惠。那就會發(fā)生內(nèi)存泄漏或者直接崩潰。
所以ViewModel
誕生了私杜,還是解耦蚕键,我把數(shù)據(jù)單獨拿出來管理,還加上生命周期衰粹,那不就可以解決這些問題了嗎锣光。而且當所有者 Activity 完全銷毀之后,ViewModel
會調(diào)用其onCleared()
方法铝耻,以便清理資源誊爹。
接下來舉個??蹬刷,看看ViewModel具體是怎么使用的:
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
class SharedViewModel : ViewModel() {
var userData = MutableLiveData<User>()
fun select(item: User) {
userData.value = item
}
override fun onCleared() {
super.onCleared()
}
}
class MyFragment1 : Fragment() {
private lateinit var btn: Button
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
btn.setOnClickListener{
model?.select(User(0,"bob"))
}
}
}
class MyFragment2 : Fragment() {
private lateinit var btn: Button
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
model?.userData?.observe(viewLifecycleOwner, Observer<User> { item ->
// Update the UI
})
}
}
復制代碼
Fragment中,獲取到viewmodel
的實例频丘,然后進行數(shù)據(jù)監(jiān)聽等操作办成。等等,你能發(fā)現(xiàn)什么不搂漠? 對了迂卢,數(shù)據(jù)通信。不同的 Fragment 可以使用其父Activity共享ViewModel
來進行數(shù)據(jù)的通信桐汤,厲害吧而克。還有很多其他的用法,去項目中慢慢發(fā)現(xiàn)吧怔毛!
WorkManager
使用 WorkManager API 可以輕松地調(diào)度即使在應用退出或設(shè)備重啟時仍應運行的可延遲異步任務(wù)员萍。
聽聽這個介紹就很神奇了,應用退出和設(shè)備重啟都能自動運行拣度?通過廣播碎绎?那數(shù)據(jù)又是怎么保存的呢?聽說還可以執(zhí)行周期性異步任務(wù)蜡娶,順序鏈式調(diào)用哦混卵!接下來一一解密
- 關(guān)于應用退出和設(shè)備重啟
如果APP正在運行,WorkManager
會在APP進程中起一個新線程來運行任務(wù)窖张;如果APP沒有運行,WorkManager
會選擇一個合適的方式來調(diào)度后臺任務(wù)--根據(jù)系統(tǒng)級別和APP狀態(tài)蚁滋,WorkManager可能會使用JobScheduler宿接,F(xiàn)ireBase JobDispatcher
或者AlarmManager
。 - 關(guān)于數(shù)據(jù)保存
WorkManager
創(chuàng)建的任務(wù)數(shù)據(jù)都會保存到數(shù)據(jù)庫辕录,用的是Room
框架睦霎。然后重啟等時間段都會去數(shù)據(jù)庫尋找需要安排執(zhí)行的任務(wù),然后判斷約束條件
走诞,滿足即可執(zhí)行副女。
一般這個API應用到什么場景呢?想想蚣旱,可靠運行碑幅,還可以周期異步。 對了塞绿,發(fā)送日志沟涨。可以通過WorkManager
設(shè)定周期任務(wù)异吻,每天執(zhí)行一次發(fā)送日志的任務(wù)裹赴。而且能夠保證你的任務(wù)可靠運行,一定可以上傳到,當然也是支持監(jiān)聽任務(wù)結(jié)果等棋返。??:
1)導入庫
dependencies {
def work_version = "2.3.4"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
}
復制代碼
2) 新建任務(wù)類延都,繼承Worker
,重寫doWork
方法睛竣,返回任務(wù)結(jié)果窄潭。
class UploadLogcatWork(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
if (isUploadLogcatSuc()) {
return Result.success()
} else if (isNeedRetry()){
return Result.retry()
}
return Result.failure()
}
fun isUploadLogcatSuc(): Boolean {
var isSuc: Boolean = false
return isSuc
}
fun isNeedRetry(): Boolean {
var isSuc: Boolean = false
return isSuc
}
}
復制代碼
3)最后就是設(shè)定約束(是否需要網(wǎng)絡(luò),是否支持低電量酵颁,是否支持充電執(zhí)行嫉你,延遲等等),執(zhí)行任務(wù)(單次任務(wù)或者循環(huán)周期任務(wù))
//設(shè)定約束
val constraints =
Constraints.Builder()
//網(wǎng)絡(luò)鏈接的時候使用
.setRequiredNetworkType(NetworkType.CONNECTED)
//是否在設(shè)備空閑的時候執(zhí)行
.setRequiresDeviceIdle(false)
//是否在低電量的時候執(zhí)行
.setRequiresBatteryNotLow(true)
//是否在內(nèi)存不足的時候執(zhí)行
.setRequiresStorageNotLow(true)
//是否時充電的時候執(zhí)行
.setRequiresCharging(true)
//延遲執(zhí)行
.setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
.build()
//設(shè)定循環(huán)任務(wù)
val uploadRequest =
PeriodicWorkRequestBuilder<UploadLogcatWork>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.addTag("uploadTag")
.build()
//執(zhí)行
WorkManager.getInstance(applicationContext).enqueue(uploadRequest)
//監(jiān)聽執(zhí)行結(jié)果
WorkManager.getInstance(this)
// .getWorkInfosByTagLiveData("uploadTag") //通過tag拿到work
.getWorkInfoByIdLiveData(uploadRequest.id) //通過id拿到work
.observe(this, Observer {
it?.apply {
when (this.state) {
WorkInfo.State.BLOCKED -> println("BLOCKED")
WorkInfo.State.CANCELLED -> println("CANCELLED")
WorkInfo.State.RUNNING -> println("RUNNING")
WorkInfo.State.ENQUEUED -> println("ENQUEUED")
WorkInfo.State.FAILED -> println("FAILED")
WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
else -> println("else status ${this.state}")
}
}
})
復制代碼
4)另外還支持任務(wù)取消躏惋,任務(wù)鏈式順序調(diào)用等
//取消
fun cancelWork(){
WorkManager.getInstance(applicationContext).cancelAllWorkByTag("uploadTag")
}
fun startLineWork(){
//圖片濾鏡1
val filter1 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片濾鏡2
val filter2 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片壓縮
val compress = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片上傳
val upload = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
WorkManager.getInstance(applicationContext)
.beginWith(listOf(filter1, filter2))
.then(compress)
.then(upload)
.enqueue()
}
復制代碼
總結(jié)
Jetpack-架構(gòu)組件講完了幽污,大家吃??應該吃飽了吧哈哈。希望這篇文章能讓不怎么熟悉Jetpack
的同學熟悉熟悉簿姨。
當然距误,這還遠遠不夠,在我看來扁位,本文更像是一個科普文
准潭,只是告訴了大家Jetpack-架構(gòu)組件有哪些成員,有什么用處域仇。實際項目中刑然,我們還需要建立MVVM
的思想,深刻了解每個組件的設(shè)計意義暇务,靈活運用組件泼掠。(附件有個項目是官方的Jetpack實踐項目
,可以看看)
最后希望大家都能通過jetpack
構(gòu)建高質(zhì)量垦细,簡易并優(yōu)質(zhì)的項目架構(gòu)择镇,從而解放生產(chǎn)力,成為效率達人
括改。
有用的話腻豌,點個贊吧?( ′???` )比心