是时候更新手里的武器了—Jetpack架构组件简析(上)

简介: 最近两年,MVVM的呼声越来越高,说实话,在经历了MVP的臃肿,MVP的繁琐,我有点怕了。但是这次Google官方带来的一系列为MVVM架构设计的武器—Jetpack,真的让我惊喜到了。

前言


最近两年,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 属性代表一开始显示的fragment
  • android:name 属性代表对应的Fragment路径
  • action 代表该Fragment存在的跳转事件,比如myFragment1可以跳转myFragment2。


  1. 修改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代码地址



目录
相关文章
|
2月前
|
监控 网络协议 Java
Tomcat源码解析】整体架构组成及核心组件
Tomcat,原名Catalina,是一款优雅轻盈的Web服务器,自4.x版本起扩展了JSP、EL等功能,超越了单纯的Servlet容器范畴。Servlet是Sun公司为Java编程Web应用制定的规范,Tomcat作为Servlet容器,负责构建Request与Response对象,并执行业务逻辑。
Tomcat源码解析】整体架构组成及核心组件
|
17天前
|
负载均衡 5G 网络性能优化
深入解析LTE(长期演进技术)的基本架构及其关键组件
深入解析LTE(长期演进技术)的基本架构及其关键组件
81 2
|
2月前
|
Kubernetes API 调度
Kubernetes 架构解析:理解其核心组件
【8月更文第29天】Kubernetes(简称 K8s)是一个开源的容器编排系统,用于自动化部署、扩展和管理容器化应用。它提供了一个可移植、可扩展的环境来运行分布式系统。本文将深入探讨 Kubernetes 的架构设计,包括其核心组件如何协同工作以实现这些功能。
102 0
|
27天前
|
XML Java 数据库
在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂
【9月更文挑战第8天】在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂。日志作为系统行为的第一手资料,传统记录方式因缺乏全局视角而难以满足跨服务追踪需求。本文通过一个电商系统的案例,介绍如何在Spring Boot应用中手动实现日志链路追踪,提升调试效率。我们生成并传递唯一追踪ID,确保日志记录包含该ID,即使日志分散也能串联。示例代码展示了使用过滤器设置追踪ID,并在日志记录及配置中自动包含该ID。这种方法不仅简化了问题定位,还具有良好的扩展性,适用于各种基于Spring Boot的微服务架构。
31 3
|
2月前
|
存储 安全 虚拟化
深入解析:Docker的架构与组件
【8月更文挑战第27天】
133 5
|
2月前
|
JSON 前端开发 API
Django 后端架构开发:通用表单视图、组件对接、验证机制和组件开发
Django 后端架构开发:通用表单视图、组件对接、验证机制和组件开发
47 2
|
2月前
|
消息中间件 运维 NoSQL
基础架构组件选型及服务化
【8月更文挑战第22天】本文概述了分布式系统中常见的基础架构组件及其选型与服务化的重要性。
|
10天前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2
|
10天前
|
负载均衡 Java 应用服务中间件
微服务分布式系统架构之zookeeper与dubbor-1
微服务分布式系统架构之zookeeper与dubbor-1
|
2月前
|
Kubernetes Cloud Native Docker
云原生之旅:从容器到微服务的架构演变
【8月更文挑战第29天】在数字化时代的浪潮下,云原生技术以其灵活性、可扩展性和弹性管理成为企业数字化转型的关键。本文将通过浅显易懂的语言和生动的比喻,带领读者了解云原生的基本概念,探索容器化技术的奥秘,并深入微服务架构的世界。我们将一起见证代码如何转化为现实中的服务,实现快速迭代和高效部署。无论你是初学者还是有经验的开发者,这篇文章都会为你打开一扇通往云原生世界的大门。
下一篇
无影云桌面