天天看點

Jetpack Room,Navigation,ViewModelJetpack

Jetpack

Gayhub位址

本頁面内容僅為個人學習筆記,受限于微網誌的能力,可能存在一定概念或者了解上的問題。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-AmbtnUAq-1629425442877)(https://i.loli.net/2021/07/20/OM42k9NuhpxfUTG.png)]

1.什麼是Jetpack

Jetpack就是Google官方推出的一套友善開發者的庫。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-fGUK8dd7-1629425442879)(https://i.loli.net/2021/07/20/wnpGKRFxuNUavQb.png)]

Jetpack Room,Navigation,ViewModelJetpack

其緻力于

  • 遵循最佳做法

    [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gztuuiIi-1629425442883)(https://i.loli.net/2021/07/20/9ivqUOHfAy8nZY1.png)]

    https://developer.android.google.cn/stories/apps/iheartradio

  • 減少樣闆代碼

    [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-d1IeikoR-1629425442884)(https://i.loli.net/2021/07/20/C2k1R8xUsDmq5LN.png)]

    https://developer.android.google.cn/stories/apps/monzo-camerax

  • 減少不一緻

    [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Ik4yoNNo-1629425442885)(https://i.loli.net/2021/07/20/XmBs82ZTqAdejbI.png)]

    https://developer.android.google.cn/jetpack/testimonials

    總之不管男女老少用過都說好

2.在項目中引入Jetpack

沒想到吧,當我們建立App的時候build.gradle已經為我們添加了Jetpack的支援。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-x8j4x1LN-1629425442886)(https://i.loli.net/2021/07/20/KAIGsouOpckCERy.png)]

在引入google()之後便可以在dependences添加對應的Jetpack元件了。比如LiveData,Lifecycle,ViewModel,Navigation…

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-592XODAz-1629425442886)(https://i.loli.net/2021/07/20/umPeiCO8lTVf4W6.png)]

…然後就是一番感人的調包環節了。

3.細化Jetpack元件的使用

  • LiveData
  • ViewModel
  • Lifecycle
  • Room
  • Navigation
  • DataBinding/ViewBindingg
  • Dagger2/Hilt

ViewModel

1.什麼是ViewModel

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-IhsQOt6f-1629425442887)(https://i.loli.net/2021/07/20/XkwNftWE4epUzJL.png)]

借用google的一句話就是緩存資料的,當我的Activity發生配置變化時候會重新調用onCreate方法建立新的一個Activity的執行個體。這會導緻螢幕内的資料丢失。這很不符合使用者預想中的使用,是以通常情況下我們會通過

onSaveInstanceState()

來儲存并拯救丢失的資料。但是

onSaveInstanceState()

隻可以序列化再反序列化的少量資料,而不适合數量可能較大的資料,是以它不太适合存儲整個頁面的資料。是以就有了ViewModel,但ViewModel并不是

onSaveInstanceState()

的替代品。

2.ViewModel的初步使用

Code

Tips:代碼在

com/example/viewmodeldemo/MainActivity.kt,

com/example/viewmodeldemo/ui/activity/vm/MainViewModel.kt

檔案中

step1 建立ViewMode

package com.example.viewmodeldemo

import androidx.lifecycle.ViewModel

/**
 *@author ZhiQiang Tu
 *@time 2021/7/19  11:18
 *@signature 我将追尋并擷取我想要的答案
 */

class MainViewModel : ViewModel(){
    var number:Long = 0
        private set
    fun plusOne(){
        number++
    }
    fun plusTwo(){
        number+=2
    }
}
           

step2 在視圖元件(Activity,Fragment,…)中擷取ViewModel的執行個體

package com.example.viewmodeldemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    //懶加載ViewModel的執行個體
    private val viewModel:MainViewModel by lazy { ViewModelProvider(this,ViewModelProvider.NewInstanceFactory()).get(MainViewModel::class.java)}
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initView()
        //這種寫法可能會出現空指針,建議使用ViewBinding/DataBinding
        plus_one.setOnClickListener {
            viewModel.plusOne()
            updateTextView()
        }
        plus_two.setOnClickListener {
            viewModel.plusTwo()
            updateTextView()
        }
    }
	//更新視圖
    private fun updateTextView() {
        show_number.text = viewModel.number.toString()
    }
	//初始化視圖
    private fun initView() {
        updateTextView()
    }

}
           

3.ViewModel的進一步探究

1.ViewModel的生命周期

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-KErOFEPz-1629425442888)(https://i.loli.net/2021/07/20/FSKdxCNgID9f2sO.png)]

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-N1mOp5PZ-1629425442889)(https://i.loli.net/2021/07/20/Mynz27C9Ku8BfRs.png)]

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-8ryEp1EN-1629425442889)(https://i.loli.net/2021/07/20/nkCA96upUvZgoR2.png)]

也就是說ViewModel的生命周期依托于ViewModelProvider傳入的lifecycle參數,而lifecycle接口又由Activity,Fragment等實作,故ViewModel會和視圖元件間接性的形成聯系。并且在lifecycle元件第一次調用onCreate後初始化VIewModel(在之後的配置變化如螢幕旋轉等都不會再次建立ViewModel),等到lifecycle元件調用了onDestroy方法,并且徹底涼透了,ViewModel才會調用onCleared釋放記憶體。

總體來說

  • ViewModel依賴于Lifecycle元件。在Lifecycle元件利用ViewModelProvider建立ViewModel執行個體的時候建立聯系,并在Lifecycle元件第一次調用onCreate時候建立ViewModel,在Lifecycle元件徹底涼透了再釋放ViewModel記憶體。
  • ViewModel的生命周期長于Activity。我們不能讓ViewModel持有Lifecycle元件。否者會發生記憶體洩漏。
2.ViewModel的種類
  • 普通ViewModel

  • AndroidViewModel
    這是一個具有application的ViewModel除此之外與其他的ViewModel并沒有什麼不同(這個AndriodViewModel并***不是***說生命周期綁定的application,它生命周期綁定的***還是***this(lifecycle),***隻是構造函數中被傳入了一個application參數。***)

    Code

    Tipes:

    代碼在

    com/example/viewmodeldemo/ui/activity/vm/DemoAndroidViewModel.kt,

    com/example/viewmodeldemo/ui/activity/presentation/AndroidViewModelActivity.kt

    檔案中

    step1 建立ViewModel
    package com.example.viewmodeldemo.vm
    
    import android.app.Application
    import androidx.lifecycle.AndroidViewModel
    
    /**
     *@author ZhiQiang Tu
     *@time 2021/7/19  21:06
     *@signature 我們不明前路,卻已在路上
     */
    class DemoAndroidViewModel(application: Application) : AndroidViewModel(application) {
        val mApplication = getApplication<Application>()
        
    }
               
    step2 執行個體化AndroidViewModel
    class AndroidViewModelActivity : AppCompatActivity() {
        private val viewModel:DemoAndroidViewModel by lazy { ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(DemoAndroidViewModel::class.java) }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_android_view_model)
        }
    }
               
  • SharedViewModel

SharedViewModel實作了同Activity上的Fragment之間的資料共享。

這就是一個以Activity為Lifecycle的Fragment的ViewModel。

有點繞,也就是說這個ViewModel是給Fragment用的但是建立ViewModelProvider用的ViewModelStoreOwner卻是Fragment所Attach的activity。

因為隻有将ViewModelStoreOwner變成activity才能實作fragment間資料的共享。

Code

Tips:代碼在

com/example/viewmodeldemo/ui/activity/presentation/SharedViewModelActivity.kt,

com/example/viewmodeldemo/ui/fragment/presentation

檔案中

step1 建立ViewModel

package com.example.viewmodeldemo.ui.fragment.vm

import androidx.lifecycle.ViewModel
import com.example.viewmodeldemo.ui.fragment.model.DemoData

/**
 *@author ZhiQiang Tu
 *@time 2021/7/19  21:46
 *@signature 我們不明前路,卻已在路上
 */
class SharedViewModel: ViewModel() {
    //測試
    var data:DemoData = DemoData(0,"data")
}
           

step2 建立Fragment并執行個體化ViewModel

package com.example.viewmodeldemo.ui.fragment.presentation

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.viewmodeldemo.R
import com.example.viewmodeldemo.ui.fragment.vm.SharedViewModel
import kotlinx.android.synthetic.main.fragment_demo01.*

private const val TAG = "DemoFragment01"

class DemoFragment01 : Fragment() {
    private val viewModel by lazy { ViewModelProvider(requireActivity()).get(SharedViewModel::class.java) }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        return inflater.inflate(R.layout.fragment_demo01, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        textView.text = "目前viewModel為$viewModel"
        textView2.text = "目前data為${viewModel.data}"
        Log.e(TAG, "目前viewModel為$viewModel" )
        Log.e(TAG,  "目前data為${viewModel.data}")
    }

}
           
package com.example.viewmodeldemo.ui.fragment.presentation

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.viewmodeldemo.R
import com.example.viewmodeldemo.ui.fragment.vm.SharedViewModel
import kotlinx.android.synthetic.main.fragment_demo02.*
import kotlin.math.log

private const val TAG = "DemoFragment02"

class DemoFragment02 : Fragment() {
    private val viewModel by lazy { ViewModelProvider(requireActivity()).get(SharedViewModel::class.java) }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_demo02, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        textView4.text = "目前viewModel為$viewModel"
        textView5.text = "目前data為${viewModel.data}"
        Log.e(TAG, "目前viewModel為$viewModel" )
        Log.e(TAG,  "目前data為${viewModel.data}")
    }
    
}
           

我們在兩個Fragment拿到的ViewModel和ViewModel的data的hashCode是一樣的

Jetpack Room,Navigation,ViewModelJetpack
  • 自定義構造器的ViewModel
    從之前的幾種ViewModel中我們可以分成兩類:
    • 一類是預設構造含函數的ViewModel比如

      SharedViewModel

      ,最基礎的ViewModel。
    • 另外一就是非預設構造函數的ViewModel比如AndroidViewModel。

    那如何建立一個自定義的構造函數的ViewModel呢?

    那不簡單,這樣嘛

    //ViewModel
    class MyViewModel(val myData:Data):ViewModel()	
    
    //初始化
    val viewModel = MyViewModel(myData)
               

    我竟無法反駁。

    值得注意的是當我們建立一個ViewModel的時候是利用的ViewModelProvider建立的,不是直接

    MyViewModel(myData)

    這樣new出來,是以上述的方法貌似沒什麼用。

    回歸ViewModelProvider上看看

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }
               

你發現了什麼,他有一個構造方法需要傳入兩個參數,一個owner一個

Factory

,哦是以自定義的構造函數的參數的傳遞需要靠這玩意了呗。

Code

Tips:代碼在

com/example/viewmodeldemo/ui/activity/vm/CustomViewModel.kt,

com/example/viewmodeldemo/ui/activity/presentation/CustomFactoryViewModelActivity.kt

檔案中

step1 建立ViewModel

package com.example.viewmodeldemo.ui.activity.vm

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.viewmodeldemo.ui.fragment.model.DemoData

/**
 *@author ZhiQiang Tu
 *@time 2021/7/20  7:34
 *@signature 我們不明前路,卻已在路上
 */
private const val TAG = "CustomViewModel"
class CustomViewModel(var data: DemoData) : ViewModel(){
    fun logData(){
        //檢測data是否真的被傳入了
        Log.e(TAG, "$data")
    }
}
           

step2 自定義Factory

class CustomFactory:ViewModelProvider.Factory{
    //這個方法是ViewModel内部調用建立ViewModel執行個體的,是以它的任務就隻是傳回一個ViewModel,你怎麼傳回它并不關心。
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        val data = DemoData(0,"data")
        val customViewModel = CustomViewModel(data)
        return customViewModel as T
    }

}
           

step3 在owner元件中初始化ViewModel執行個體

package com.example.viewmodeldemo.ui.activity.presentation

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import com.example.viewmodeldemo.R
import com.example.viewmodeldemo.ui.activity.vm.CustomFactory
import com.example.viewmodeldemo.ui.activity.vm.CustomViewModel

class CustomFactoryViewModelActivity : AppCompatActivity() {
    private val viewModel by lazy { ViewModelProvider(this,CustomFactory()).get(CustomViewModel::class.java) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_custom_factory_view_model)
        viewModel.logData()
    }
}
           

注意這個factory必須要傳入的哦,不傳入就會這樣。

