8年老Android開發(fā)談:還搞不懂Jetpack架構(gòu)組件,淘汰就離你不遠了

前言

最近兩年碴倾,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湖饱。

官方文檔
Demo代碼地址

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)
    }
}    
復制代碼

官方文檔
Demo代碼地址

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ā)送消息锅知,所以也就避免崩潰問題售睹。

官方文檔
Demo代碼地址

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甘桑。
  1. 修改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)看看

官方文檔
Demo代碼地址

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ù)源了敏沉。

官方文檔
Demo代碼地址

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)吧怔毛!

官方文檔
Demo代碼地址

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()
    }

復制代碼

官方文檔
Demo代碼地址

總結(jié)

Jetpack-架構(gòu)組件講完了幽污,大家吃??應該吃飽了吧哈哈。希望這篇文章能讓不怎么熟悉Jetpack的同學熟悉熟悉簿姨。

當然距误,這還遠遠不夠,在我看來扁位,本文更像是一個科普文准潭,只是告訴了大家Jetpack-架構(gòu)組件有哪些成員,有什么用處域仇。實際項目中刑然,我們還需要建立MVVM的思想,深刻了解每個組件的設(shè)計意義暇务,靈活運用組件泼掠。(附件有個項目是官方的Jetpack實踐項目,可以看看)
最后希望大家都能通過jetpack構(gòu)建高質(zhì)量垦细,簡易并優(yōu)質(zhì)的項目架構(gòu)择镇,從而解放生產(chǎn)力,成為效率達人括改。

有用的話腻豌,點個贊吧?( ′???` )比心

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嘱能,隨后出現(xiàn)的幾起案子吝梅,更是在濱河造成了極大的恐慌,老刑警劉巖焰檩,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憔涉,死亡現(xiàn)場離奇詭異,居然都是意外死亡析苫,警方通過查閱死者的電腦和手機兜叨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門穿扳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人国旷,你說我怎么就攤上這事矛物。” “怎么了跪但?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵履羞,是天一觀的道長。 經(jīng)常有香客問我屡久,道長忆首,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任被环,我火速辦了婚禮糙及,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筛欢。我一直安慰自己浸锨,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布版姑。 她就那樣靜靜地躺著柱搜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪剥险。 梳的紋絲不亂的頭發(fā)上聪蘸,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音炒嘲,去河邊找鬼宇姚。 笑死,一個胖子當著我的面吹牛夫凸,可吹牛的內(nèi)容都是我干的纫事。 我是一名探鬼主播新荤,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恒序!你這毒婦竟也來了衷咽?” 一聲冷哼從身側(cè)響起鸽扁,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镶骗,沒想到半個月后桶现,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡鼎姊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年骡和,在試婚紗的時候發(fā)現(xiàn)自己被綠了相赁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡慰于,死狀恐怖钮科,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婆赠,我是刑警寧澤绵脯,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站休里,受9級特大地震影響蛆挫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妙黍,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一悴侵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧废境,春花似錦畜挨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至驮宴,卻和暖如春逮刨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背堵泽。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工修己, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人迎罗。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓睬愤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纹安。 傳聞我的和親對象是個殘疾皇子尤辱,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355