Jetpack

jetpack是Google推出的新的架构组件库,分为基础,架构,行为,界面4个部分。

ViewModel

$\texttt{ViewModel}$可以帮助Activity分担一部分工作,这样就不用让Activity里面的代码看着非常复杂,减少逻辑。并且$\texttt{ViewModel}$的生命周期是要比Activity长的,当手机屏幕旋转时,Activity会经历onPause(),onStop(),onDestroy(),onCreate(),onStart(),onResume()的过程,这中途我们创建的数据就会一同丢失,而$\texttt{ViewModel}$则很好的避免了这一点,他能在旋转时数据不丢失,而只有Activity退出时才会一起退出。

先在build.gradle里面添加:

1
2
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"

假设我们要实现一个计数器,按一下加一。activity_main.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/tv_main_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btn_main_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Plus One"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_main_info" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后创建一个MainViewModel类继承ViewModel,里面用于存储我们计数的值:

1
2
3
class MainViewModel : ViewModel() {
var counter = 0
}

然后MainActivity.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel
lateinit var mBtnPlus: Button
lateinit var mTvText: TextView
lateinit var mBtnClear: Button

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycle.addObserver(MyObserve())

mTvText = findViewById(R.id.tv_main_info)
mBtnPlus = findViewById(R.id.btn_main_plus)

ViewModel = ViewModelProvider(this).get(MainViewModel::class.java)

mBtnPlus.setOnClickListener {
viewModel.counter++
update()
}
update()
}

private fun update() {
mTvText.text = viewModel.counter.toString()
}
}

我们在创建ViewModel时,为什么不直接写成viewmodel = MainViewModel()呢?因为如果这样,每次旋转屏幕时都会调用onCreate()方法,则ViewModel就跟着被重新创建了,不能达到预期效果,所以我们需要通过ViewModelProvider来创建实例:

ViewModelProvider(Activity/Fragment实例).get(...ViewModel::class.java)

这样我们就可以实现基本的计数,按一次加一,同时当我们旋转屏幕时数据也不会丢失。

向ViewModel传递参数

假如我们想对这个计数器实现保存或者自定义初始数功能,又该如何向ViewModel传递参数呢?我们需要借助ViewModelProvider.Factory接口,重写create接口:

MainViewModel.kt:

1
2
3
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = countReserved
}

新建一个MainViewModelFactory类:

1
2
3
4
5
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}

然后我们加上一个clear按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/tv_main_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btn_main_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Plus One"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_main_info" />

<Button
android:id="@+id/btn_main_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="clear"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_main_plus" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后修改MainActivity.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel
lateinit var mBtnPlus: Button
lateinit var mTvText: TextView
lateinit var mBtnClear: Button

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycle.addObserver(MyObserve())
mTvText = findViewById(R.id.tv_main_info)
mBtnPlus = findViewById(R.id.btn_main_plus)
mBtnClear = findViewById(R.id.btn_main_clear)

ViewModel = ViewModelProvider(this, MainViewModelFactory(1)).get(MainViewModel::class.java)

mBtnPlus.setOnClickListener {
viewModel.counter++
update()
}
mBtnClear.setOnClickListener {
viewModel.counter = 0
update()
}
update()
}

private fun update() {
mTvText.text = viewModel.counter.toString()
}
}

Lifecycles

我们在一个activity页面中能很好的感知他的生命周期,假如我们想要在其他类中同样去感知生命周期呢,当然可以构建一个方法,在activity的生命周期中去调用这个方法去告诉他我们现在是什么生命周期。而为了减少activity的逻辑,我们就引入了lifecycle。

新建一个MyObserver类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyObserver : DefaultLifecycleObserver{
override fun onCreate(owner: LifecycleOwner) {
Log.d("zzx","(OnCreate:)-->>");
}

override fun onStart(owner: LifecycleOwner) {
Log.d("zzx","(OnStart:)-->>");
}

override fun onResume(owner: LifecycleOwner) {
Log.d("zzx","(OnResume:)-->>");
}

override fun onPause(owner: LifecycleOwner) {
Log.d("zzx","(OnPause:)-->>");
}

override fun onStop(owner: LifecycleOwner) {
Log.d("zzx","(onStop:)-->>");
}

override fun onDestroy(owner: LifecycleOwner) {
Log.d("zzx","(onDestroy:)-->>");
}
}

通过继承DefaultLifecycleObserver来实现对Activity生命周期的感知。

然后在MainActivity.kt添加这行代码:

1
lifecycle.addObserver(MyObserver())

就实现了对activity的生命周期的监听。其中lifecycle是通过getLifecycle()得到的一个Lifecycle对象。

LiveData

LiveData能在数据改变时响应并能主动提供给观察者,能和ViewModel搭配使用。我们之前的加一方法在单线程时肯定能用,但是如果我们在MainViewModel里面去开启了一些新的线程,此时我们在MainActivity里面调用肯定是行不通的,所以我们就可以用LiveData去让数据主动通知观察者。

