前言
最近兩年,MVVM的呼聲越來越高,說實話,在經歷了MVP的臃腫,MVP的繁瑣,我有點怕了。但是這次google官方帶來的一系列為MVVM架構設計的武器—Jetpack,真的讓我驚喜到了。
也許你還沒有使用這個新的武器,那么我真的建議你去使用一下,感受下這個新武器的快準狠,感受下這個新架構的精妙解耦。
介紹
2018年谷歌I/O,Jetpack橫空出世,官方介紹如下:
Jetpack 是一套庫、工具和指南,可幫助開發者更輕松地編寫優質應用。這些組件可幫助您遵循最佳做法、讓您擺脫編寫樣板代碼的工作并簡化復雜任務,以便您將精力集中放在所需的代碼上。
一直以來,Android開發都充斥了大量的不規范的操作和重復代碼,比如生命周期的管理,開發過程的重復,項目架構的選擇等等。所以Google為了規范開發行為,就推出這套指南,旨在讓開發者們能夠更好,更快,更規范地開發出優質應用。
當然,這兩年的實踐也確實證明了Jetpack做到了它介紹的那樣,便捷,快速,優質。所以我們作為開發者還是應該早點應用到這些工具,提高自己的開發效率,也規范我們自己的開發行為。
今天給大家帶來的是Jetpack中的架構組件,這個模塊的組件可以說就是為MVVM框架服務的,每個庫也都是可以單獨使用的。
Jetpack-架構組件
先簡單說下MVVM,Model—View—ViewModel。
- Model層主要指數據,比如服務器數據,本地數據庫數據,所以網絡操作和數據庫讀取就是這一層,只保存數據。
- View層主要指UI相關,比如xml布局文件,Activity界面顯示
- ViewModel層是MVVM的核心,連接view和model,需要將model的數據展示到view上,以及view上的操作數據反映轉化到model層,所以就相當于一個雙向綁定。
所以就需要,databinding進行數據的綁定,單向或者雙向。viewmodel進行數據管理,綁定view和數據。lifecycle進行生命周期管理。LiveData進行數據的及時反饋。 迫不及待了吧,跟隨我一起看看每個庫的神奇之處。
數據綁定
數據綁定庫是一種支持庫,借助該庫,您可以使用聲明性格式(而非程序化地)將布局中的界面組件綁定到應用中的數據源。
主要指的就是數據綁定庫DataBinding,下面從六個方面具體介紹下
配置應用使用數據綁定:
android {
...
dataBinding {
enabled = true
}
}
1)布局和綁定表達式通過數據綁定,我們可以讓xml布局文件中的view與數據對象進行綁定和賦值,并且可以借助表達式語言編寫表達式來處理視圖分派的事件。舉個:
//布局 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")
}
通過@{}符號,可以在布局中使用數據對象,并且可以通過DataBindingUtil獲取賦值對象。并且@{}里面的表達式語言支持多種運算符,包括算術運算符,邏輯運算符等等。
2)可觀察的數據對象可觀察性是指一個對象將其數據變化告知其他對象的能力。通過數據綁定庫,您可以讓對象、字段或集合變為可觀察。
比如上文剛說到的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顯示數據也會跟著改變,這就是可觀察數據對象。
3)生成的綁定類
剛才我們獲取綁定布局是通過DataBindingUtil.setContentView方法生成ActivityMainBinding對象并綁定布局。那么ActivityMainBinding類是怎么生成的呢?只要你的布局用layout屬性包圍,編譯后就會自動生成綁定類,類名稱基于布局文件的名稱,它會轉換為 Pascal 大小寫形式并在末尾添加 Binding 后綴。
正常創建綁定對象是通過如下寫法:
//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)綁定適配器
適配器這里指的是布局中的屬性設置,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里面的屬性為你需要定義的屬性名稱,這里設置的是imageUrl。所以在布局中就可以使用app:imageUrl,并傳值為String類型,系統就會找到這個適配器方法并執行。
5)將布局視圖綁定到架構組件這一塊就是實際應用了,和jetpack其他組件相結合使用,形成完整的MVVM分層架構。
// 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)雙向數據綁定
剛才我們介紹的都是單向綁定,也就是布局中view綁定了數據對象,那么如何讓數據對象也對view產生綁定呢?也就是view改變的時候數據對象也能接收到訊息,形成雙向綁定。
很簡單,比如一個EditText,需求是EditText改變的時候,user對象name數據也會跟著改變,只需要把之前的"@{}"改成"@={}"
//布局 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,數據綁定到view
@JvmStatic
@BindingAdapter("app:bind_refreshing")
fun setSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout,newValue: Boolean) {
if (swipeRefreshLayout.isRefreshing != newValue)
swipeRefreshLayout.isRefreshing = newValue
}
//方法1,view改變會通知bind_refreshingChanged,并且從該方法獲取view的數據
@JvmStatic
@InverseBindingAdapter(attribute = "app:bind_refreshing",event = "app:bind_refreshingChanged")
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =swipeRefreshLayout.isRefreshing
//方法3,view如何改變來影響數據內容
@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屬性,將數據viewModel.refreshing綁定到view上,這樣數據變化,view也會跟著變化。然后view變化的時候,通過InverseBindingAdapter注釋,會調用bind_refreshingChanged事件,而bind_refreshingChanged事件告訴了我們view什么時候會進行數據的修改,在這個案例中也就是swipeRefreshLayout下滑的時候會導致數據進行改變,于是數據對象會從isSwipeRefreshLayoutRefreshing方法獲取到最新的數值,也就是從view更新過來的數據。
這里要注意的一個點是,雙向綁定要考慮到死循環問題,當View被改變,數據對象對應發生更新,同時,這個更新又回通知View層去刷新UI,然后view被改變又會導致數據對象更新,無限循環下去了。所以防止死循環的做法就是判斷view的數據狀態,當發生改變的時候才去更新view。
Lifecycles
生命周期感知型組件可執行操作來響應另一個組件(如 Activity 和 Fragment)的生命周期狀態的變化。這些組件有助于您寫出更有條理且往往更精簡的代碼,這樣的代碼更易于維護。
Lifecycles,稱為生命周期感知型組件,可以感知和響應另一個組件(如 Activity 和 Fragment)的生命周期狀態的變化。
可能有人會疑惑了,生命周期就那幾個,我為啥還要導入一個庫呢?有了庫難道就不用寫生命周期了嗎,有什么好處呢? 舉個,讓你感受下。
首先導入庫,可以根據實際項目情況導入
// 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"
//.......
現在有一個定位監聽器,需要在Activity啟動的時候開啟,銷毀的時候關閉。正常代碼如下:
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里面管理,還容易漏掉。 所以解決辦法就是實現解耦,讓需要管理生命周期的類自己管理,這樣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
}
}
很簡單吧,只要實現LifecycleObserver接口,就可以用注釋的方式執行每個生命周期要執行的方法。然后在Activity里面addObserver綁定即可。
同樣的,Lifecycle也支持自定義生命周期,只要繼承LifecycleOwner即可,然后通過markState方法設定自己類的生命周期,舉個
class BindingActivity : AppCompatActivity(), LifecycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.markState(Lifecycle.State.CREATED)
}
public override fun onStart() {
super.onStart()
lifecycleRegistry.markState(Lifecycle.State.STARTED)
}
}
LiveData
LiveData 是一種可觀察的數據存儲器類。與常規的可觀察類不同,LiveData 具有生命周期感知能力,意指它遵循其他應用組件(如 Activity、Fragment 或 Service)的生命周期。這種感知能力可確保 LiveData 僅更新處于活躍生命周期狀態的應用組件觀察者。
LiveData 是一種可觀察的數據存儲器類。 等等,這個介紹好像似曾相識?對,前面說數據綁定的時候就有一個可觀察的數據對象ObservableField。那兩者有什么區別呢?
1)LiveData 具有生命周期感知能力,可以感知到Activity等的生命周期。這樣有什么好處呢?很常見的一點就是可以減少內存泄漏和崩潰情況了呀,想想以前你的項目中針對網絡接口返回數據的時候都要判斷當前界面是否銷毀,現在LiveData就幫你解決了這個問題。
具體為什么能解決崩潰和泄漏問題呢?
- 不會發生內存泄漏 觀察者會綁定到 Lifecycle 對象,并在其關聯的生命周期遭到銷毀后進行自我清理。
- 不會因 Activity 停止而導致崩潰 如果觀察者的生命周期處于非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。
- 自動判斷生命周期并回調方法 如果觀察者的生命周期處于 STARTED 或 RESUMED狀態,則 LiveData 會認為該觀察者處于活躍狀態,就會調用onActive方法,否則,如果 LiveData 對象沒有任何活躍觀察者時,會調用 onInactive()方法。
2) LiveData更新數據更靈活,不一定是改變數據,而是調用方法(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? ->
// 監聽livedata的數據變化,如果調用了setValue或者postValue會調用該onChanged方法
//更新UI數據或者其他處理
})
}
}
這是一個股票數據對象,StockManager為股票管理器,如果該對象有活躍觀察者時,就去監聽股票市場的情況,如果沒有活躍觀察者時,就可以斷開監聽。 當監聽到股票信息變化,該股票數據對象就會通過setValue方法進行數據更新,反應到觀察者的onChanged方法。這里要注意的是setValue方法只能在主線程調用,而postValue則是在其他線程調用。 當Fragment這個觀察者生命周期發生變化時,LiveData就會移除這個觀察者,不再發送消息,所以也就避免崩潰問題。
Navigation
導航 Navigation 組件旨在用于具有一個主 Activity 和多個 Fragment 目的地的應用。主 Activity 與導航圖相關聯,且包含一個負責根據需要交換目的地的 NavHostFragment。在具有多個 Activity 目的地的應用中,每個 Activity 均擁有其自己的導航圖。
所以說白了,Navigation就是一個Fragment的管理框架。 怎么實現?創建Activity,Fragment,進行連接。
1)導入庫
def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
2)創建3個Fragment和一個Activity
3)創建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存在的跳轉事件,比如myFragment1可以跳轉myFragment2。
- 修改Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/my_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到,Activity的布局文件就是一個fragment控件,name為NavHostFragment,navGraph為剛才新建的mynavigation文件。
5)配置完了之后,就可以設置具體的跳轉邏輯了。
override fun onClick(v: View) {
//不帶參數
v.findNavController().navigate(R.id.action_blankFragment_to_blankFragment2)
//帶參數
var bundle = bundleOf("amount" to amount)
v.findNavController().navigate(R.id.confirmationAction, bundle)
}
//接收數據
tv.text = arguments?.getString("amount")
需要注意的是,跳轉這塊官方建議用Safe Args 的Gradle 插件,該插件可以生成簡單的 object 和 builder類,以便以類型安全的方式瀏覽和訪問任何關聯的參數。這里就不細說了,感興趣的可以去官網看看
Room
Room 持久性庫在 SQLite 的基礎上提供了一個抽象層,讓用戶能夠在充分利用 SQLite 的強大功能的同時,獲享更強健的數據庫訪問機制。
所以Room就是一個數據庫框架。問題來了,市面上那么多數據庫組件,比如ormLite,greendao等等,為什么google還要出一個room,有什么優勢呢?
- 性能優勢,一次數據庫操作主要包括:構造sql語句—編譯語句—傳入參數—執行操作。ORMLite主要在獲取參數屬性值的時候,是通過反射獲取的,所以速度較慢。GreenDao在構造sql語句的時候是通過代碼拼接,所以較慢。Room是通過接口方法的注解生成sql語句,也就是編譯成字節碼的時候就生成了sql語句,所以運行起來較快。
- 支持jetpack其他組件(比如LiveData,Paging)以及RxJAVA,這就好比借助了當前所在的優勢環境,就能給你帶來一些得天獨厚的優勢。當然實際使用起來也確實要方便很多,比如liveData結合,就能在數據查詢后進行自動UI更新。
既然Room這么優秀,那就用起來吧。 Room的接入主要有三大點:DataBase、Entity、Dao。分別對應數據庫,表和數據訪問。
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)建立數據庫類,聲明數據庫表成員,數據庫名稱,數據庫版本,單例等等
@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)建表,可以設置主鍵,外鍵,索引,自增等等
@Entity
data class User(@PrimaryKey(autoGenerate = true) val id: Int,
val name: String)
4)Dao,數據操作
@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>)
}
然后就可以進行數據庫操作了,很簡單吧。
Paging
分頁庫可幫助您一次加載和顯示一小塊數據。按需載入部分數據會減少網絡帶寬和系統資源的使用量。
所以Paging就是一個分頁庫,主要用于Recycleview列表展示。下面我就結合Room說說Paging的用法。 使用Paging主要注意兩個類:PagedList和PagedListAdapter。1)PagedList用于加載應用數據塊,綁定數據列表,設置數據頁等。結合上述Room的Demo我繼續寫了一個UserModel進行數據管理:
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 類,用于比較數據,進行數據更新用。
ok,數據源,adapter都設置好了,接下來就是監聽數據,刷新數據就可以了
// 監聽users數據,數據改變調用submitList方法
viewModel.users.observe(this, Observer(adapter::submitList))
對,就是這么一句,監聽PagedList,并且在它改變的時候調用PagedListAdapter的submitList方法。 這分層夠爽吧,其實這也就是paging或者說jetpack給我們項目帶來的優勢,層層解耦,adapter都不用維護list數據源了。
ViewModel
ViewModel 類旨在以注重生命周期的方式存儲和管理界面相關的數據。ViewModel 類讓數據可在發生屏幕旋轉等配置更改后繼續留存。
終于說到ViewModel了,其實之前的demo都用了好多遍了,ViewModel主要是從界面控制器邏輯中分離出視圖數據,為什么要這么做呢?主要為了解決兩大問題:
- 以前Activity中如果被系統銷毀或者需要重新創建的時候,頁面臨時性數據都會丟失,需要通過onSaveInstanceState() 方法保存,onCreate方法中讀取。而且數據量一大就更加不方便了。
- 在Activity中,難免有些異步調用,所以就會容易導致界面銷毀時候,這些調用還存在。那就會發生內存泄漏或者直接崩潰。
所以ViewModel誕生了,還是解耦,我把數據單獨拿出來管理,還加上生命周期,那不就可以解決這些問題了嗎。而且當所有者 Activity 完全銷毀之后,ViewModel會調用其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的實例,然后進行數據監聽等操作。等等,你能發現什么不? 對了,數據通信。不同的 Fragment 可以使用其父Activity共享ViewModel 來進行數據的通信,厲害吧。還有很多其他的用法,去項目中慢慢發現吧!
WorkManager
使用 WorkManager API 可以輕松地調度即使在應用退出或設備重啟時仍應運行的可延遲異步任務。
聽聽這個介紹就很神奇了,應用退出和設備重啟都能自動運行?通過廣播?那數據又是怎么保存的呢?聽說還可以執行周期性異步任務,順序鏈式調用哦!接下來一一解密
一般這個API應用到什么場景呢?想想,可靠運行,還可以周期異步。 對了,發送日志??梢酝ㄟ^WorkManager設定周期任務,每天執行一次發送日志的任務。而且能夠保證你的任務可靠運行,一定可以上傳到,當然也是支持監聽任務結果等。:
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) 新建任務類,繼承Worker,重寫doWork方法,返回任務結果。
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)最后就是設定約束(是否需要網絡,是否支持低電量,是否支持充電執行,延遲等等),執行任務(單次任務或者循環周期任務)
//設定約束
val constraints =
Constraints.Builder()
//網絡鏈接的時候使用
.setRequiredNetworkType(NetworkType.CONNECTED)
//是否在設備空閑的時候執行
.setRequiresDeviceIdle(false)
//是否在低電量的時候執行
.setRequiresBatteryNotLow(true)
//是否在內存不足的時候執行
.setRequiresStorageNotLow(true)
//是否時充電的時候執行
.setRequiresCharging(true)
//延遲執行
.setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
.build()
//設定循環任務
val uploadRequest =
PeriodicWorkRequestBuilder<UploadLogcatWork>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.addTag("uploadTag")
.build()
//執行
WorkManager.getInstance(applicationContext).enqueue(uploadRequest)
//監聽執行結果
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)另外還支持任務取消,任務鏈式順序調用等
//取消
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()
}
總結
Jetpack-架構組件講完了,大家吃應該吃飽了吧哈哈。希望這篇文章能讓不怎么熟悉Jetpack的同學熟悉熟悉。
當然,這還遠遠不夠,在我看來,本文更像是一個科普文,只是告訴了大家Jetpack-架構組件有哪些成員,有什么用處。實際項目中,我們還需要建立MVVM的思想,深刻了解每個組件的設計意義,靈活運用組件。最后希望大家都能通過jetpack構建高質量,簡易并優質的項目架構,從而解放生產力,成為效率達人。
當然了,本文所列出的知識點還不完全,要比較系統的學習,我這里可以分享我一份Jetpack的資料。
更重要的是,還有大佬收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
這些都是我現在閑暇還會反復翻閱的精品資料。里面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效的幫助大家掌握知識、理解原理。
當然你也可以拿去查漏補缺,提升自身的競爭力。
如果你有需要,可以私信或者評論獲取
喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支持一下唄~