Jetpack Room,Navigation,ViewModelJetpack
Caused by: java.lang.InstantiationException: java.lang.Class<com.example.viewmodeldemo.ui.activity.vm.CustomViewModel> has no zero argument constructor

不傳入預設就是無參構造函數,

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-zL8WYHXu-1629425442891)(https://i.loli.net/2021/07/20/XT9WwpM581BDlRJ.png)]

内部通過使用get函數裡面的class參數進行反射建立ViewModel。

而ViewModel并沒有無參構造,這就直接crash了。

3.ViewModel+Ktx擴充

看看下面幾個ViewModel的初始化方法。

//1
private val viewModel: DemoAndroidViewModel by lazy { ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(DemoAndroidViewModel::class.java) }

//2
private val viewModel by lazy { ViewModelProvider(this).get(CustomViewModel::class.java) }

//3
private val viewModel by lazy { ViewModelProvider(requireActivity()).get(SharedViewModel::class.java) }
           

太長了對吧。而且這個初始化很模闆化。就是

ViewModelProvider(viewModel所聯接的元件,factory).get(你所需要建立的ViewModel的class參數)

為了簡化viewModel的初始化,ktx有更為簡單的擴充。

def lifecycle_version = "2.4.0-alpha02"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")

//ktx
implementation"androidx.activity:activity-ktx:1.2.2"
implementation"androidx.fragment:fragment-ktx:1.3.4"
           

然後之前冗長的代碼就變成了這樣,簡直爽翻天。

//1
private val viewModel:DemoAndroidViewModel by viewModels()

//2
private val viewModel:CustomViewModel by viewModels { CustomFactory() }

//3
private val viewModel:SharedViewModel by activityViewModels()
           
4.ViewModel失效了!
每當我們使用ViewModel的時候我們總是認為:ViewModel一定能幫我們在任何情況下儲存好界面的資料,然而真實情況是這樣的嗎?

進行以下設定

  • 打開設定面闆
    Jetpack Room,Navigation,ViewModelJetpack
  • 選擇開發者選項
    Jetpack Room,Navigation,ViewModelJetpack
  • 選擇應用設定,勾選切入背景不保留activity
Jetpack Room,Navigation,ViewModelJetpack

然後發生了很恐怖的事情,切入背景再回來,資料沒了。

效果圖(GIF)

Jetpack Room,Navigation,ViewModelJetpack

這是為什麼,螈來是當打開開發者設定***不保留背景程序***之後,切入背景之後,Activity會直接被系統鲨了,并且不調用任何生命周期方法。連帶着ViewModel都挂了,是以資料沒法儲存。還記得最早的時候說的:ViewModel不是onSaveInstanceState的替代品嗎?

系統殺死Activity是不會調用任何什麼周期方法的,那我們有什麼方法能拯救那些資料🐎?

其實是有的,在系統殺死Activity之前它留了一線生機。會調用onSaveInstanceState這會是你 恢複資料的希望。我們可以這樣寫。

Code

Tip:該代碼在com/example/viewmodeldemo/MainActivity.kt中

重寫onSaveInstanceState把需要存放的資料儲存下來

override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("number", show_number.text.toString())
    }
           

然後再onCreate方法中判斷一下savedInstanceState是不是空的。若不是空的就把資料取出來。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    savedInstanceState?.let {
        viewModel.number = (it.get("number") as String).toLong()
    }
    initView()
    setListener()
}
           

效果圖(GIF)

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Jx1hLhtF-1629425442892)(https://gitee.com/False_Mask/jetpack-demos-pics/raw/master/PicsAndGifs/viewmodel_saved_state.gif)]

除此之外我們還能使用ViewModel來實作。

不過還得加入一個依賴

// Saved state module for ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
           

Code

Tips:代碼位置

com/example/viewmodeldemo/MainActivity.kt,

com/example/viewmodeldemo/ui/activity/vm/MainViewModel.kt

還有一個需要提醒大家的是SavedStateHandle不适合把整個頁面的資料都儲存下來,它的定位是儲存最為重要的***一小部分資料***與onSaveInstanceState的定位是一樣的。

如下。

Jetpack Room,Navigation,ViewModelJetpack

對之前的MianViewModel***稍作修改***

package com.example.viewmodeldemo.ui.activity.vm

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel

/**
 *@author ZhiQiang Tu
 *@time 2021/7/19  11:18
 *@signature 我将追尋并擷取我想要的答案
 */
private val TAG = "MainViewModel"
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    var number:Long = if (savedStateHandle.contains("number")) {
        savedStateHandle.get<Long>("number") !!
    }
    else {
        savedStateHandle.set("number",0)
        0
    }

    fun plusOne(){
        number++
        updateSavedStateHandle(number)
    }

    private fun updateSavedStateHandle(number: Long) {
        savedStateHandle.set("number",number)
    }

    fun plusTwo(){
        number+=2
        updateSavedStateHandle(number)
    }
}
           

執行個體化ViewModel

注意這裡的

SavedStateViewModelFactory

構造函數需要傳入兩個參數,一個Application,一個是

SavedStateRegistryOwner

public SavedStateViewModelFactory(@Nullable  Application application,
        @NonNull SavedStateRegistryOwner owner) 


           

ComponentActivity

Fragment

已近實作了

LifecycleOwner

ViewModelStoreOwner

SavedStateRegistryOwner

接口。

其中

AppCompatActivity

繼承自

FragmentActivity

繼承自

ComponentActivity

Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
5.ViewModel的使用建議

❌ Don’t let ViewModels (and Presenters) know about Android framework classes

不要讓ViewModel知曉Android的FrameWork,ViewModel隻是用來寫點邏輯代碼和存資料的。

Jetpack Room,Navigation,ViewModelJetpack
我所讀的文章告訴我要将ViewModel和FrameWork隔離,最好的辦法就是在ViewModel中不要導入android.的包,(android.arch.除外)(也算是個好辦法。霧…)

✅ Keep the logic in Activities and Fragments to a minimum

盡量不要在Activity,Fragment中寫邏輯代碼。

這個在剛學Android的時候估計是非常常見的問題,一個Activity 六七百行人的寫傻了。

也算是設計模式的一個運用了,在MVVM中推薦将業務邏輯代碼寫在ViewModel中(這個其實也會存在問題的,後面有講)。

❌ Avoid references to Views in ViewModels.

不要将頁面元件(Activity,Fragment)放在ViewModel中,這個估計懂得都懂。ViewModel生命周期比Activity等owner要長一點,如果ViewModel持有Activity/Fragment等就會造成記憶體洩漏嘛。

✅ Instead of pushing data to the UI, let the UI observe changes to it.