MainViewModel.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainViewModel(countReserved: Int) : ViewModel() {

val counter = MutableLiveData<Int>()

init {
counter.value = countReserved
}

fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}

fun clear() {
counter.value = 0
}
}

我们将counter变量修改成了MutableLiveData对象,并将泛型指定成$\texttt{Int}$。这是一种可变的LiveData,能通过getValue(),setValue(),postValue()三种方法进行读写数据。getValue()是获取LiveData中的数据,setValue()是给LiveData设置数据,但是只能在主线程调用。如果我们开启了新线程,则需要用postValue()设置数据。上面代码则是用的getValue()setValue()的语法糖。然后修改MainActivity.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel
lateinit var mBtnPlus: Button
lateinit var mTvText: TextView
lateinit var mBtnClear: Button

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycle.addObserver(MyObserver())

viewModel = ViewModelProvider(this, MainViewModelFactory(100)).get(MainViewModel::class.java)

mTvText = findViewById(R.id.tv_main_info)
mBtnPlus = findViewById(R.id.btn_main_plus)
mBtnClear = findViewById(R.id.btn_main_clear)

mBtnPlus.setOnClickListener {
viewModel.plusOne()
}
mBtnClear.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this) {count ->
mTvText.text = count.toString()
}
}
}

这里我们通过调用counter这个对象的observe方法来观察数据变化,第一个参数是LifecycleOwner对象,由于Activity和fragment本身继承了lifecycleowner,所以可以直接传this进去,第二个参数就是Observer接口,当counter包含的数据变化时,会直接回调到这里。注意,这里其实并不能写成函数API的形式,因为this本质上是LifecycleOwner也是个单抽象方法接口,所以这里要么两种都写成api函数形式,但这里已经用了this了所以不行。但implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"这个库加入了对observe()的语法扩展,我们就可以改成上面这种格式了。

然后现在的counter我们是暴露在外面的,破坏了封装性,我们可以设置一个永不可变的变量暴露给外面,但我们调用他时拿到的确实内部可变的counter,这样外部只能拿而不能修改了。MainViewModel.kt代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainViewModel(countReserved: Int) : ViewModel() {

val counter: LiveData<Int>
get() = _counter
private val _counter = MutableLiveData<Int>()

init {
_counter.value = countReserved
}

fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}

fun clear() {
_counter.value = 0
}
}

map和switchMap

map

map()方法是将实际包含数据的liveData()对象与被观察的liveData()对象间的转换。比如我们定义一个数据类:

1
data class User(var firstName: String, var lastName: String, var age: Int)

然后再在MaiViewModel.kt中创建一个$\texttt{liveData}$对象:

1
val userLiveData = MutableLiveData<User>()

但假如我们只想关注用户的名字而不想让年龄暴露出去,就需要将这个User的$\texttt{liveData}$对象转成只带名字的$\texttt{liveData}$对象,就需要用到map()这个方法来将两个$\texttt{liveData}$对象进行转换。

google开发者说明中,lifecycle2.5是用的Transformations.map(liveData) {...}这种形式,而在2.6往后就改成了liveData.map {...}。然后我们进行转换:

1
2
3
4
private val userLiveData = MutableLiveData<User>()
val username: LiveData<String> = userLiveData.map { user ->
"${user.firstName} ${user.lastName}"
}

switchMap

switchMap()使用方法就比较固定了,适用于对不在MainViewModel类里面创建的$\texttt{liveData}$对象进行观察。比如我们创建一个单例类:

1
2
3
4
5
6
7
8
object Repository {

fun getUser(userId: String) : LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}

我们接受一个userId的参数来返回一个$\texttt{liveData}$对象。然后在MainViewModel.kt中接收:

1
2
3
fun getUser(userId: String) {
userIdLiveData.value = userId
}

但是在MainActivity.kt中直接用viewModel.getUser(userId).observe(this) {user -> }是肯定不行的,因为这样每次的返回一个新的$\texttt{liveData}$实例,但上述observe返回的却会是老的$\texttt{liveData}$实例,无法观察到数据的变化。这时候我们就可以用switchMap来观察:

1
2
3
4
5
6
7
val userIdLiveData = MutableLiveData<String>()
val users: LiveData<User> = userIdLiveData.switchMap { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}

我们创建了一个空的可变$\texttt{liveData}$对象,每次Activity中调用getUser()时仅仅只会改变userIdLiveData的值,而当这个值发生变化时,switchMap便会进行观察,然后将函数返回的值转成一个可观察的$\texttt{liveData}$对象,然后我们只需要在Activity中去观察users这个对象就好了。

假如getUser()中没有参数怎么办呢?只需要改成:

1
2
3
fun getUser() {
userIdLiveData.value = userIdLiveData.value
}

这样就可以了,因为$\texttt{liveData}$内部只需要判断是否调用setValue()getValue()方法,而不会判断是否与原数据相同。

