前言
最近两年,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。
官方文档
Demo代码地址
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) } }
官方文档
Demo代码地址
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
就会移除这个观察者,不再发送消息,所以也就避免崩溃问题。
官方文档
Demo代码地址
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
属性代表一开始显示的fragmentandroid: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
类,以便以类型安全的方式浏览和访问任何关联的参数。这里就不细说了,感兴趣的可以去官网看看
官方文档
Demo代码地址