Skip to content

Jetpack

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

ViewModel

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

先在build.gradle里面添加:

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

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

xml
<?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,里面用于存储我们计数的值:

kotlin
class MainViewModel : ViewModel() {
    var counter = 0
}

然后MainActivity.kt:

kotlin
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:

kotlin
class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = countReserved
}

新建一个MainViewModelFactory类:

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

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

xml
<?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:

kotlin
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类:

kotlin
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添加这行代码:

kotlin
lifecycle.addObserver(MyObserver())

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

LiveData

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

MainViewModel.kt:

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

kotlin
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代码如下:

kotlin
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()对象间的转换。比如我们定义一个数据类:

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

然后再在MaiViewModel.kt中创建一个liveData对象:

kotlin
val userLiveData = MutableLiveData<User>()

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

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

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

switchMap

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

kotlin
object Repository {

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

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

kotlin
fun getUser(userId: String) {
    userIdLiveData.value = userId
}

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

kotlin
val userIdLiveData = MutableLiveData<String>()
val users: LiveData<User> = userIdLiveData.switchMap { userId ->
    Repository.getUser(userId)
}
fun getUser(userId: String) {
    userIdLiveData.value = userId
}

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

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

kotlin
fun getUser() {
    userIdLiveData.value = userIdLiveData.value
}

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

ViewBinding

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

首先在build.gradle中启用ViewBinding:

android {
    ...
    viewBinding {
        enabled = true
    }
}

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

xml
<LinearLayout
        ...
        tools:viewBindingIgnore="true" >
    ...
</LinearLayout>

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

三个类绑定API:

kotlin
// 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

kotlin
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

kotlin
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

kotlin
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类似)

kotlin
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

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

封装

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

Activity的封装

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

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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需要加载:

kotlin
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,你可以决定什么时候添加,只需要添加如下代码:

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

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