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
| fun <T> bind(view : View) : T
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) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) 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() _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 { 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
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)
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
| val customViewBinding = CustomViewBinding.inflate(layoutInflater, binding.root, false)
binding.root.addView(customViewBinding.root)
|