是时候更新手里的武器了—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代码地址



目录
相关文章
|
1月前
|
JSON JavaScript 前端开发
Vue3源码架构简析及Monorepo流程构建
【10月更文挑战第12天】Vue3源码架构简析及Monorepo流程构建
Vue3源码架构简析及Monorepo流程构建
|
29天前
|
消息中间件 存储 Java
RocketMQ(一):消息中间件缘起,一览整体架构及核心组件
【10月更文挑战第15天】本文介绍了消息中间件的基本概念和特点,重点解析了RocketMQ的整体架构和核心组件。消息中间件如RocketMQ、RabbitMQ、Kafka等,具备异步通信、持久化、削峰填谷、系统解耦等特点,适用于分布式系统。RocketMQ的架构包括NameServer、Broker、Producer、Consumer等组件,通过这些组件实现消息的生产、存储和消费。文章还提供了Spring Boot快速上手RocketMQ的示例代码,帮助读者快速入门。
|
1月前
|
存储 分布式计算 API
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
84 0
|
12天前
|
SQL 数据采集 分布式计算
【赵渝强老师】基于大数据组件的平台架构
本文介绍了大数据平台的总体架构及各层的功能。大数据平台架构分为五层:数据源层、数据采集层、大数据平台层、数据仓库层和应用层。其中,大数据平台层为核心,负责数据的存储和计算,支持离线和实时数据处理。数据仓库层则基于大数据平台构建数据模型,应用层则利用这些模型实现具体的应用场景。文中还提供了Lambda和Kappa架构的视频讲解。
【赵渝强老师】基于大数据组件的平台架构
|
1月前
|
SQL 存储 分布式计算
大数据-157 Apache Kylin 背景 历程 特点 场景 架构 组件 详解
大数据-157 Apache Kylin 背景 历程 特点 场景 架构 组件 详解
29 9
|
1月前
|
消息中间件 监控 Java
大数据-109 Flink 体系结构 运行架构 ResourceManager JobManager 组件关系与原理剖析
大数据-109 Flink 体系结构 运行架构 ResourceManager JobManager 组件关系与原理剖析
66 1
|
30天前
|
消息中间件 运维 NoSQL
基础架构组件选型及服务化
【10月更文挑战第15天】本文概述了分布式系统中常见的基础架构组件及其选型与服务化的重要性。
|
1月前
|
消息中间件 运维 NoSQL
基础架构组件选型及服务化
【10月更文挑战第2天】本文介绍了常见的分布式基础架构组件,包括分布式服务化框架(如Dubbo、Spring Cloud)、分布式缓存(如Redis、Memcached)、数据库及分布式数据库框架(如MySQL、TiDB)、消息中间件(如Kafka、RabbitMQ)和前端接入层(如LVS、Nginx)。文中探讨了组件选型问题,强调统一标准的重要性,避免重复劳动与维护难题。最后,提出基础架构服务化的必要性,通过标准化和平台化提升运维效率
|
10天前
|
缓存 负载均衡 JavaScript
探索微服务架构下的API网关模式
【10月更文挑战第37天】在微服务架构的海洋中,API网关犹如一座灯塔,指引着服务的航向。它不仅是客户端请求的集散地,更是后端微服务的守门人。本文将深入探讨API网关的设计哲学、核心功能以及它在微服务生态中扮演的角色,同时通过实际代码示例,揭示如何实现一个高效、可靠的API网关。
|
8天前
|
Cloud Native 安全 数据安全/隐私保护
云原生架构下的微服务治理与挑战####
随着云计算技术的飞速发展,云原生架构以其高效、灵活、可扩展的特性成为现代企业IT架构的首选。本文聚焦于云原生环境下的微服务治理问题,探讨其在促进业务敏捷性的同时所面临的挑战及应对策略。通过分析微服务拆分、服务间通信、故障隔离与恢复等关键环节,本文旨在为读者提供一个关于如何在云原生环境中有效实施微服务治理的全面视角,助力企业在数字化转型的道路上稳健前行。 ####
下一篇
无影云桌面