前言
最近兩年,MVVM的呼聲越來(lái)越高,說(shuō)實(shí)話,在經(jīng)歷了MVP的臃腫,MVP的繁瑣,我有點(diǎn)怕了。但是這次google官方帶來(lái)的一系列為MVVM架構(gòu)設(shè)計(jì)的武器—Jetpack,真的讓我驚喜到了。
也許你還沒(méi)有使用這個(gè)新的武器,那么我真的建議你去使用一下,感受下這個(gè)新武器的快準(zhǔn)狠,感受下這個(gè)新架構(gòu)的精妙解耦。
介紹
2018年谷歌I/O,Jetpack橫空出世,官方介紹如下:
Jetpack 是一套庫(kù)、工具和指南,可幫助開(kāi)發(fā)者更輕松地編寫優(yōu)質(zhì)應(yīng)用。這些組件可幫助您遵循最佳做法、讓您擺脫編寫樣板代碼的工作并簡(jiǎn)化復(fù)雜任務(wù),以便您將精力集中放在所需的代碼上。
一直以來(lái),Android開(kāi)發(fā)都充斥了大量的不規(guī)范的操作和重復(fù)代碼,比如生命周期的管理,開(kāi)發(fā)過(guò)程的重復(fù),項(xiàng)目架構(gòu)的選擇等等。所以Google為了規(guī)范開(kāi)發(fā)行為,就推出這套指南,旨在讓開(kāi)發(fā)者們能夠更好,更快,更規(guī)范地開(kāi)發(fā)出優(yōu)質(zhì)應(yīng)用。
當(dāng)然,這兩年的實(shí)踐也確實(shí)證明了Jetpack做到了它介紹的那樣,便捷,快速,優(yōu)質(zhì)。所以我們作為開(kāi)發(fā)者還是應(yīng)該早點(diǎn)應(yīng)用到這些工具,提高自己的開(kāi)發(fā)效率,也規(guī)范我們自己的開(kāi)發(fā)行為。
今天給大家?guī)?lái)的是Jetpack中的架構(gòu)組件,這個(gè)模塊的組件可以說(shuō)就是為MVVM框架服務(wù)的,每個(gè)庫(kù)也都是可以單獨(dú)使用的。
Jetpack-架構(gòu)組件
先簡(jiǎn)單說(shuō)下MVVM,Model—View—ViewModel。
- Model層主要指數(shù)據(jù),比如服務(wù)器數(shù)據(jù),本地?cái)?shù)據(jù)庫(kù)數(shù)據(jù),所以網(wǎng)絡(luò)操作和數(shù)據(jù)庫(kù)讀取就是這一層,只保存數(shù)據(jù)。
- View層主要指UI相關(guān),比如xml布局文件,Activity界面顯示
- ViewModel層是MVVM的核心,連接view和model,需要將model的數(shù)據(jù)展示到view上,以及view上的操作數(shù)據(jù)反映轉(zhuǎn)化到model層,所以就相當(dāng)于一個(gè)雙向綁定。
所以就需要,databinding進(jìn)行數(shù)據(jù)的綁定,單向或者雙向。viewmodel進(jìn)行數(shù)據(jù)管理,綁定view和數(shù)據(jù)。lifecycle進(jìn)行生命周期管理。LiveData進(jìn)行數(shù)據(jù)的及時(shí)反饋。 迫不及待了吧,跟隨我一起看看每個(gè)庫(kù)的神奇之處。
數(shù)據(jù)綁定
數(shù)據(jù)綁定庫(kù)是一種支持庫(kù),借助該庫(kù),您可以使用聲明性格式(而非程序化地)將布局中的界面組件綁定到應(yīng)用中的數(shù)據(jù)源。
主要指的就是數(shù)據(jù)綁定庫(kù)DataBinding,下面從六個(gè)方面具體介紹下
配置應(yīng)用使用數(shù)據(jù)綁定:
android {
...
dataBinding {
enabled = true
}
}
1)布局和綁定表達(dá)式通過(guò)數(shù)據(jù)綁定,我們可以讓xml布局文件中的view與數(shù)據(jù)對(duì)象進(jìn)行綁定和賦值,并且可以借助表達(dá)式語(yǔ)言編寫表達(dá)式來(lái)處理視圖分派的事件。舉個(gè):
//布局 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>
//實(shí)體類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")
}
通過(guò)@{}符號(hào),可以在布局中使用數(shù)據(jù)對(duì)象,并且可以通過(guò)DataBindingUtil獲取賦值對(duì)象。并且@{}里面的表達(dá)式語(yǔ)言支持多種運(yùn)算符,包括算術(shù)運(yùn)算符,邏輯運(yùn)算符等等。
2)可觀察的數(shù)據(jù)對(duì)象可觀察性是指一個(gè)對(duì)象將其數(shù)據(jù)變化告知其他對(duì)象的能力。通過(guò)數(shù)據(jù)綁定庫(kù),您可以讓對(duì)象、字段或集合變?yōu)榭捎^察。
比如上文剛說(shuō)到的User類,我們將name屬性改成可觀察對(duì)象,
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)
然后綁定到布局中,這時(shí)候這個(gè)User的name屬性就是被觀察對(duì)象了,如果userName改變,布局里面的TextView顯示數(shù)據(jù)也會(huì)跟著改變,這就是可觀察數(shù)據(jù)對(duì)象。
3)生成的綁定類
剛才我們獲取綁定布局是通過(guò)DataBindingUtil.setContentView方法生成ActivityMainBinding對(duì)象并綁定布局。那么ActivityMainBinding類是怎么生成的呢?只要你的布局用layout屬性包圍,編譯后就會(huì)自動(dòng)生成綁定類,類名稱基于布局文件的名稱,它會(huì)轉(zhuǎn)換為 Pascal 大小寫形式并在末尾添加 Binding 后綴。
正常創(chuàng)建綁定對(duì)象是通過(guò)如下寫法:
//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}"表達(dá)式為例,庫(kù)會(huì)查找接受user.getName()所返回類型的setText(arg) 方法。 重要的是,我們可以自定義這個(gè)適配器了,也就是布局里面的屬性我們可以隨便定義它的名字和作用。來(lái)個(gè)
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String) {
Picasso.get().load(url).into(view)
}
<ImageView App:imageUrl="@{venue.imageUrl}" />
在類中定義一個(gè)外部可以訪問(wèn)的方法loadImage,注釋@BindingAdapter里面的屬性為你需要定義的屬性名稱,這里設(shè)置的是imageUrl。所以在布局中就可以使用app:imageUrl,并傳值為String類型,系統(tǒng)就會(huì)找到這個(gè)適配器方法并執(zhí)行。
5)將布局視圖綁定到架構(gòu)組件這一塊就是實(shí)際應(yīng)用了,和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ù)對(duì)象,那么如何讓數(shù)據(jù)對(duì)象也對(duì)view產(chǎn)生綁定呢?也就是view改變的時(shí)候數(shù)據(jù)對(duì)象也能接收到訊息,形成雙向綁定。
很簡(jiǎn)單,比如一個(gè)EditText,需求是EditText改變的時(shí)候,user對(duì)象name數(shù)據(jù)也會(huì)跟著改變,只需要把之前的"@{}"改成"@={}"
//布局 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>
很簡(jiǎn)單吧,同樣,這個(gè)雙向綁定功能也是支持自定義的。來(lái)個(gè)
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改變會(huì)通知bind_refreshingChanged,并且從該方法獲取view的數(shù)據(jù)
@JvmStatic
@InverseBindingAdapter(attribute = "app:bind_refreshing",event = "app:bind_refreshingChanged")
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =swipeRefreshLayout.isRefreshing
//方法3,view如何改變來(lái)影響數(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>
簡(jiǎn)單說(shuō)明下,首先通過(guò)bind_refreshing屬性,將數(shù)據(jù)viewModel.refreshing綁定到view上,這樣數(shù)據(jù)變化,view也會(huì)跟著變化。然后view變化的時(shí)候,通過(guò)InverseBindingAdapter注釋,會(huì)調(diào)用bind_refreshingChanged事件,而bind_refreshingChanged事件告訴了我們view什么時(shí)候會(huì)進(jìn)行數(shù)據(jù)的修改,在這個(gè)案例中也就是swipeRefreshLayout下滑的時(shí)候會(huì)導(dǎo)致數(shù)據(jù)進(jìn)行改變,于是數(shù)據(jù)對(duì)象會(huì)從isSwipeRefreshLayoutRefreshing方法獲取到最新的數(shù)值,也就是從view更新過(guò)來(lái)的數(shù)據(jù)。
這里要注意的一個(gè)點(diǎn)是,雙向綁定要考慮到死循環(huán)問(wèn)題,當(dāng)View被改變,數(shù)據(jù)對(duì)象對(duì)應(yīng)發(fā)生更新,同時(shí),這個(gè)更新又回通知View層去刷新UI,然后view被改變又會(huì)導(dǎo)致數(shù)據(jù)對(duì)象更新,無(wú)限循環(huán)下去了。所以防止死循環(huán)的做法就是判斷view的數(shù)據(jù)狀態(tài),當(dāng)發(fā)生改變的時(shí)候才去更新view。
Lifecycles
生命周期感知型組件可執(zhí)行操作來(lái)響應(yīng)另一個(gè)組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化。這些組件有助于您寫出更有條理且往往更精簡(jiǎn)的代碼,這樣的代碼更易于維護(hù)。
Lifecycles,稱為生命周期感知型組件,可以感知和響應(yīng)另一個(gè)組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化。
可能有人會(huì)疑惑了,生命周期就那幾個(gè),我為啥還要導(dǎo)入一個(gè)庫(kù)呢?有了庫(kù)難道就不用寫生命周期了嗎,有什么好處呢? 舉個(gè),讓你感受下。
首先導(dǎo)入庫(kù),可以根據(jù)實(shí)際項(xiàng)目情況導(dǎo)入
// 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)在有一個(gè)定位監(jiān)聽(tīng)器,需要在Activity啟動(dòng)的時(shí)候開(kāi)啟,銷毀的時(shí)候關(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
}
}
}
乍一看也沒(méi)什么問(wèn)題是吧,但是如果需要管理生命周期的類一多,是不是就不好管理了。所有的類都要在Activity里面管理,還容易漏掉。 所以解決辦法就是實(shí)現(xiàn)解耦,讓需要管理生命周期的類自己管理,這樣Activity也不會(huì)遺漏和臃腫了。上代碼:
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
}
}
很簡(jiǎn)單吧,只要實(shí)現(xiàn)LifecycleObserver接口,就可以用注釋的方式執(zhí)行每個(gè)生命周期要執(zhí)行的方法。然后在Activity里面addObserver綁定即可。
同樣的,Lifecycle也支持自定義生命周期,只要繼承LifecycleOwner即可,然后通過(guò)markState方法設(shè)定自己類的生命周期,舉個(gè)
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ù)存儲(chǔ)器類。與常規(guī)的可觀察類不同,LiveData 具有生命周期感知能力,意指它遵循其他應(yīng)用組件(如 Activity、Fragment 或 Service)的生命周期。這種感知能力可確保 LiveData 僅更新處于活躍生命周期狀態(tài)的應(yīng)用組件觀察者。
LiveData 是一種可觀察的數(shù)據(jù)存儲(chǔ)器類。 等等,這個(gè)介紹好像似曾相識(shí)?對(duì),前面說(shuō)數(shù)據(jù)綁定的時(shí)候就有一個(gè)可觀察的數(shù)據(jù)對(duì)象ObservableField。那兩者有什么區(qū)別呢?
1)LiveData 具有生命周期感知能力,可以感知到Activity等的生命周期。這樣有什么好處呢?很常見(jiàn)的一點(diǎn)就是可以減少內(nèi)存泄漏和崩潰情況了呀,想想以前你的項(xiàng)目中針對(duì)網(wǎng)絡(luò)接口返回?cái)?shù)據(jù)的時(shí)候都要判斷當(dāng)前界面是否銷毀,現(xiàn)在LiveData就幫你解決了這個(gè)問(wèn)題。
具體為什么能解決崩潰和泄漏問(wèn)題呢?
- 不會(huì)發(fā)生內(nèi)存泄漏 觀察者會(huì)綁定到 Lifecycle 對(duì)象,并在其關(guān)聯(lián)的生命周期遭到銷毀后進(jìn)行自我清理。
- 不會(huì)因 Activity 停止而導(dǎo)致崩潰 如果觀察者的生命周期處于非活躍狀態(tài)(如返回棧中的 Activity),則它不會(huì)接收任何 LiveData 事件。
- 自動(dòng)判斷生命周期并回調(diào)方法 如果觀察者的生命周期處于 STARTED 或 RESUMED狀態(tài),則 LiveData 會(huì)認(rèn)為該觀察者處于活躍狀態(tài),就會(huì)調(diào)用onActive方法,否則,如果 LiveData 對(duì)象沒(méi)有任何活躍觀察者時(shí),會(huì)調(diào)用 onInactive()方法。
2) LiveData更新數(shù)據(jù)更靈活,不一定是改變數(shù)據(jù),而是調(diào)用方法(postValue或者setValue)的方式進(jìn)行UI更新或者其他操作。
好了。還是舉個(gè)更直觀的看看吧:
//導(dǎo)入庫(kù):
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)聽(tīng)livedata的數(shù)據(jù)變化,如果調(diào)用了setValue或者postValue會(huì)調(diào)用該onChanged方法
//更新UI數(shù)據(jù)或者其他處理
})
}
}
這是一個(gè)股票數(shù)據(jù)對(duì)象,StockManager為股票管理器,如果該對(duì)象有活躍觀察者時(shí),就去監(jiān)聽(tīng)股票市場(chǎng)的情況,如果沒(méi)有活躍觀察者時(shí),就可以斷開(kāi)監(jiān)聽(tīng)。 當(dāng)監(jiān)聽(tīng)到股票信息變化,該股票數(shù)據(jù)對(duì)象就會(huì)通過(guò)setValue方法進(jìn)行數(shù)據(jù)更新,反應(yīng)到觀察者的onChanged方法。這里要注意的是setValue方法只能在主線程調(diào)用,而postValue則是在其他線程調(diào)用。 當(dāng)Fragment這個(gè)觀察者生命周期發(fā)生變化時(shí),LiveData就會(huì)移除這個(gè)觀察者,不再發(fā)送消息,所以也就避免崩潰問(wèn)題。
Navigation
導(dǎo)航 Navigation 組件旨在用于具有一個(gè)主 Activity 和多個(gè) Fragment 目的地的應(yīng)用。主 Activity 與導(dǎo)航圖相關(guān)聯(lián),且包含一個(gè)負(fù)責(zé)根據(jù)需要交換目的地的 NavHostFragment。在具有多個(gè) Activity 目的地的應(yīng)用中,每個(gè) Activity 均擁有其自己的導(dǎo)航圖。
所以說(shuō)白了,Navigation就是一個(gè)Fragment的管理框架。 怎么實(shí)現(xiàn)?創(chuàng)建Activity,F(xiàn)ragment,進(jìn)行連接。
1)導(dǎo)入庫(kù)
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個(gè)Fragment和一個(gè)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 文件。配置好每個(gè)Fragment,其中:
- app:startDestination 屬性代表一開(kāi)始顯示的fragment
- android:name 屬性代表對(duì)應(yīng)的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的布局文件就是一個(gè)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 插件,該插件可以生成簡(jiǎn)單的 object 和 builder類,以便以類型安全的方式瀏覽和訪問(wèn)任何關(guān)聯(lián)的參數(shù)。這里就不細(xì)說(shuō)了,感興趣的可以去官網(wǎng)看看
Room
Room 持久性庫(kù)在 SQLite 的基礎(chǔ)上提供了一個(gè)抽象層,讓用戶能夠在充分利用 SQLite 的強(qiáng)大功能的同時(shí),獲享更強(qiáng)健的數(shù)據(jù)庫(kù)訪問(wèn)機(jī)制。
所以Room就是一個(gè)數(shù)據(jù)庫(kù)框架。問(wèn)題來(lái)了,市面上那么多數(shù)據(jù)庫(kù)組件,比如ormLite,greendao等等,為什么google還要出一個(gè)room,有什么優(yōu)勢(shì)呢?
- 性能優(yōu)勢(shì),一次數(shù)據(jù)庫(kù)操作主要包括:構(gòu)造sql語(yǔ)句—編譯語(yǔ)句—傳入?yún)?shù)—執(zhí)行操作。ORMLite主要在獲取參數(shù)屬性值的時(shí)候,是通過(guò)反射獲取的,所以速度較慢。GreenDao在構(gòu)造sql語(yǔ)句的時(shí)候是通過(guò)代碼拼接,所以較慢。Room是通過(guò)接口方法的注解生成sql語(yǔ)句,也就是編譯成字節(jié)碼的時(shí)候就生成了sql語(yǔ)句,所以運(yùn)行起來(lái)較快。
- 支持jetpack其他組件(比如LiveData,Paging)以及RxJAVA,這就好比借助了當(dāng)前所在的優(yōu)勢(shì)環(huán)境,就能給你帶來(lái)一些得天獨(dú)厚的優(yōu)勢(shì)。當(dāng)然實(shí)際使用起來(lái)也確實(shí)要方便很多,比如liveData結(jié)合,就能在數(shù)據(jù)查詢后進(jìn)行自動(dòng)UI更新。
既然Room這么優(yōu)秀,那就用起來(lái)吧。 Room的接入主要有三大點(diǎn):DataBase、Entity、Dao。分別對(duì)應(yīng)數(shù)據(jù)庫(kù),表和數(shù)據(jù)訪問(wèn)。
1)首先導(dǎo)入庫(kù):
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ù)庫(kù)類,聲明數(shù)據(jù)庫(kù)表成員,數(shù)據(jù)庫(kù)名稱,數(shù)據(jù)庫(kù)版本,單例等等
@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>)
}
然后就可以進(jìn)行數(shù)據(jù)庫(kù)操作了,很簡(jiǎn)單吧。
Paging
分頁(yè)庫(kù)可幫助您一次加載和顯示一小塊數(shù)據(jù)。按需載入部分?jǐn)?shù)據(jù)會(huì)減少網(wǎng)絡(luò)帶寬和系統(tǒng)資源的使用量。
所以Paging就是一個(gè)分頁(yè)庫(kù),主要用于Recycleview列表展示。下面我就結(jié)合Room說(shuō)說(shuō)Paging的用法。 使用Paging主要注意兩個(gè)類:PagedList和PagedListAdapter。1)PagedList用于加載應(yīng)用數(shù)據(jù)塊,綁定數(shù)據(jù)列表,設(shè)置數(shù)據(jù)頁(yè)等。結(jié)合上述Room的Demo我繼續(xù)寫了一個(gè)UserModel進(jìn)行數(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個(gè)用戶
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,所以這里需要綁定一個(gè)繼承自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ù),進(jìn)行數(shù)據(jù)更新用。
ok,數(shù)據(jù)源,adapter都設(shè)置好了,接下來(lái)就是監(jiān)聽(tīng)數(shù)據(jù),刷新數(shù)據(jù)就可以了
// 監(jiān)聽(tīng)users數(shù)據(jù),數(shù)據(jù)改變調(diào)用submitList方法
viewModel.users.observe(this, Observer(adapter::submitList))
對(duì),就是這么一句,監(jiān)聽(tīng)PagedList,并且在它改變的時(shí)候調(diào)用PagedListAdapter的submitList方法。 這分層夠爽吧,其實(shí)這也就是paging或者說(shuō)jetpack給我們項(xiàng)目帶來(lái)的優(yōu)勢(shì),層層解耦,adapter都不用維護(hù)list數(shù)據(jù)源了。
ViewModel
ViewModel 類旨在以注重生命周期的方式存儲(chǔ)和管理界面相關(guān)的數(shù)據(jù)。ViewModel 類讓數(shù)據(jù)可在發(fā)生屏幕旋轉(zhuǎn)等配置更改后繼續(xù)留存。
終于說(shuō)到ViewModel了,其實(shí)之前的demo都用了好多遍了,ViewModel主要是從界面控制器邏輯中分離出視圖數(shù)據(jù),為什么要這么做呢?主要為了解決兩大問(wèn)題:
- 以前Activity中如果被系統(tǒng)銷毀或者需要重新創(chuàng)建的時(shí)候,頁(yè)面臨時(shí)性數(shù)據(jù)都會(huì)丟失,需要通過(guò)onSaveInstanceState() 方法保存,onCreate方法中讀取。而且數(shù)據(jù)量一大就更加不方便了。
- 在Activity中,難免有些異步調(diào)用,所以就會(huì)容易導(dǎo)致界面銷毀時(shí)候,這些調(diào)用還存在。那就會(huì)發(fā)生內(nèi)存泄漏或者直接崩潰。
所以ViewModel誕生了,還是解耦,我把數(shù)據(jù)單獨(dú)拿出來(lái)管理,還加上生命周期,那不就可以解決這些問(wèn)題了嗎。而且當(dāng)所有者 Activity 完全銷毀之后,ViewModel會(huì)調(diào)用其onCleared()方法,以便清理資源。
接下來(lái)舉個(gè),看看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ìn)行數(shù)據(jù)監(jiān)聽(tīng)等操作。等等,你能發(fā)現(xiàn)什么不? 對(duì)了,數(shù)據(jù)通信。不同的 Fragment 可以使用其父Activity共享ViewModel 來(lái)進(jìn)行數(shù)據(jù)的通信,厲害吧。還有很多其他的用法,去項(xiàng)目中慢慢發(fā)現(xiàn)吧!
WorkManager
使用 WorkManager API 可以輕松地調(diào)度即使在應(yīng)用退出或設(shè)備重啟時(shí)仍應(yīng)運(yùn)行的可延遲異步任務(wù)。
聽(tīng)聽(tīng)這個(gè)介紹就很神奇了,應(yīng)用退出和設(shè)備重啟都能自動(dòng)運(yùn)行?通過(guò)廣播?那數(shù)據(jù)又是怎么保存的呢?聽(tīng)說(shuō)還可以執(zhí)行周期性異步任務(wù),順序鏈?zhǔn)秸{(diào)用哦!接下來(lái)一一解密
一般這個(gè)API應(yīng)用到什么場(chǎng)景呢?想想,可靠運(yùn)行,還可以周期異步。 對(duì)了,發(fā)送日志。可以通過(guò)WorkManager設(shè)定周期任務(wù),每天執(zhí)行一次發(fā)送日志的任務(wù)。而且能夠保證你的任務(wù)可靠運(yùn)行,一定可以上傳到,當(dāng)然也是支持監(jiān)聽(tīng)任務(wù)結(jié)果等。:
1)導(dǎo)入庫(kù)
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ò)鏈接的時(shí)候使用
.setRequiredNetworkType(NetworkType.CONNECTED)
//是否在設(shè)備空閑的時(shí)候執(zhí)行
.setRequiresDeviceIdle(false)
//是否在低電量的時(shí)候執(zhí)行
.setRequiresBatteryNotLow(true)
//是否在內(nèi)存不足的時(shí)候執(zhí)行
.setRequiresStorageNotLow(true)
//是否時(shí)充電的時(shí)候執(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)聽(tīng)執(zhí)行結(jié)果
WorkManager.getInstance(this)
// .getWorkInfosByTagLiveData("uploadTag") //通過(guò)tag拿到work
.getWorkInfoByIdLiveData(uploadRequest.id) //通過(guò)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ù)鏈?zhǔn)巾樞蛘{(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)組件講完了,大家吃應(yīng)該吃飽了吧哈哈。希望這篇文章能讓不怎么熟悉Jetpack的同學(xué)熟悉熟悉。
當(dāng)然,這還遠(yuǎn)遠(yuǎn)不夠,在我看來(lái),本文更像是一個(gè)科普文,只是告訴了大家Jetpack-架構(gòu)組件有哪些成員,有什么用處。實(shí)際項(xiàng)目中,我們還需要建立MVVM的思想,深刻了解每個(gè)組件的設(shè)計(jì)意義,靈活運(yùn)用組件。最后希望大家都能通過(guò)jetpack構(gòu)建高質(zhì)量,簡(jiǎn)易并優(yōu)質(zhì)的項(xiàng)目架構(gòu),從而解放生產(chǎn)力,成為效率達(dá)人。
當(dāng)然了,本文所列出的知識(shí)點(diǎn)還不完全,要比較系統(tǒng)的學(xué)習(xí),我這里可以分享我一份Jetpack的資料。
更重要的是,還有大佬收錄整理的Android學(xué)習(xí)PDF+架構(gòu)視頻+面試文檔+源碼筆記,高級(jí)架構(gòu)技術(shù)進(jìn)階腦圖、Android開(kāi)發(fā)面試專題資料,高級(jí)進(jìn)階架構(gòu)資料
這些都是我現(xiàn)在閑暇還會(huì)反復(fù)翻閱的精品資料。里面對(duì)近幾年的大廠面試高頻知識(shí)點(diǎn)都有詳細(xì)的講解。相信可以有效的幫助大家掌握知識(shí)、理解原理。
當(dāng)然你也可以拿去查漏補(bǔ)缺,提升自身的競(jìng)爭(zhēng)力。
如果你有需要,可以私信或者評(píng)論獲取
喜歡本文的話,不妨順手給我點(diǎn)個(gè)贊、評(píng)論區(qū)留言或者轉(zhuǎn)發(fā)支持一下唄~