在頁面View資料的更新上,不是讓ViewModel持有View通過set把資料裝載到UI上。而是讓UI 觀察ViewModel中的資料,當資料發生改變後自己更新。(觀察者模式也算的上MVVM中很重要的一部分了。

這我得跟你談談LiveData的含金量了。

Jetpack Room,Navigation,ViewModelJetpack
也就是說要讓ViewModel持有UI資料包裹的LiveData(不是MutableLiveData哦,LiveData更加符合面向對象的封裝性,而且隻要不是雙向綁定UI根本不會有更改資料的行為存在的是以沒必要暴露Mutable)最後在View去觀察對應資料的變化。

✅ Distribute responsibilities, add a domain layer if needed.

配置設定職責,在有需要的時候新加一個domain層。之前有提過在 ViewModel裡面寫邏輯代碼是存在一定問題的。那就是----ViewModel膨脹。如果app的業務邏輯比較複雜,那麼就會導緻ViewModel内代碼很多。是以很有必要對業務邏輯進行合理的分離。

Jetpack Room,Navigation,ViewModelJetpack
它給出了兩種方法:
  • 将一些業務邏輯配置設定到presenter中去,該presenter和ViewModel有相同的作用域。并且和app的其他闆塊進行互動并且更新ViewModel中的LiveData。
  • 像Clean Architecture一樣采取添加一個domain層。使得架構更加具有可測試性和可維護性。(Clean Architecture我不懂,真的,需要的自取 Clean Architecture

✅ Add a data repository as the single-point entry to your data

添加一個data repository,并且repository對資料的使用是單向的。

就和google推薦的架構差不多,repository對Model和Romote的聯接是單向箭頭。

Jetpack Room,Navigation,ViewModelJetpack
資料的擷取路徑它給出了3種
Jetpack Room,Navigation,ViewModelJetpack

Remote和Local就不必說了,增添了一種In-memory cache(記憶體緩存)。

最後就是如果你有很多的并且差異很明顯的資料,可以選擇開辟更多的Repository.

✅ Expose information about the state of your data using a wrapper or another LiveData.

通過包裝的方法,或者額外添加一個LiveData來提供資料的狀态、

其中資料狀态需要包含什麼?

MyDataState

could contain information about whether the data is currently loading, has loaded successfully or failed

狀态包含資料是否正在加載,是否加載完成,是否失敗等。

  • 通過添加額外的LiveData暴露資料狀态
    Jetpack Room,Navigation,ViewModelJetpack
    其中MyData是資料本身,MyDataState是資料加載的情況。
  • 通過裝飾模式暴露資料的狀态。
Jetpack Room,Navigation,ViewModelJetpack
data就是資料本身,message是資料的狀态資訊(這寫法不來由的想到了MVI架構中的ViewState)

✅ Design events as part of your state. For more details read LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case).

把事件當成狀态的一部分。好像這樣翻譯起來太過繞口。

我認為它想表達的意思是将Event進行封裝。

這個Event就是一些消費性事件,比如Snackbar彈窗,Toast,點選事件等這些不具有狀态的消費性事件。

為什麼要把它封裝起來封裝以後又放在哪?

1.我們知道presentation層(也就是Activity和Fragment)需要和ViewModel互動。

2.presentation層需要發送一個事件給ViewModel然後ViewModel處理這個事件。

3.比如一個登陸場景,使用者點選了LoginActivity的Login Button,然後LoginViewModel接受到一個登陸事件,這個登陸事件裡面包含了使用者名,密碼等配置資訊。LoginViewModel根據從這個事件裡面擷取的使用者名密碼發送網絡請求比對是否與伺服器上的一緻,最後更改對應的LiveData。最後Presentation觀察到LiveData變化做出響應(如登陸成功,登陸失敗。

4.當我們回首去看這個登陸流程的時候後我們會發現一個問題,這個登陸事件怎麼處理?

  • 有些人會直接通過對button設定監聽,當點選觸發直接調用viewModel裡面的方法把需要的參數直接傳入進去。不是很推薦,主要是寫法太過簡潔,不能裝*(雖然我找不出什麼問題但總是感覺怪怪的。
  • 還有些人會在xml裡面的onClick裡面通過dataBinding的單項綁定直接調用viewModel對象的方法,不過好像和上面的方法并沒有本質差別。也不是很推薦。
  • 推薦的方法式通過将這個LoginEvent封裝,因為這樣Testable(雖然我根本不懂啥是Test,并且邏輯更為清晰。

然後在需要的時候發送Event到ViewModel裡,不過為了保證這個事件是1次性消費事件還得做一些處理,比如。

具體的我就不做多的示範了,這個内容得有兩三篇部落格那麼長,況且我也沒看太懂。有興趣的可以自己看看。

SingleLiveEvent和Event確定事件為單次消費事件

LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

利用協程Flow庫確定事件為單次消費事件(2021最新解決方案

Android SingleLiveEvent Redux with Kotlin Flow

思來想去還是把代碼貼一下。

way1

通過繼承MutableLiveData建立一個特殊的MutableLiveData。

這個SingleLiveEvent確定了事件為單次的消費性事件。

但是存線上程不安全的問題。

/*
 *  Copyright 2017 Google Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.example.android.architecture.blueprints.todoapp;

import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 * <p>
 * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * <p>
 * Note that only one observer is going to be notified of changes.
 */
public class SingleLiveEvent<T> extends MutableLiveData<T> {

    private static final String TAG = "SingleLiveEvent";

    private final AtomicBoolean mPending = new AtomicBoolean(false);

    @MainThread
    public void observe(LifecycleOwner owner, final Observer<T> observer) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
        }

        // Observe the internal MutableLiveData
        super.observe(owner, new Observer<T>() {
            @Override
            public void onChanged(@Nullable T t) {
                if (mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
            }
        });
    }

    @MainThread
    public void setValue(@Nullable T t) {
        mPending.set(true);
        super.setValue(t);
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    public void call() {
        setValue(null);
    }
}
           
way2 使用裝飾器包裹一層。
/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

           

way3 利用協程Flow

這個我沒看。

4.内容來源

什麼是ViewModel,ViewModel的初步使用 ---- Google官方文檔,Google官方文檔提供的部落格
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
ViewModel + Ktx 擴充 ---- Bilibili
Jetpack Room,Navigation,ViewModelJetpack
ViewModel的生命周期 ---- Google官方文檔
ViewModel失效了 ---- Bilibili,Google官方文檔提供的部落格
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
ViewModel的使用建議 ---- Google官方文檔提供的部落格
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack

Navigation

1.什麼是Navigation

Jetpack Room,Navigation,ViewModelJetpack

也就是說Navigation是用來處理頁面之間的跳轉的(不知道是否準确,但應該不至于錯的離譜)。

在日常開發種我們經常會遇見頁面跳轉的,比如購物的時候點選商品清單之後會跳轉到商品詳細資訊。點選底部的導航欄會在不同的Fragment頁面間進行來回切換,點選抽屜式菜單在頁面中來回切換。等等一系列。除此之外我們在進行頁面跳轉的時候可能還會出現其他的需要加入考慮的事情,比如參數的傳遞,比如跳轉動畫的添加,比如Fragment的回退棧等…是以頁面跳轉一直都不僅僅是

startActivity(Intent(this,SecondActivity::class.java))

這麼簡單。

哪裡有困難哪裡就有 Jetpack,是以就有了Navigation的出現。

2.Navigation一覽

Navigation的組成

Navigation由3個部分組成

  • NavGrap
這是一個XML資源,它描述了頁面間的跳轉關系,從哪裡跳轉到哪裡,需要傳入什麼參數,跳轉過程有什麼動畫等等。
  • NavHostFragment
NavHostFragment是一個特殊的Fragment。它是Fragment的容器,我們可以将NavGrap通過XML的形式引入到NavHostFragment中,然後NavHostFragment會呈現相應的頁面。
  • NavController
從Controller可以看出它是一個用于管理的類,管理什麼?管理頁面的切換,管理NavHostFragment的視圖呈現。
Navigation的優勢
  • 處理 Fragment 事務。
  • 預設情況下,正确處理往返操作。
  • 為動畫和轉換提供标準化資源。
  • 實作和處理深層連結。
  • 包括導航界面模式(例如抽屜式導航欄和底部導航),使用者隻需完成極少的額外工作。
  • Safe Args - 可在目标之間導航和傳遞資料時提供類型安全的 Gradle 插件。
  • ViewModel

    支援 - 您可以将

    ViewModel

    的範圍限定為導航圖,以在圖表的目标之間共享與界面相關的資料。

    就是navGraphViewModels

    //就像這樣 這樣的ViewModel的生命周期與傳入的navGraph一種
    val viewModel:TestViewModel by navGraphViewModels(R.id.nav_demo)
               
此外,您還可以使用 Android Studio 的 Navigation Editor 來檢視和編輯導航圖。(應該沒有人手打Navigation吧.haha)

3.摸一摸Navigation

也就是Navigation的基本使用吧。

第一步加入依賴

有兩種辦法

  • way 1 去Google官方文檔上檢視。

    位址

dependencies {
  val nav_version = "2.3.5"

  // Java language implementation
  implementation("androidx.navigation:navigation-fragment:$nav_version")
  implementation("androidx.navigation:navigation-ui:$nav_version")

  // Kotlin
  implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
  implementation("androidx.navigation:navigation-ui-ktx:$nav_version")

  // Feature module Support
  implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")

  // Testing Navigation
  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")

  // Jetpack Compose Integration
  implementation("androidx.navigation:navigation-compose:2.4.0-alpha04")
}
           
  • way 2 讓Android Studio自己幫我們導入
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
看自動導入了兩個依賴
Jetpack Room,Navigation,ViewModelJetpack

第二步建立Fragment或者需要跳轉的Activity

我這裡建立了兩個Fragment

Jetpack Room,Navigation,ViewModelJetpack
第三步将Fragment或者需要跳轉的Activity添加到NavGrap中
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack

第四步通過Navagation Editor建立跳轉關系

拖動demoFragment01的小圓點與demoFragment02相連

Jetpack Room,Navigation,ViewModelJetpack
這樣一個跳轉關系就建立了。
Jetpack Room,Navigation,ViewModelJetpack

第五步使用NavHostFragment呈現Fragment

之前說過NavHostFragment是Fragment的容器,它可以展示Fragment。前幾步我們完成了對跳轉關系的配置,Fragment的建立,但是并沒有将Fragment展示出來。也就是說現在的app還是一片空白。

在MainActivity中添加NavHostFragment

Jetpack Room,Navigation,ViewModelJetpack
選擇對應的NavGrap(由于NavHostFragment需要進行頁面的呈現,是以它必須知道頁面的跳轉配置,這樣它才知道什麼時候應該呈現什麼樣的布局。)
Jetpack Room,Navigation,ViewModelJetpack
稍微寫了一下Fragment的界面
Jetpack Room,Navigation,ViewModelJetpack
現在Fragment已近可以呈現到Activity上了,但是跳轉的邏輯還沒有寫。
Jetpack Room,Navigation,ViewModelJetpack

第六步實作Fragment之間的跳轉

對主Fragment的按鈕進行監聽,點選事件觸發以後直接調用navigation的api。先通過Fragment的擴充方法findNavController尋找到當下的NavController執行個體,在通過調用NavController的navigate方法實作跳轉。

也就是說管理跳轉的是NavController對象

demo01_jump_button.setOnClickListener {
    findNavController().navigate(R.id.action_demoFragment01_to_demoFragment02)
}
           

不算總結的總結

完成一個Navigation的跳轉需要完成:

  • 添加依賴
  • 建立Fragment或者需要跳轉的Activity
  • 将Fragment或者需要跳轉的Activity添加到NavGraph中
  • 通過Navagation Editor建立跳轉關系
  • 将NavHostFragment添加到Activity的XML中
  • 通過NavController實作跳轉

4.Navigation初級探究

1. NavHostFragment XML布局參數解析

參考自

如果你打開XML布局去尋找NavHostFragment的時候你或許會驚奇。因為并沒有NavHostFragment的标簽。
Jetpack Room,Navigation,ViewModelJetpack
有的隻是一個

FragmentContainerView

Jetpack Room,Navigation,ViewModelJetpack

然鵝确是有NavHostFragment這個類的

我們可以通過

NavHostFragment

的name參數指定

NavHostFragment

于此同時使用fragment标簽指定name參數的效果也是一樣的。

是以就把NavHostFragment看成是一個特殊的Fragment吧。

  • android:name

    屬性包含

    NavHost

    實作的類名稱。隻要你使用的是NavHostFragment,就把NavHostFragment的包路徑抄下來吧。androidx.navigation.fragment.NavHostFragment
  • app:navGraph

    屬性将

    NavHostFragment

    與導航圖相關聯。導航圖會在此

    NavHostFragment

    中指定使用者可以導航到的所有目的地。也就是說通過這個将NavGraph資源引入
  • app:defaultNavHost="true"

    屬性確定您的

    NavHostFragment

    會攔截系統傳回按鈕。請注意,隻能有一個預設

    NavHost

    。如果同一布局(例如,雙窗格布局)中有多個宿主,請務必僅指定一個預設

    NavHost

    。true表示你的傳回操作會被送出到NavHostFragment中處理,值得注意的是隻能有一個NavHost,如果一個界面有多個NavHostFragment務必隻選取一個将app:defaultNavHost設定為true其餘是以為false。
2.目的地XML解析

參考自

什麼是目的地?

這就是。

Jetpack Room,Navigation,ViewModelJetpack

Navigation Editor裡面的所有頁面都是目的地。

XML參數解析

  • id 由于目的地與目的地之間會存在跳轉關系,需要描述從哪裡到哪裡,id就是區分不同destination的參數
  • name 也就是目前元件的引用位址,好讓NavGraph知道這是什麼Fragment或者Activity…
  • label 标簽,在和頂部的Toolbar或者Actionbar進行關聯的時候,Navigation會使用label的值作為其标題。
    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack
3.action标簽
  • android:id​ 用于區分不同的action
  • app:destination 與名稱想表達的意思一樣,就是跳轉的目的地的id
4.導航到目的地

參考自

Tips:Navigation還可以導航到Activity,和導航Fragment是類似的,下面執行個體就不多講Navigation在Activity之間的跳轉,不懂可以看看參考文檔。

導航到目的地是使用 NavController完成的,它是一個在

NavHost

中管理應用導航的對象。每個

NavHost

均有自己的相應

NavController

。您可以使用以下方法之一檢索

NavController

在Kotlin中可以直接使用findNavController()擷取NavController

Jetpack Room,Navigation,ViewModelJetpack
其中

Activity的.findNavController

其實是存在一定問題的。

如果直接傳入”NavHostFragment“ 的id

他會報錯

Jetpack Room,Navigation,ViewModelJetpack

說在Activity中找不到NavController

但是如果傳入的id是這個

Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack

很奇怪…又可以了。

先不糾結了。

是以在Activity中擷取NavController最好改成這樣

val navHostFragment =
    supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
val navController = navHostFragment.navController
           

之後就是調用navigate方法。

NavController有13個重載的navigate()方法。在此不做多的解釋了。可以自行翻閱。

5.使用 Safe Args插件進行傳遞參數

safe args如其名稱一樣是安全的,它確定了類型的安全性。

Tips:這裡也閹割了Activity的傳參。

參考自

參考自

依賴
buildscript {
 repositories {
     google()
 }
 dependencies {
     val nav_version = "2.3.5"
     classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version")
 }
}
           
plugins {
 id("androidx.navigation.safeargs")
}

plugins {
 id("androidx.navigation.safeargs.kotlin")
}
           

Safe Args會幫我們生成一些代碼,然後確定傳遞參數時的類型安全。

我們可以試着對比一下

原生Bundle傳參

先建立了一個User類

package com.example.navigationdemo.model

import java.io.Serializable

/**
*@author ZhiQiang Tu
*@time 2021/7/21  16:53
*@signature 我們不明前路,卻已在路上
*/
data class User(val username:String,val age:Int):Serializable
           
然後在navigate之前new一個User,放進bundle裡面,再通過navigate傳入。
val bundle = Bundle().also {
 it.putSerializable("user",User("tr",19))
}
findNavController().navigate(R.id.demoFragment02,bundle)
           
再在目的地擷取bundle強轉
val user = arguments?.get("user") as User
           

這裡就出問題了。

第一argument需要判空,如果你是用的java,寫的還可能存在空指針…

第二通過argument.get到的是一個Object。也就是說你強轉成String編譯器在編譯階段也不會報錯。

Jetpack Room,Navigation,ViewModelJetpack

這樣就埋下了隐患。程式一運作就crash了,java.lang.ClassCastException: com.example.navigationdemo.model.User cannot be cast to java.lang.String

使用Safe Args傳參

使用Safe Args傳參的時候需要在NavGraph中建立跳轉關系也就是action。否者就會是這樣的,什麼都沒有生成毫無作用。

Jetpack Room,Navigation,ViewModelJetpack
連接配接以後隻生成了這麼一個類
Jetpack Room,Navigation,ViewModelJetpack
接着我們在跳轉的終點加入所需要的參數。
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
在rebuild一下又生成了一個類
package com.example.navigationdemo.ui.fragment

import android.os.Bundle
import android.os.Parcelable
import androidx.navigation.NavArgs
import com.example.navigationdemo.model.User
import java.io.Serializable
import java.lang.IllegalArgumentException
import java.lang.UnsupportedOperationException
import kotlin.Suppress
import kotlin.jvm.JvmStatic

public data class DemoFragment02Args(
public val user: User
) : NavArgs {
@Suppress("CAST_NEVER_SUCCEEDS")
public fun toBundle(): Bundle {
 val result = Bundle()
 if (Parcelable::class.java.isAssignableFrom(User::class.java)) {
   result.putParcelable("user", this.user as Parcelable)
 } else if (Serializable::class.java.isAssignableFrom(User::class.java)) {
   result.putSerializable("user", this.user as Serializable)
 } else {
   throw UnsupportedOperationException(User::class.java.name +
       " must implement Parcelable or Serializable or must be an Enum.")
 }
 return result
}

public companion object {
 @JvmStatic
 public fun fromBundle(bundle: Bundle): DemoFragment02Args {
   bundle.setClassLoader(DemoFragment02Args::class.java.classLoader)
   val __user : User?
   if (bundle.containsKey("user")) {
     if (Parcelable::class.java.isAssignableFrom(User::class.java) ||
         Serializable::class.java.isAssignableFrom(User::class.java)) {
       __user = bundle.get("user") as User?
     } else {
       throw UnsupportedOperationException(User::class.java.name +
           " must implement Parcelable or Serializable or must be an Enum.")
     }
     if (__user == null) {
       throw IllegalArgumentException("Argument \"user\" is marked as non-null but was passed a null value.")
     }
   } else {
     throw IllegalArgumentException("Required argument \"user\" is missing and does not have an android:defaultValue")
   }
   return DemoFragment02Args(__user)
 }
}
}
           
然後在跳轉的時候初始化一個NavDirections作為navigate的參數傳遞過去。完事。
val action =
 DemoFragment01Directions.actionDemoFragment01ToDemoFragment022(User("tr", 19))
findNavController().navigate(action)
           
使用的時候利用DemoFragment02Args就可以了。
arguments?.let {
 val user = DemoFragment02Args.fromBundle(it).user
 Log.e(TAG, "${user.username} ${user.age}" )
}
           

但是好像還是有點複雜

那試試kotlin的委托。這樣簡單多了吧,而且還確定了類型安全。

val args:DemoFragment02Args by navArgs()
val user = args.user
Log.e(TAG, "${user.username} ${user.age}" )
           
相較之下顯然Safe Args要好不少
6.跳轉動畫

Tips:

還是閹割了Activity的跳轉動畫。

Code Place:

com/example/navigationdemo/ui/fragment/DemoFragment01.kt,

com/example/navigationdemo/ui/fragment/DemoFragment02.kt

參考自

普通跳轉動畫

很多時候跳轉需要和動畫結合,在沒有Navigation前,跳轉的實作需要加上一些代碼,但有了Navigation之後可以在Navigation Editor裡面直接加入。簡單不少。

首先點選Action,動畫是添加到Action裡面的

Jetpack Room,Navigation,ViewModelJetpack
然後添加了幾個系統自帶的動畫。
Jetpack Room,Navigation,ViewModelJetpack
效果圖(GIF)
Jetpack Room,Navigation,ViewModelJetpack
分析一下XML中參數的意義
  • app:enterAnim 入場動畫(可以在這裡引入入場動畫的XML資源)。A到B的一個跳轉A退場,B入場,如果對這個action使用enterAnim的話配置的就是B入場的動畫。
  • app:exitAnim 退場動畫。同上
  • app:popEnterAnim 也就是彈棧時候的入場動畫。之前舉了A到B跳轉的例子,現在在此基礎上在B按下傳回鍵,在Navigation回退棧中B被彈出A出現。這可以了解為B到A,其中A入場,B退場。在這個例子中添加popEnterAnim會對A添加一個動畫效果。
  • app:popExitAnim 也就是彈棧時候的退場動畫。同上。

共享元素動畫

這玩意不太好解釋,還是上圖

效果圖(GIF)

Jetpack Room,Navigation,ViewModelJetpack

共享元素動畫也就是,跳轉的起點和終點存在共同的元件,上圖中的共享元素是機器人頭像。

如何加入共享元素動畫?

首先建立一個Transition資源

<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
    <autoTransition/>
</transitionSet>
           

這個比較通用就用這個了,還有其他的Transition資源

這裡就不做多的闡述。有興趣的可以下去自行看文檔。

Jetpack Room,Navigation,ViewModelJetpack
然後在跳轉的目的地的onCreate方法中設定sharedElementEnterTransition
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = TransitionInflater.from(requireContext())
        .inflateTransition(R.transition.demo_transition)
}
           
最後在需要跳轉的地方建立Navigator.Extras并傳入到navigate中去。
Navigator.Extrasdemo01_jump_button.setOnClickListener {

    //建立Navigator.Extras
    val imageTransaction = Pair<View,String>(imageView,"demoImage")
    val extras = FragmentNavigatorExtras(imageTransaction)
    val action =
        DemoFragment01Directions.actionDemoFragment01ToDemoFragment022(User("tr", 19))
    findNavController().navigate(action,extras)
}
           

最最後。

不建議将NavGraph的action動畫和共享元素動畫一起使用(不是我說的,Google說的。

Jetpack Room,Navigation,ViewModelJetpack
也即是這兩個擇一即可。
Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack
7.DeepLink

參考自

在 Android 中,深層連結是指将使用者直接轉到應用内特定目的地的連結。

借助 Navigation 元件,您可以建立兩種不同類型的深層連結:顯式深層連結和隐式深層連結。

Tip:

Code Place :

com/example/navigationdemo/ui/fragment/DemoFragment01.kt

  • 顯式的深層連結

    顯式深層連結是深層連結的一個執行個體,該執行個體使用

    PendingIntent

    将使用者轉到應用内的特定位置。例如,您可以在通知或應用微件中顯示顯式深層連結。

    比如在Fragment中發送一條Notification,Notifaction承載一個由Navigation建立的PendingIntent

    deep_link_button.setOnClickListener {
                val manager = NotificationManagerCompat.from(requireContext())
                manager.notify(notificationId++,createNotification())
        
            }
        
    //建立Notification
        private fun createNotification(): Notification {
            val notificationName = requireActivity().packageName
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(
                    notificationName, "DeepLinkChanner",
                    NotificationManager.IMPORTANCE_DEFAULT
                )
        
                val notificationManager =
                    requireActivity().getSystemService(NotificationManager::class.java)
                notificationManager.createNotificationChannel(channel)
        
            }
            return NotificationCompat.Builder(requireContext(), notificationName)
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle("這是DeepLink Demo")
                .setContentText("DeepLink")
                .setContentIntent(getPendingIntent())
                .setAutoCancel(true)
                .build()
        }
    
    
               
//建立一個PendingIntent
  private fun getPendingIntent(): PendingIntent {
      return NavDeepLinkBuilder(requireActivity())
          .setGraph(R.navigation.nav_demo)
          //.setComponentName感覺很雞肋,又沒太懂他是幹啥的。
          //.setComponentName(DeepLinkActivity::class.java)
          .setDestination(R.id.deepLinkActivity)
          .createPendingIntent()
  }
           

Activity中進行DeepLink與此相似

代碼在com/example/navigationdemo/MainActivity.kt中不做過多解釋。

總的來說DeepLink并沒甚過人之處,它隻是提供了PendingIntent。實際DeepLink的使用也就隻有這點代碼

NavDeepLinkBuilder(requireActivity())
    .setGraph(R.navigation.nav_demo)
    .setDestination(R.id.deepLinkActivity)
    .createPendingIntent()
           
對了deepLink還可以這樣建立PendingIntent,跟上面的代碼時等價的(通過NavController)
findNavController()
    .createDeepLink()
    .setDestination(R.id.deepLinkActivity)
    .createPendingIntent()
           
  • 隐式的深層連結

    我們在學習startActivity的時候學了顯式啟動和隐式啟動,DeepLink也即是類似的。

    我們隻需要配置兩步即可完成。

    在對應的連結位置配置好deeplink

    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack
    然後就是在對應的activity的manifest檔案中加一個nav-graph标簽(不是必須要配置到MainActivity中隻是我的NavGraph在MainActivivity中是以這樣配置)
    Jetpack Room,Navigation,ViewModelJetpack

    然後就配置完成了。

    就可以通過通過這個URI指向這個app了。

    注意不能不能直接在遊覽器中搜尋這個URI這樣是無法跳轉的。

    在浏覽器中會是這樣的

    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack
    為了測試這個跳轉我網上抄了一段HTML代碼
    <!DOCTYPE html>
    <!DOCTYPE html>
    <html>
        
    <head>
        <title>跳轉測試</title>
        <meta http-equiv="content-type" content="text/html">
    </head>
        
    <body>
    <a href="http://zhiqiangtu.com/1">點選跳轉到app</a>
    </body>
        
    </html>
               
    傳入到手機裡面,通過浏覽器打開。
    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack
    DeepLink講的有些次,有興趣可以在文檔上查查。
8.Navigation UI

Navigation 元件包含

NavigationUI

類。此類包含多種靜态方法,可幫助您使用頂部應用欄、抽屜式導航欄和底部導航欄來管理導航。

頂部導航欄

參考自

Jetpack Room,Navigation,ViewModelJetpack
利用

NavigationUI

包含的方法,您可以在使用者浏覽應用的過程中自動更新頂部應用欄中的内容。例如,

NavigationUI

可使用導航圖中的目的地标簽及時更新頂部應用欄的标題。
<navigation>
    <fragment ...
              android:label="Page title">
      ...
    </fragment>
</navigation>
           
總結一句話就是頂部導航欄上的TextView的text屬性是NavGraph中fragment的label标簽的值。

NavigationUI

支援以下頂部應用欄類型:
  • Toolbar

  • CollapsingToolbarLayout

  • ActionBar

Tip:

Code Place: com/example/navigationdemo/MainActivity.kt

Navigation如何管理頂部應用欄呢?通過

AppBarConfiguration

導航按鈕的行為會根據使用者是否位于頂層目的地而變化。

頂層目的地是一組存在層次關系的目的地中的根級或最進階目的地。頂層目的地不會在頂部應用欄中顯示“向上”按鈕,因為不存在更高等級的目的地。預設情況下,應用的起始目的地是唯一的頂層目的地。

當使用者位于頂層目的地時,如果目的地使用了

DrawerLayout

,導航按鈕會變為抽屜式導航欄圖示
Jetpack Room,Navigation,ViewModelJetpack
。如果目的地沒有使用

DrawerLayout

,導航按鈕處于隐藏狀态。當使用者位于任何其他目的地時,導航按鈕會顯示為向上按鈕
Jetpack Room,Navigation,ViewModelJetpack
。在配置導航按鈕時,如需将起始目的地用作唯一頂層目的地,請建立

AppBarConfiguration

對象并傳入相應的導航圖,如下所示

在某些情況下,您可能需要定義多個頂層目的地,而不是使用預設的起始目的地。這種情況的一種常見用例是

BottomNavigationView

,在此場景中,同級螢幕可能彼此之間并不存在層次關系,并且可能各自有一組相關的目的地。對于這樣的情況,您可以改為将一組目的地 ID 傳遞給構造函數,如下所示:

開始敲代碼了

theme改成NoActionBar XML中寫入Toolbar

val appBarConfiguration = AppBarConfiguration(navController.graph)
toolbar.setupWithNavController(navController,appBarConfiguration)
           

Navigation Menu

參考自

NavigationUI

提供了對Menu驅動跳轉的支援,

NavigationUI

包含一個方法

onNavDestinationSelected()

。它将MenuItem和Destination進行關聯。如果

Menu

的Id與Destination的Id是一緻的那麼NavController就會直接幫我們導航到Destination去。

上代碼

Code Place:

com/example/navigationdemo/MainActivity.kt

//建立Menu
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.main_menu,menu)
    return true
}

 //利用Navigation對MenuItem進行導航
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
    }
//如果你是用的Toolbar記得調用 setSupportActionBar(toolbar) 否者,Menu是顯示不出來的。
           

Navigation Drawer

參考自

抽屜式導航欄是顯示應用主導航菜單的界面面闆。當使用者觸摸應用欄中的抽屜式導航欄圖示
Jetpack Room,Navigation,ViewModelJetpack
或使用者從螢幕的左邊緣滑動手指時,就會顯示抽屜式導航欄。
  • 抽屜式導航欄圖示會顯示在使用

    DrawerLayout

    的所有頂層目的地上。
  • 如需添加抽屜式導航欄,請先聲明

    DrawerLayout

    為根視圖。在

    DrawerLayout

    内,為主界面内容以及包含抽屜式導航欄内容的其他視圖添加布局。
例如,以下布局使用含有兩個子視圖的

DrawerLayout

:包含主内容的

NavHostFragment

和适用于抽屜式導航欄内容的

NavigationView

<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <!-- Layout to contain contents of main body of screen (drawer will slide over this) -->
    <androidx.fragment.app.FragmentContainerView
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:id="@+id/nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <!-- Container for contents of drawer - use NavigationView to make configuration easier -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true" />

</androidx.drawerlayout.widget.DrawerLayout>
           

也即是說如果使用DrawerLayout抽屜視圖,那麼根節點必須是DrawerLayout,DrawerLayout内包含兩個View一個是主界面,一個是側滑菜單。由于需要将側滑菜單和Navigation關聯,是以就使用了NavigationView。

但是這樣的NavigationView貌似還是一個空白什麼也沒有,如何将抽屜菜單的内容加入到NavigationView中呢?

<com.google.android.material.navigation.NavigationView
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:id="@+id/navigation_view"
    android:layout_gravity="start"
    />
           

其實很簡單,引入app:menu的标簽,将menu資源引入即可。

注意menu的id和destination的id必須一緻否者無法跳轉。

先建立一個menu資源

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/drawerFragment02"
        android:title="drawerFragment02"
        android:icon="@drawable/ic_baseline_looks_5_24"
        android:orderInCategory="2"/>
    <item
        android:id="@+id/drawerFragment01"
        android:title="drawerFragment01"
        android:icon="@drawable/ic_baseline_looks_4_24"
        android:orderInCategory="1"/>
    <item
        android:id="@+id/drawerFragment03"
        android:title="drawerFragment03"
        android:icon="@drawable/ic_baseline_looks_6_24"
        android:orderInCategory="3"/>
</menu>
           

然後再建立幾個Fragment加入到NavGraph中

最後就是一點點配置代碼

最最後還有将drawerLayout傳入AppBarConfiguration的構造函數中(不然左上角Toolbar是沒有這個圖示的

Jetpack Room,Navigation,ViewModelJetpack
inline fun AppBarConfiguration(
    navGraph: NavGraph,
    drawerLayout: Openable? = null,
    noinline fallbackOnNavigateUpListener: () -> Boolean = { false }
)
           

BottomNavigation

參考自

NavigationUI

也可以處理底部導航。當使用者選擇某個菜單項時,

NavController

會調用

onNavDestinationSelected()

并自動更新底部導航欄中的所選項目。

這個嘛還是那麼簡單。

先建立一個Menu

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:title="首頁"
        android:id="@+id/demoFragment01"
        android:orderInCategory="1"
        android:icon="@drawable/ic_baseline_home_24"/>
    <item android:title="第二頁"
        android:id="@+id/secondPageFragment"
        android:orderInCategory="2"
        android:icon="@drawable/ic_baseline_looks_two_24"/>
    <item android:title="第三頁"
        android:id="@+id/thirdPageFragment"
        android:orderInCategory="3"
        android:icon="@drawable/ic_baseline_looks_3_24"/>
</menu>
           

然後添加一點配置

效果圖(GIF)

Jetpack Room,Navigation,ViewModelJetpack
是不是有點奇怪。按道理 “第二頁”,“第三頁”的Fragment是和"首頁"一樣的頂層視圖。也就是說它是不可以傳回的,然鵝Toolbar确顯示了回退按鈕,這很奇怪。是以得将“第二頁“,”第三頁”加入到AppbarConfiguration的頂層視圖的集合中。也就稍微改改AppbarConfiguration。
Jetpack Room,Navigation,ViewModelJetpack
//之前
AppBarConfiguration(navGraph = navController.graph
    ,drawerLayout = drawer_layout
    ,fallbackOnNavigateUpListener = ::onSupportNavigateUp)
//修改後
AppBarConfiguration(setOf(R.id.demoFragment01,R.id.secondPageFragment,R.id.thirdPageFragment)
    ,drawer_layout
    ,::onSupportNavigateUp)
           
最終效果圖
Jetpack Room,Navigation,ViewModelJetpack

雖然已近寫了這麼多了,但是還有很多都沒講。比如Navigation的DSL,Navigation傳回棧,Navigation進行子產品間的導航…

Room

1.什麼是Room

Room 在 SQLite 上提供了一個抽象層,以便在充分利用 SQLite 的強大功能的同時,能夠流暢地通路資料庫。

2.Room一覽

Room 包含 3 個主要元件:
  • DataBase:包含資料庫持有者,并作為應用已保留的持久關系型資料的底層連接配接的主要接入點。

    使用

    @Database

    注釋的類應滿足以下條件:
    • 是擴充

      RoomDatabase

      的抽象類。
    • 在注釋中添加與資料庫關聯的實體清單。
    • 包含具有 0 個參數且傳回使用

      @Dao

      注釋的類的抽象方法。
    在運作時,您可以通過調用

    Room.databaseBuilder()

    Room.inMemoryDatabaseBuilder()

    擷取

    Database

    的執行個體。
  • Entity:表示資料庫中的表。
  • DAO:包含用于通路資料庫的方法。
關于與資料庫的通路。
Jetpack Room,Navigation,ViewModelJetpack
  • 應用使用 Room 資料庫來擷取與該資料庫關聯的資料通路對象 (DAO)。
  • 然後,應用使用每個 DAO 從資料庫中擷取實體,然後再将對這些實體的所有更改儲存回資料庫中。
  • 最後,應用使用實體來擷取和設定與資料庫中的表列相對應的值。
也就是說Room提供了DAO (Data Access Objects)作為App和DataBase的中間人。

3.Room的基本使用

參考自

Code Place:

com/example/roomdemo/db,

com/example/roomdemo/MainActivity.kt

  • 添加依賴
    dependencies {
        def room_version = "2.3.0"
    	//運作時的依賴
        implementation("androidx.room:room-runtime:$room_version")
        //注解處理器
        annotationProcessor "androidx.room:room-compiler:$room_version"
        // To use Kotlin annotation processing tool (kapt)
     	//這個也是注解處理器隻不過時kotlin-kapt
      	kapt("androidx.room:room-compiler:$room_version")
      	// To use Kotlin Symbolic Processing (KSP) 類似于kapt速度快一些,不過暫時處于測試版
      	ksp("androidx.room:room-compiler:$room_version")
    
      	// optional - Kotlin Extensions and Coroutines support for Room
      	implementation("androidx.room:room-ktx:$room_version")
    
      	// optional - RxJava2 support for Room
      	implementation "androidx.room:room-rxjava2:$room_version"
    
      	// optional - RxJava3 support for Room
      	implementation "androidx.room:room-rxjava3:$room_version"
    
      	// optional - Guava support for Room, including Optional and ListenableFuture
      	implementation "androidx.room:room-guava:$room_version"
    
      	// optional - Test helpers
      	testImplementation("androidx.room:room-testing:$room_version")
    }
               
    這裡我就使用了兩個必要的一個是運作時的一個是注解處理器。
    //如果使用kapt一定要加上,kotlin的注解處理插件都沒kapt個錘錘。
      id 'kotlin-kapt'
    
      //room
      def room_version = "2.3.0"
      implementation("androidx.room:room-runtime:$room_version")
      // To use Kotlin annotation processing tool (kapt)
      kapt("androidx.room:room-compiler:$room_version")
               
  • 建立實體類Entity
    @Entity(tableName = "user_table")
      data class User(
          @PrimaryKey(autoGenerate = true) val uid: Int,
          @ColumnInfo(name = "first_name") val firstName: String?,
          @ColumnInfo(name = "last_name") val lastName: String?
      )
               

    其中PrimaryKey是主鍵,我們可以通過這個主鍵直接從資料庫中查詢到該資料。一個表單中必須要有主鍵(沒有為什麼。

    其實一個Entity表單其實就相當于一個Excel表格,比如上面的user_table就可以這樣寫

    Jetpack Room,Navigation,ViewModelJetpack
    是以uId ,firstName,lastName每個變量都占據一列。ColumnInfo就是設定每列的屬性。
  • 建立Dao
    @Dao
    interface UserDao {
        @Query("SELECT * FROM user_table")
        fun getAll(): List<User>
    
        @Query("SELECT * FROM user_table WHERE uid IN (:userIds)")
        fun loadAllByIds(userIds: IntArray): List<User>
    
        @Query("SELECT * FROM user_table WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
        fun findByName(first: String, last: String): User
    
        @Insert
        fun insertAll(vararg users: User)
    
        @Delete
        fun delete(user: User)
    }
               
    這個大家因該都能懂就不解釋了。
  • 建立資料庫DataBase
    @Database(entities = arrayOf(User::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
        //單例。
        companion object{
            var instance:AppDatabase? = null
        
            @Synchronized
            fun getInstance(applicationContext: Context):AppDatabase {
                instance?.let {
                    return it
                }
                return Room.databaseBuilder(applicationContext,AppDatabase::class.java,
                    APP_DATABASE_NAME).build().apply {
                        instance = this
                }
            }
        }
        
    }
               

    有一點需要注意這個Database是抽象類。除此之外我們在建立過程中有兩中選擇,一種是持久化的資料庫,一種是緩存資料庫。

    然後就是在activity中使用(這代碼寫的有億點爛。想必大家能懂意思。實際開發得用Google官方推薦的标準架構

    Jetpack Room,Navigation,ViewModelJetpack
    package com.example.roomdemo
    
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import androidx.lifecycle.lifecycleScope
    import com.example.roomdemo.db.AppDatabase
    import com.example.roomdemo.db.dao.UserDao
    import com.example.roomdemo.model.User
    import kotlinx.android.synthetic.main.activity_main.*
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.launch
    import kotlin.math.log
    
    private const val TAG = "MainActivity"
    class MainActivity : AppCompatActivity() {
        lateinit var userDao:UserDao
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            userDao = AppDatabase.getInstance(applicationContext).userDao()
            setContentView(R.layout.activity_main)
            setListeners()
        }
    
        private fun setListeners() {
            button_get_all.setOnClickListener {
                lifecycleScope.launch(Dispatchers.IO) {
                    val all = userDao.getAll()
                    all.forEach{
                        Log.e(TAG, "$it" )
                    }
                }
            }
    
            button_find_by_name.setOnClickListener {
                lifecycleScope.launch(Dispatchers.IO) {
                    val result = userDao.findByName("a","a")
                    Log.e(TAG, "$result" )
                }
            }
    
            button_load_all_by_ids.setOnClickListener {
                lifecycleScope.launch (Dispatchers.IO){
                    val result = userDao.loadAllByIds(intArrayOf(1,2,3,4))
                    result.forEach{
                        Log.e(TAG, "$it" )
                    }
                }
            }
    
            button_delete.setOnClickListener {
                lifecycleScope.launch(Dispatchers.IO) {
                    userDao.delete(User("a","a"))
                }
            }
    
            button_insert_all.setOnClickListener {
                lifecycleScope.launch(Dispatchers.IO){
                    userDao.insertAll(User("a","a"),
                        User("b","b"),
                        User("c","c"),
                        User("d","d"))
                }
            }
        }
    }
               
注解詳解

對于基礎的@DAO,@Database,@Entity我們已近有所了解。但是其實還存在一些比較常用的。(注意:Room的注解其實不算少,但是有很多的注解不是很正常,是以就暫時沒必要花時間去學,以下隻會對常用的注解進行較為詳細的描述,不常用的就一筆帶過。)

@Entity

@Entity
data class User(
@PrimaryKey var id: Int,
var firstName: String?,
var lastName: String?
)
           

這個大家都熟吧。

其中有一點需要注意,如果你想将某個變量添加到資料庫的表單中一定要滿足以下兩個條件中的一種

1.要麼該資料為一個public變量

2.如果該資料是一個private變量,你得提供get,set方法。

  • 主鍵PrimaryKey的使用

    每個實體類至少要有一個主鍵,主鍵可以通過對變量使用@PrimaryKey,于此同時還可以在中進行申明 @Entity(primaryKeys = arrayOf())(二選一即可)

    比如這樣

    @Entity(primaryKeys = arrayOf("firstName", "lastName"))
        data class User(
            val firstName: String?,
            val lastName: String?
        )
    
               
    有的時候我們可能懶得自己去生成主鍵,但是我們可以讓Room自動幫我們生成,使用@PrimaryKey(autoGenerate=true)即可。
  • 指定表單名稱

    Entity是一個表單,我們可以自己定義表單的名稱。隻需要這樣就行了(預設表單名稱和實體類名是一緻的)

    @Entity(tableName = "users")
        data class User (
            // ...
        )
               
  • 指定變量名稱

    這裡需要引入一個新的注解@ColumnInfo,這個注解是作用在實體類的成員屬性上的,可以指定成員屬性的一些資訊。關于指定變量名稱,與前面的指定表名稱類似

    @Entity(tableName = "users")
        data class User (
            @PrimaryKey val id: Int,
            @ColumnInfo(name = "first_name") val firstName: String?,
            @ColumnInfo(name = "last_name") val lastName: String?
        )
               
  • 忽略變量

    預設情況下,Room 會為實體中定義的每個字段建立一個列。如果某個實體中有您不想保留的字段,則可以使用 **@Ignore **為這些字段添加注釋,如以下代碼段所示:

    @Entity
        data class User(
            @PrimaryKey val id: Int,
            val firstName: String?,
            val lastName: String?,
            @Ignore val picture: Bitmap?
        )
               
    如果實體繼承了父實體的字段,則使用**@Entity**屬性的 **ignoredColumns **屬性通常會更容易:
    open class User {
            var picture: Bitmap? = null
        }
    
        @Entity(ignoredColumns = arrayOf("picture"))
        data class RemoteUser(
            @PrimaryKey val id: Int,
            val hasVpn: Boolean
        ) : User()
               
  • 支援全文搜尋

    如果您的應用需要通過全文搜尋 (FTS) 快速通路資料庫資訊,請使用虛拟表(使用 FTS3 或 FTS4為您的實體提供支援)。

    如果在2.1.0以及更高版本中Room提供了 @Fts3 或 @Fts4注解,按理用處不大。這玩意因該是用于快速查找的,一般情況下手機上的資料應該不是很多的,是以用處不大。就跳過了。

    需要了解的兄的看這裡

  • 提供對Java AutoValue的支援

    對Java的支援跟我Kotlin有啥關系。ha

    稍微說一下,AutoValue是用于Java的實體類建立的,有的時候我們在建立對象的時候在構造函數會傳入null值,這會為空指針埋下隐患,是以在初始化bean的成員變量的時候保險的方案是這樣的。

    AutoValue_User(String name, int age, String address) {
        if (name == null) {
          throw new NullPointerException("Null name");
        } else {
          this.name = name;
          this.age = age;
          if (address == null) {
            throw new NullPointerException("Null address");
          } else {
            this.address = address;
          }
        }
      }
               

    但是你有沒有發現代碼有點長,而且都是一些模闆化的判空,是以Google的幾個工程師就寫了一個小的java實體類生成工具。也就是AutoValue。在kt中無疑data class是更好的解決方案。

    附上官方的代碼

    @AutoValue
        @Entity
        public abstract class User {
            // Supported annotations must include `@CopyAnnotations`.
            @CopyAnnotations
            @PrimaryKey
            public abstract long getId();
    
            public abstract String getFirstName();
            public abstract String getLastName();
    
            // Room uses this factory method to create User objects.
            public static User create(long id, String firstName, String lastName) {
                return new AutoValue_User(id, firstName, lastName);
            }
        }
               
    其實還有很多東西都沒講,有時間可以自己下來研究。沒時間就算了,這些也差不多夠用了。/狗頭

@DAO

Jetpack Room,Navigation,ViewModelJetpack

需要注意的是DAO是抽象的東西,它可以用接口寫,其實用抽象類寫也是可以的,他的實作是Room通過注解處理器自動生成的。(隻不過通常都是用的接口寫,可能代碼稍微少一點,方法可以不寫abstract)

@Dao
abstract class UserDao {
    @Query("SELECT * FROM user_table")
    abstract fun getAll(): List<User>

    @Query("SELECT * FROM user_table WHERE uid IN (:userIds)")
    abstract fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM user_table WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
    abstract fun findByName(first: String, last: String): User

    @Insert
    abstract fun insertAll(vararg users: User)

    @Delete
    abstract fun delete(user: User)
}
           

下面的是Room未我們生成的DAO的實作類

可以在:build/generated/source/kapt/debug/…下查找代碼

Jetpack Room,Navigation,ViewModelJetpack

我們知道資料庫的操作分為4種:

增,删,改,查

分别對應DAO的注解**@Insert,@Delete,@Update,@Query**。

其中增,删,改封裝的比較好,我們可以不需要寫任何的sql語句。

但是查就不一樣了,因為查詢的方法是多樣的,你可以給出一個範圍查詢,也可能隻是一個确切的值進行查詢,這個無法很好的封裝。沒辦法,隻好和sql打交道了。你也能從下面的代碼中發現。(于此同時@Query注解的能力是非常強的它能完成查詢,但是其他的增,删,改其實也能。但這需要sql的基礎了。)

  • DAO注解的使用

    @Insert标注的方法内傳入的參數必須是@Entity标注的實體類。(除此之外别忘了還得在Database中聲明)

    • @Insert
      @Dao
       interface MyDao {
           @Insert(onConflict = OnConflictStrategy.REPLACE)
           fun insertUsers(vararg users: User)
      
           @Insert
           fun insertBothUsers(user1: User, user2: User)
      
           @Insert
           fun insertUsersAndFriends(user: User, friends: List<User>)
       }
                 

      上述方法了解就夠了。通常情況下我們很少整那麼多花樣。一般要麼插入一個實體類,要麼插入一個集合。

      @Insert标記的注解其實是可以有傳回值的

      Jetpack Room,Navigation,ViewModelJetpack

      如果插入的參數是一個實體類放回值要麼沒有,要麼就是Long,這個long的含義是SQL中的rawid

      而SQL裡面的rawid好像是INTEGER類型的PrimaryKey(因為Primarykey可以是SQL的TEXT類也就是String類)

      Jetpack Room,Navigation,ViewModelJetpack

      如果@Insert方法傳入的是一個集合,那麼傳回的值可以是List,這List也就是rawid的集合。

      @Insert注解裡面有兩個值一個是 entity ,一個是onConflict。

      • entity
      先看看大概長什麼樣吧
      @Entity
       public class Playlist {
         @PrimaryKey(autoGenerate = true)
         long playlistId;
      
         String name;
         @Nullable
         String description
      
         @ColumnInfo(defaultValue = "normal")
         String category;
         @ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
         String createdTime;
         @ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
         String lastModifiedTime;
       }
      
       public class NameAndDescription {
         String name;
         String description
       }
      
       @Dao
       public interface PlaylistDao {
         @Insert(entity = Playlist.class)
         public void insertNewPlaylist(NameAndDescription nameDescription);
       }
                 

      我們可以看出Playlist有3個變量是預設生成的,一個變量是primaryKey并設定了自動生成,也就是說如果我們需要插入一個Playlist變量到資料庫,隻需要給出name和description變量即可。

      為了友善我們插入Playlist,我們可以把name和description聲明為一個新的類,然後把新的類作為參數傳入到@Insert标記的方法中。最後聲明entity = Playlist.class這個聲明的意思是傳入的參數是Playlist的一部分。這樣實際插入的是Playlist。總感覺有點畫蛇添足。

      • onConflict

        看看長什麼樣。

        @Insert(onConflict = OnConflictStrategy.ABORT)
        abstract fun insertAll(vararg users: User):List<Long>
                   

        在了解任何處理插入沖突之前先了解什麼是插入沖突。

        我們之前講過PrimaryKey,一個Entity必須要有一個PrimaryKey。因為PrimaryKey有特殊的用處。PrimaryKey是區分不同行的重要标準。

        回歸到插入沖突,前面說了PrimaryKey是判斷不同行的标準。試想一個情景。

        如果我取消了PrimaryKey的autoGenerate,當我插入資料的時候,兩個資料PrimaryKey都是預設指定的2就像這樣。

        @Entity(tableName = "user_table")
        data class User(
            @ColumnInfo(name = "first_name") val firstName: String?,
            @ColumnInfo(name = "last_name") val lastName: String?
        ){
            @PrimaryKey(/*autoGenerate = true*/) var uid:Int = 2
            override fun toString(): String {
                return "$uid-$firstName-$lastName"
            }
        }
                   

        沒錯這樣就發生了插入沖突。

        現在我們知道了插入沖突,那你認為Room會怎麼做?答案是app Crash了

        Jetpack Room,Navigation,ViewModelJetpack

        UNIQUE constraint failed。(也就是Primarykey是唯一的)

        Room給出處理插入沖突的預設方式就是OnConflictStrategy.ABORT也即是復原,抛異常。

        除此之外還有兩種方式

        • OnConflictStrategy.IGNORE 忽略掉,就當什麼都沒發生。
        • OnConflictStrategy.REPLACE 将新的值替換掉舊的值。

          比如我先插入了 User(PrimaryKey = 1,data = “1”)

          然後又插入了User(PrimaryKey = 1,data = “2”)

          那麼Room會将User(PrimaryKey = 1,data = “2”)替換掉User(PrimaryKey = 1,data = “2”)所對應的位置。

    • @Updata

      從Updata這個單詞就能看出這個是用于更改某一行資料的,和@Insert基本上是一緻的。。連文檔的例子都是一樣的。我屬實有些懵了。

      @Update标記的方法接受一個實體對象,或者是一個實體集合。代碼如下

      @Dao
      interface UserDao {
       @Update
       fun updateUsers(vararg users: User)
      }
                 

      @Updata依然是靠的PrimaryKey比對行。

      不過多闡述了。就當是@Insert得了ha。

    • @Delete
      @Delete依然是通過PrimaryKey索引行(也就是說其他成員變量是否一緻并不重要)的,依然與@Insert有點類似,不過不一樣的是索引到了對應行是删除,而不是更新插入啥的了。方法參數也是一樣的實體類,實體集合
      @Dao
      interface UserDao {
       @Delete
       fun deleteUsers(vararg users: User)
      }
                 

      還有@Delete标記的方法的傳回值可以是Unit也可以是Int。

      Int表示成功删除的資料的個數。注解内的有個entity變量,前面其實以及講過。

      通常情況下如果使用@Delete得先通過@Query查詢比對結果,然後再删除。當然也可以直接使用@Query删除。反正@Query啥都能幹。

    • @Query

      Query的參數很簡單,就一個String,而這個String是用于與SQL互動的。可能下列的代碼看不太懂,不給知道能這麼寫就好了。

      • 簡單查詢
        @Dao
            interface MyDao {
                //查詢加載所有User
                @Query("SELECT * FROM user")
                fun loadAllUsers(): Array<User>
            }
                   
      • 傳遞參數給Query
        @Dao
            interface MyDao {
                //查詢滿足條件age大于傳入闡述minAge的所有User 
                @Query("SELECT * FROM user WHERE age > :minAge")
                fun loadAllUsersOlderThan(minAge: Int): Array<User>
            }
                   
        上面的例子隻傳入了單個參數,其實還可以傳入多個參數。
        @Dao
            interface MyDao {
                @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
                fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>
        
                @Query("SELECT * FROM user WHERE first_name LIKE :search " +
                       "OR last_name LIKE :search")
                fun findUserWithName(search: String): List<User>
            }
                   
      • 傳回列的子集
        data class NameTuple(
                @ColumnInfo(name = "first_name") val firstName: String?,
                @ColumnInfo(name = "last_name") val lastName: String?
            )
        
        //這個表明我們隻傳回所有user的first_name和last_name
         @Dao
            interface MyDao {
                @Query("SELECT first_name, last_name FROM user")
                fun loadFullName(): List<NameTuple>
            }
                   
      • 傳遞參數的集合
        @Dao
            interface MyDao {
                @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
                fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
            }
                   
      • 直接光标通路
        @Dao
            interface MyDao {
                @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
                fun loadRawUsersOlderThan(minAge: Int): Cursor
            }
            
                   
        對于Cusor我也不是很清楚,網上找了相關描述
        • Cursor 是每行的集合。
        • 使用 moveToFirst() 定位第一行。
        • 你必須知道每一列的名稱。
        • 你必須知道每一列的資料類型。
        • Cursor 是一個随機的資料源。
        • 所有的資料都是通過下标取得。
        Google不是很推薦使用Cusor,除非你認為你的需求隻有使用Cusor才能很好滿足的時候。使用前一定要三思。
      • 查詢多個表格

        以下代碼段展示了如何執行表格聯接以整合以下兩個表格的資訊:一個表格包含目前借閱圖書的使用者,另一個表格包含目前處于已被借閱狀态的圖書的資料。

        @Dao
            interface MyDao {
                @Query(
                    "SELECT * FROM book " +
                    "INNER JOIN loan ON loan.book_id = book.id " +
                    "INNER JOIN user ON user.id = loan.user_id " +
                    "WHERE user.name LIKE :userName"
                )
                fun findBooksBorrowedByNameSync(userName: String): List<Book>
            }
                   
        我也看不太懂,知道能在多個表查詢即可。有需要的可以看看這個(在此之前還是先學SQL吧)

@Database

RoomDatabase的标簽。

@Database中有5個值:

entities,views,version,exportSchema,autoMigrations

  • entities
    這個我們接觸的也算比較多的,主要用于在資料庫聲明實體。
  • views

    不是很懂,這個好像和一個視圖資料庫有些關系。

  • version
    資料庫目前的版本号
  • exportSchema
    導出schema檔案也就是Room結構檔案。
  • autoMigrations

    這個自動遷移是依靠schema檔案的結構,是以exportSchema一定得是true

    Room 2.3.0暫時用不了。

    Jetpack Room,Navigation,ViewModelJetpack
    得更新2.4.0 alpha才行
    Jetpack Room,Navigation,ViewModelJetpack

4.Room進一步探究

1.Room 資料庫的遷移

參考自

當您在應用中添加和更改功能(版本改變)時,需要修改 Room 實體類。但是,如果應用更新更改了資料庫架構,我們如何将之前版本的使用者資料儲存下來就很重要。而這也是資料庫遷移需要解決的問題。

Room是通過Migration類進行資料庫版本的遷移的,通過重寫Migration的migrate方法實作資料庫的遷移。以在運作時将資料庫遷移到合适的版本。

在此之前介紹一個東西,schema檔案。schema檔案是一個json檔案,它包含了資料庫的結構圖。當進行版本遷移後它能很好的反映版本的變化情況。

導出schema的設定預設是開着的,但是在build的時候程式并不知道schema檔案放在哪,是以我們隻需要将schema檔案的存放位址生命就好了。(如果不打算導出schema記得給在@Database中加入exportSchema = false,不加這個也可以,程式可以運作。隻不過在gradle build的過程中可能爆紅,有點礙眼。)

defaultConfig{
    ......
 	javaCompileOptions {
    	annotationProcessorOptions {
        	arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
    	}
	}   
    ......
}
           
  • 手動遷移
Room 會從一個或多個

Migration

子類運作

migrate()

方法,以在運作時将資料庫遷移到最新版本:

比如這樣

有些煩人的是手動遷移需要和sql語句打交道,不太懂,欸。

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
                "PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
		.build()
           
  • 自動遷移

    我在看文檔的時候我發現了一個很…的事情,資料庫遷移的文檔中文和英文版竟然不一樣。中文少了一個自動遷移。

    圖檔為證

    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack

    這麼說是不是之前看的文檔是不是都可能少了點東西(霧。

    淦中文文檔更新滞留了…(暗示學英文。

    Jetpack Room,Navigation,ViewModelJetpack
    Jetpack Room,Navigation,ViewModelJetpack

    這告訴我們以後看文檔還是稍微注意一下文檔更新時間。如果是前幾年更新的說不定少了點什麼。。

    不扯了。其實自動遷移還隻在測試版是以還可能出現一些變化,暫時隻有英文文檔上有相關介紹,有需要的兄弟可以去看看。由于是測試版我就不多講了。(學了萬一變更了呢 hh )

    Jetpack Room,Navigation,ViewModelJetpack

    在此稍微提一下,一般庫的版本要經曆三個階段

    alph,beta,stable(正式版)

    alph – 預覽版,bug最多,可能存在比較大的變更。

    beta – 測試版,雖然經曆了alph的階段,但是仍然存在一些bug,但是通常情況下很少出現變更了,這時候就可以去學了。

    stable – 正式版,這個基本上bug就比較少了,内容就更小幾率發生變更了。

  • 破壞性遷移

    我們知道如果資料庫版本變化但是程式又沒有找到對應的遷移政策,那麼就會抛出一個

    IllegalStateException

    。有的時候我們軟體版本變化太大了,以至于資料庫的結構發生了翻天覆地的變化,保留資料已近很困難了。那麼就可以選擇直接丢棄掉目前資料庫裡面的資料,讓資料庫版本進行更新。

    讓Room采用這種遷移方式很簡單,隻需要讓它在Build的時候加入fallbackToDestructiveMigration()即可。

    如下

    Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
            .fallbackToDestructiveMigration()
            .build()
               

    注意:這個方法是用于沒有定義遷移政策的時候調用,如果定義了就不會調用。

    如果您隻想讓 Room 在特定情況下回退到破壞性重新建立,可以使用

    fallbackToDestructiveMigration()

    的一些替代選項:
    • 如果你想在某些版本的遷移中使用破壞性遷移,可以選用

      fallbackToDestructiveMigrationFrom()

      ,此方法接受多個int參數,每個int表示進行破壞性遷移的版本值。比如某app在版本4到版本5變更巨大,采用破壞性遷移,那麼隻需往

      fallbackToDestructiveMigrationFrom()

      傳入4即可。
    • 如果隻有在高版本到低版本的時候進行破壞性遷移,那麼就可以使用這個

      fallbackToDestructiveMigrationOnDowngrade()

  • 特殊的遷移

    這種遷移是為了解決一個bug。

    在很多時候給參數加入預設值這是很常見的一個需求。但是在Room 2.2.0以前加入預設值的方式隻有一種,那就是利用sql語句遷移的過程中添加一個。

    不像在2.2.0以後可以直接使用

    @ColumnInfo(defaultValue = "...")

    看下面一個執行個體。

    如果使用者在版本1到版本2遷移過程中在資料表單中添加了一列并設定了預設值。

    //版本1下的實體類 Room版本為2.1.0
    // Song Entity, DB Version 1, Room 2.1.0
    @Entity
    data class Song(
        @PrimaryKey
        val id: Long,
        val title: String
    )
    
    //版本2下的實體類 Room版本為2.1.0
    // Song Entity, DB Version 2, Room 2.1.0
    @Entity
    data class Song(
        @PrimaryKey
        val id: Long,
        val title: String,
        val tag: String // Added in version 2.
    )
    //從版本1遷移到版本2的政策
    // Migration from 1 to 2, Room 2.1.0
    val MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            //建立了新的一列‘tag’并設定預設值為‘’
            database.execSQL(
                "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''")
        }
    }
               

    乍一看這代碼是沒有問題的。如果這樣想:這個預設值是在資料庫遷移的過程中進行設定的,但是如果不進行遷移呢?也就是說直接安裝資料庫版本号對應為2的軟體。這樣躲過了資料庫的遷移,你會發現直接安裝版本2的資料庫沒有設定預設值。而遷移的有預設值。這造成了資料庫版本2的資料庫結構不一緻。但在2.1.0版本這并不會造成什麼問題。

    但但是,如果你在這個時候将Room更新到了2.2.0以及以上并使用了@CoumnInfo設定預設值就會導緻架構驗證錯誤。(可能會直接crash,不清楚沒試過)

    是以為了讓Room更新到2.2.0時的資料庫結構一緻。可以在之前的版本2上進行一次特殊的遷移。

    遷移需要完成一下3步

    1. 使用

      @ColumnInfo

      注釋在各自的實體類中聲明列預設值。
    2. 将資料庫版本号增加 1。
    3. 定義實作了删除并重新建立政策的新版本遷移路徑,将必要的預設值添加到現有列。

    第一步是為了讓直接安裝版本3的資料庫具有預設值。

    第三步是為了保證遷移過程中将沒有預設值的資料庫轉化成有預設值的資料庫。

    第三步操作的代碼如下

    //遷移過程中先建立一個new_Song的資料表單,在建立過程中設定預設值。
    //然後将Song資料表單複制到new_Song中去。
    //最後删除Song表單将new_Song重命名為Song表單。
    
    // Migration from 2 to 3, Room 2.2.0
    val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("""
                    CREATE TABLE new_Song (
                        id INTEGER PRIMARY KEY NOT NULL,
                        name TEXT,
                        tag TEXT NOT NULL DEFAULT ''
                    )
                    """.trimIndent())
            database.execSQL("""
                    INSERT INTO new_Song (id, name, tag)
                    SELECT id, name, tag FROM Song
                    """.trimIndent())
            database.execSQL("DROP TABLE Song")
            database.execSQL("ALTER TABLE new_Song RENAME TO Song")
        }
    }
               
    除此之外還有一種遷移,隻不過和預填充資料庫有些關系,就放在了預填充資料庫哪裡去了。
2.預填充資料庫

參考自

**有時,您可能希望應用啟動時資料庫中就已經加載了一組特定的資料。**這稱為預填充資料庫。在 Room 2.2.0 及更高版本中,您可以使用 API 方法在初始化時用裝置檔案系統中預封裝的資料庫檔案中的内容預填充 Room 資料庫。

注意:緩存Room 資料庫不支援使用 createFromAsset() 或 createFromFile() 預填充資料庫。

緩存資料庫就是建立在記憶體中的資料庫,當程式退出資料庫的資源全部回收。建立方法很簡單,使用Room.inMemoryDatabaseBuilder()進行建立即可(建立方式與Room.databaseBuilder()基本上一緻)

從應用資源預填充

assets/

檔案某種意義上來說也算是一個資料庫的,這個文價夾是預設不建立的,需要我們自己建立。建立方式如下

Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack

是以預填充資料庫與他确實有億點關系。

如果你想從assets目錄下讀取檔案并預填充資料庫,那麼可以在Room.databaseBuilder()調用build()前使用 createFromAsset(),該方法接收一個String,也就是預填充的檔案在assets中的位置。

比如這樣

Room.databaseBuilder(applicationContext,AppDatabase::class.java,
                APP_DATABASE_NAME)
                .createFromAsset("database/myapp.db")
                .build()
           
注意:從某個資源預填充時,Room 會驗證資料庫,以便確定其架構與預封裝資料庫的架構相比對。在建立預封裝資料庫檔案時,您應導出資料庫的架構以作為參考。

從檔案系統預填充

如需從位于裝置檔案系統任意位置(應用的 assets/ 目錄除外)的預封裝資料庫檔案預填充 Room 資料庫,請先從 RoomDatabase.Builder 對象調用 createFromFile() 方法,然後再調用 build():
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
     .createFromFile(File("mypath"))
     .build()
           

與前一個是類似的。

根據文檔描述:預填充資料庫是通過将預填充檔案複制進自己app定義的資料庫檔案中,而不是直接使用預填充資料庫的檔案。是以是需要預填充檔案的讀取權限的。

Jetpack Room,Navigation,ViewModelJetpack
Jetpack Room,Navigation,ViewModelJetpack

處理包含預封裝資料庫的遷移

我們知道fallbackToDestructiveMigration()會直接銷毀掉所有的資料。但是在破壞性遷移的同時我們還可以加上預填充,這樣破壞性遷移以後會預設使用預填充填充資料庫。

代碼如下

這種情況下,在破壞性遷移以後會自動預填充。

// Database class definition declaring version 3.
    @Database(version = 3)
    abstract class AppDatabase : RoomDatabase() {
        ...
    }

    // Destructive migrations are enabled and a prepackaged database
    // is provided.
    Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromAsset("database/myapp.db")
        .fallbackToDestructiveMigration()
        .build()
           
但是這種情況下并不會。因為并不是破壞性遷移。
// Database class definition declaring version 3.
    @Database(version = 3)
    abstract class AppDatabase : RoomDatabase() {
        ...
    }

    // Migration path definition from version 2 to version 3.
    val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            ...
        }
    }

    // A prepackaged database is provided.
    Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromAsset("database/myapp.db")
        .addMigrations(MIGRATION_2_3)
        .build()
           
這個資料庫的遷移會經曆這樣的步驟
  • 由于沒有定義2_3的遷移方式,會啟動破壞性遷移。又由于加入了預填充資料庫,是以在破壞性遷移以後會啟用預填充。
  • 又由于加入了3_4的遷移,是以在預填充以後會加載3_4的遷移。
  • 最後由于預填充會将預填充檔案複制到app的資料庫是以預填充檔案得以保留。資料庫版本變更到4
//Tips:目前資料庫版本為2
// Database class definition declaring version 4.
    @Database(version = 4)
    abstract class AppDatabase : RoomDatabase() {
        ...
    }

    // Migration path definition from version 3 to version 4.
    val MIGRATION_3_4 = object : Migration(3, 4) {
        override fun migrate(database: SupportSQLiteDatabase) {
            ...
        }
    }

    // Destructive migrations are enabled and a prepackaged database is
    // provided.
    Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromAsset("database/myapp.db")
        .addMigrations(MIGRATION_3_4)
        .fallbackToDestructiveMigration()
        .build()
           
3.定義對象之間的關系

參考自:

Google文檔

部落格位址

由于 SQLite 是關系型資料庫,是以您可以指定各個實體之間的關系。盡管大多數對象關系映射庫都允許實體對象互相引用,但 Room 明确禁止這樣做。如需了解此決策背後的技術原因,請參閱了解 Room 為何不允許對象引用。(主要原因還是性能問題。)

建立嵌套對象

有時,我們存在一種需求就是:将某個實體或資料對象在資料庫邏輯中表示為一個緊密的整體。我們可以使用@Embedded實作。代碼如下
data class Address(
     val street: String?,
     val state: String?,
     val city: String?,
     @ColumnInfo(name = "post_code") val postCode: Int
 )

 @Entity
 data class User(
     @PrimaryKey val id: Int,
     val firstName: String?,
     @Embedded val address: Address?
 )

           
這樣

User

對象表中就包含

id

firstName

street

state

city

post_code

簡單來講就是:如果Room表單實體類和實體類之間如果存在這種嵌套的關系就得利用@Embeded,這樣Room才知道這裡存在嵌套關系,它才知道這裡需要将Address展開。否者他就認為Address隻是一個變量。

注意:嵌套字段還可以包含其他嵌套字段。

為了避免@Embeded修飾的變量重複名,提供了@Embeded提供了一個參數prefix,prefix是字首。上代碼

@Embedded(prefix = "loc_")
Coordinates coordinates;
           
這樣Coordianate變量在資料庫裡的實際名稱就變成了loc_coordinates.

定義一對一關系

Code Place

com/example/roomdemo/model/entity,

com/example/roomdemo/db

兩個實體之間的一對一關系是指這樣一種關系:父實體的每個執行個體都恰好對應于子實體的一個執行個體,反之亦然。

Jetpack Room,Navigation,ViewModelJetpack
假如我們生活在一個(悲傷的)世界,每個人隻能擁有一條狗,并且每條狗也隻能有一個主人。這就是一對一關系。為了在關系型資料庫中 表示這一關系,我們建立了兩張表,

Dog

Owner

。在 Room 中,我們建立兩個表
@Entity
data class Dog(
    @PrimaryKey val dogId: Long,
    val dogOwnerId: Long,
    val name: String,
    val cuteness: Int,
    val barkVolume: Int,
    val breed: String
)

@Entity
data class Owner(@PrimaryKey val ownerId: Long, val name: String)
           

上述隻是建立了實體了,但是還沒有建立實體關系。

而建立實體關系需要在建立一個data class代碼如下

data class DogAndOwnerOneToOne(
    @Embedded val owner: Owner,
    @Relation(
        parentColumn = "ownerId",
        entityColumn = "dogOwnerId"
    )
    val dog: Dog
)
           

我們可以看出DogAndOwnerOneToOne中有兩個實體表對象的執行個體。

并通過@Relation建立了表單與表單的關系。

在實體類中由于Dog具有dogOwnerId也即是說可以通過Dog在Sql中索引到Owner,但是Dog和Owner在對象引用的角度上來看是不存在引用關系的。我們稱Dog和Owner具有邏輯關系。這種邏輯關系就是一對一關系**,其中通過Dog可以索引到Owner故又定義Dog為子實體,Owner為**父實體。

在回歸到一對一關系的建立,

  • @Relation是作用于子實體的,也即是Dog。
  • parentColum是父實體的primaryKey對應的列的名稱。
  • entityColum是子實體中與父實體PrimaryKey相對的列的名稱。
最後我們還需要在Dao中的方法加上一個注解。

@Transaction

這個注解是為了確定資料庫操作的原子性。
@Transaction
    @Query("SELECT * FROM Owner")
    fun getDogAndOwnerOneToOne(): List<DogAndOwnerOneToOne>
           
如果利用SQL來擷取DogAndOwnerOneToOne則需要經曆以下步驟
  • SELECT * FROM Owner

    比對資料庫中所有的Owner

  • SELECT * FROM Dog

    WHERE dogOwnerId IN (ownerId1, ownerId2, …)

    将第一步搜尋的Owner的id與Dog中的dogOwnerId 進行比對

  • 最後映射成DogAndOwnerOneToOne對象傳回

定義一對多關系

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-cN2b3QWO-1629425442931)(https://gitee.com/False_Mask/jetpack-demos-pics/raw/master/PicsAndGifs/image-20210730212553271.png)]

兩個實體之間的一對多關系是指這樣一種關系:父實體的每個執行個體對應于子實體的零個或多個執行個體,但子實體的每個執行個體隻能恰好對應于父實體的一個執行個體。

也就是說一個父實體對應多個子實體。

一對多和一對一關系是類似的,主要的差别是關系的建立上。

建立新的Relation

@Entity
data class Dog(
 @PrimaryKey val dogId: Long,
 val dogOwnerId: Long,
 val name: String,
 val cuteness: Int,
 val barkVolume: Int,
 val breed: String
)

@Entity
data class Owner(@PrimaryKey val ownerId: Long, val name: String)



data class DogAndOwnerOneToMany(
 @Embedded
 val owner:Owner,

 @Relation(
     parentColumn = "ownerId",
     entityColumn = "dogOwnerId"
 )
 val dogs:List<Dog>
)
           
差别也不是很大,dog變成dogs了其餘好像都沒變化。

定義多對多關系

Jetpack Room,Navigation,ViewModelJetpack
現在假設我們生活在一個完美的世界,每個主人可以擁有多條狗,每條狗也可以有多個主人。要對此關系進行模組化,僅僅通過

Dog

表和

Owner

表是不夠的。由于一條狗可能有多個主人,是以同一個

dogId

可能需要多條資料,以比對不同的主人。但是在

Dog

表中,

dogId

是主鍵,我們不能插入多個 id 相同,主人不同的狗狗。為了解決這一問題,我們需要額外建立一個存儲

(dogId,ownerId)

的 關聯表 (也稱為交叉引用表) 。

主要差異還是關系建立上。

那不簡單。這樣?

data class OwnersWithDogs(
     @Embedded val owners: List<Owner>,
     @Relation(
          parentColumn = "ownerId",
          entityColumn = "dogOwnerId"
     )
     val dogs: List<Dog>
)
           

錯的,這樣建立沒有任何意義。

你會發現owners和dogs都是獨立的。

這樣确認從表面上看是Owners to Dogs,但是這樣的關系互相間無法引用,沒有意義。是以多對多我們不采用這樣的描述方式。

而是通過兩個單多描述。

//一個Dog多個Owner
data class DogWithOwners(
    @Embedded
    val dog:Dog,
    @Relation(
        parentColumn = "dogId",
        entityColumn = "ownerId",
        associateBy = Junction(DogOwnerCrossRef::class)
    )
    val owner:List<Owner>
)
//一個Owner多個Dogs
data class OwnerWithDogs(
    @Embedded val owner: Owner,
    @Relation(
        parentColumn = "ownerId",
        entityColumn = "dogId",
        associateBy = Junction(DogOwnerCrossRef::class)
    )
    val dogs:List<Dog>
)
           
除此之外還差一個關系表,關系表是用來存儲這兩個對象的邏輯關系的。(注意兩個一對多表内都要通過associateBy引入關系表)
@Entity(primaryKeys = ["dogId","ownerId"])
data class DogOwnerCrossRef(
    val dogId:Long,
    val ownerId:Long
)
           
最後在Dao裡面聲明兩個查詢方法
//many to many
@Transaction
@Query("select * from Owner")
fun getOwnerWithDogs():List<OwnerWithDogs>

@Transaction
@Query("select * from Dog")
fun getDogWithOwners():List<DogWithOwners>
           
如果我需要建立這樣的關系
ownerId dogId
1 2,4
2 2,3,5
3 2,3,4,5

ownerId為1的人,持有dogId為2,4兩條狗。

ownerId為…

//将以下關系表插入即可。
dogeAndOwnerDao.insertRelationMap(
    DogOwnerCrossRef(4,1),
    DogOwnerCrossRef(2,2),
    DogOwnerCrossRef(3,2),
    DogOwnerCrossRef(5,2),
    DogOwnerCrossRef(2,3),
    DogOwnerCrossRef(3,3),
    DogOwnerCrossRef(4,3),
    DogOwnerCrossRef(5,3)
)
           
然後在監聽點選後查詢
get_dog_and_owner.setOnClickListener {
    lifecycleScope.launch (Dispatchers.IO){
       
        val ownerWithDogs = dogeAndOwnerDao.getOwnerWithDogs()
        ownerWithDogs.forEach {
            Log.e(TAG, "getOwnerWithDogs $it" )
        }

        val dogWithOwners = dogeAndOwnerDao.getDogWithOwners()
        dogWithOwners.forEach{
            Log.e(TAG, "getDogWithOwners $it")
        }
    }
           

多對多與單對多綜合執行個體

比如我們在做音樂播放器的時候,通常有這樣的需求,查詢使用者的的所有歌單以及每個使用者的歌單中包含的所有歌曲。

實體類如下

@Entity
    data class User(
        @PrimaryKey val userId: Long,
        val name: String,
        val age: Int
    )

    @Entity
    data class Playlist(
        @PrimaryKey val playlistId: Long,
        val userCreatorId: Long,
        val playlistName: String
    )

    @Entity
    data class Song(
        @PrimaryKey val songId: Long,
        val songName: String,
        val artist: String
    )

    @Entity(primaryKeys = ["playlistId", "songId"])
    data class PlaylistSongCrossRef(
        val playlistId: Long,
        val songId: Long
    )
           
我們可以得知:
  • User和Playlist是一對多的關系。
  • Playlist和Song是多對多的關系。
建立User和Playlist的關系
data class PlaylistWithSongs(
        @Embedded val playlist: Playlist,
        @Relation(
             parentColumn = "playlistId",
             entityColumn = "songId",
             associateBy = @Junction(PlaylistSongCrossRef::class)
        )
        val songs: List<Song>
    )
           
建立User和Playlist的關系。
data class UserWithPlaylistsAndSongs {
        @Embedded val user: User
        @Relation(
            entity = Playlist::class,
            parentColumn = "userId",
            entityColumn = "userCreatorId"
        )
        val playlists: List<PlaylistWithSongs>
    }
           

總結

  • 上述的的方法皆是解決的Room表單中的實體對象關系建立的問題。

    我們通常想到的對象關系就是引用,但是由于引用關系會多Room資料庫造成性能問題,是以Room禁止,Room提倡使用注解的方式建立對象間的邏輯關系進而提高效率。

  • 建立關系一般有以下幾步
    • 在保持原有的實體對象不變的情況下,新建立一個用于描述實體類之間的關系的類。
    • 在通過對新建立的關系類加入@Relation的注解描述關系。(如果是多對多關系還得再建立一個交叉引用表)
    • 最後在Dao裡面添加對應的查詢語句。(别忘了@Transaction注解確定原子性)
4.對于複雜資料的處理

之前的所有操作都是對簡單的對象進行處理,比如Int,String,Long,Double,Float…這種。如果遇上複雜的對象類型(除基本資料類型和數組外的類型)Room其實是不認識的。

這就引入了另一個注解@TypeConverter

如果我們的Entity是這樣的

@Entity
data class ConverterEntity(
    val data:Date
)
           

當我們build的時候就會爆這樣的錯誤。

因為Room不知道Date是個什麼類型。它推薦我們使用@TypeConverter

Jetpack Room,Navigation,ViewModelJetpack

首先建立一個一個class,對方法加入@TypeConverter注解

由于Room隻知道基本資料類型,如果我們傳入複雜的類型也隻能通過将其轉化為基本資料類型進行存儲。

加入@TypeConverter後Room會判斷方法傳入的變量和傳回值。

比如dateToTimestamp Room存儲Date的時候就會自動調用把Date轉化為Long然後再存儲。

相似的fromTimestamp,Room會在取出過程中需要将Long轉化為Date的時候自動調用。

class Converters {
   
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}
           
光這樣還是不夠的,還得把converter轉載到Database中去。
@Database(version = 1,entities = [ConverterEntity::class])
@TypeConverters(Converters::class)
abstract class ConverterDatabase : RoomDatabase() {
    abstract fun getConverterDao():ConverterDao

    companion object{
        var instance:ConverterDatabase? = null
        @Synchronized
        fun getInstance(applicationContext:Context): ConverterDatabase {
            instance?.let {
                return it
            }
            return Room.databaseBuilder(applicationContext,ConverterDatabase::class.java,
                CONVERTER_DATA_BASE_NAME)
                .build()
        }
    }
}