ViewBinding

ViewBinding可以用来代替重复写findViewById,在每个视图生成时一次性加载全部控件。

首先在build.gradle中启用ViewBinding:

1
2
3
4
5
6
android {
...
viewBinding {
enabled = true
}
}

如果某一个xml不需要生成绑定类,就添加:

1
2
3
4
5
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>

且XML文件生成的绑定类类名为xml文件名转换为Pascal大小写,并加上Binding。如:activity_main.xml转为ActivityMainBinding

三个类绑定API:

1
2
3
4
5
6
// View已存在
fun <T> bind(view : View) : T

// View未存在
fun <T> inflate(inflater : LayoutInflater) : T
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T

接下来是各种场景ViewBinding的演示:

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1、实例化绑定实例
binding = ActivityMainBinding.inflate(layoutInflater)
// 2、获得对根视图的引用
val view = binding.root
// 3、让根视图称为屏幕上的活动视图
setContentView(view)
// 4、引用视图控件
binding.tvContent.text = "修改TextView文本"
}
}

Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ContentFragment: Fragment() {
private var _binding: FragmentContentBinding? = null
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentContentBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.ivLogo.visibility = View.GONE
}

override fun onDestroyView() {
super.onDestroyView()
// Fragment的存活时间比View长,务必在此方法中清除对绑定类实例的所有引用
// 否则会引发内存泄露
_binding = null
}
}

RecyclerView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class TestAdapter(list: List<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {
private var mList: List<String> = list

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 需在此初始化以获得父类容器,假设父类容器为item_test
val binding = ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvItem.text = "Adapter"
}

override fun getItemCount() = mList.size

// 传递Binding对象
class ViewHolder(binding: ItemTestBinding) : RecyclerView.ViewHolder(binding.root) {
var tvItem: TextView = binding.tvItem
}
}

Dialog

如果是继承DialogFragment写法同Fragment,如果是继承Dialog写法示例如下(PopupWindow类似)

1
2
3
4
5
6
7
8
class TestDialog(context: Context) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DialogTestBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvTitle.text = "对话框标题"
}
}

include

在使用include导入xml布局时,也可以用viewbinding

1
2
val includeBinding = binding.includeLayout
includeBinding.etInput.setText("info")

封装

如果这样写,我们每次都需要写一遍很麻烦,我们可以用泛型去封装。

Activity的封装

以上面的计时器为例,先创建一个BaseActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class BaseActivity<T: ViewBinding>: AppCompatActivity() {
val binding get() = _binding!!
var _binding: T? = null

abstract fun inflateBinding(): T

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = inflateBinding()
setContentView(binding.root)
}

override fun onDestroy() {
super.onDestroy()
_binding = null
}
}

MainActivity.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MainActivity : BaseActivity<ActivityMainBinding>() {

lateinit var viewModel: MainViewModel
lateinit var mBtnPlus: Button
lateinit var mTvText: TextView
lateinit var mBtnClear: Button

override fun inflateBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycle.addObserver(MyObserver())

viewModel = ViewModelProvider(this, MainViewModelFactory(100)).get(MainViewModel::class.java)

mTvText = findViewById(R.id.tv_main_info)
mBtnPlus = findViewById(R.id.btn_main_plus)
mBtnClear = findViewById(R.id.btn_main_clear)

binding.btnMainPlus.setOnClickListener {
viewModel.plusOne()
}
binding.btnMainClear.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this) {count ->
mTvText.text = count.toString()
}
}
}

Fragment的封装

BaseFragment.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class BaseFragment<T: ViewBinding> : Fragment() {
var _binding: T? = null
val binding get() = _binding!!

abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): T

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = inflateBinding(inflater, container)
return binding.root
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null //一定要置空
}
}

BlankFragment.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BlankFragment : BaseFragment<FragmentBlankBinding>() {
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentBlankBinding {
return FragmentBlankBinding.inflate(inflater,container,false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 在这里编写你的代码
binding.textView.text = "Hello, ViewBinding in Fragment!"
}
}

使用ViewBinding加载绑定视图

假设现在有个custom_view.xml需要加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : BaseActivity<ActivityMainBinding>() {

override fun inflateBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 使用 LayoutInflater 加载包含布局
val inflater = LayoutInflater.from(this)
val customViewBinding = CustomViewBinding.inflate(inflater, binding.root, true)

// 操作包含布局中的视图
customViewBinding.textView.text = "Hello from Custom View!"
}
}

补:关于inflate()最后一个参数,表示是否将这个视图添加到root中,如果为true则会立即添加,如果为false,你可以决定什么时候添加,只需要添加如下代码:

1
2
3
4
5
// 使用 LayoutInflater 加载包含布局,但不自动添加到父视图中
val customViewBinding = CustomViewBinding.inflate(layoutInflater, binding.root, false)

// 手动将新视图添加到父视图中
binding.root.addView(customViewBinding.root)