跳至主要內容

从 Sunflower 开始学习优雅的 Jetpack 架构

JI,XIAOYONG...大约 10 分钟android

Google 大法 NB!!!(破音)

前言

Jetpackopen in new window是 Google 推出的一系列 Android 软件集合,"使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上"

Sunfloweropen in new window则是 Google 用来演示如何使用 Jetpack 进行 Android 开发的 Demo,有着非常优雅的架构与十分简洁的代码,可以帮助我们很好地学习 Jetpack 以及 MVVM 思想。

本文主要是结合 Sunflower 中的示例代码,分析 Jetpack 架构中各部分的作用,以及他们如何巧妙的搭配使用,方便指导日后对 Jetpack 的使用。

本文中的大部分代码、示意图除非特殊注明外,皆来自 Google 的Sunflower 工程open in new window或其他互联网资源,根据篇幅需要做了部分精简,所有权益归原作者所有。

下图是Google Jetpack 官网open in new window对 Jetpack 的介绍图:

## 对 Sunflower 的整体分析

下图是 Sunflower 架构的简单示意图:

可以看到,APP 的界面有我的花园植物目录植物介绍三部分,这三者的切换逻辑通过实现。

每个界面的XML中的布局信息(包括数据、事件(clickListener等),RecycleView的LayoutManager,Adapter等等)通过DataBindingViewModel中的可观察数据LiveData绑定在一起,只要数据库中的数据有更新,就会通过LiveData主动通知布局更新界面;同时DataBinding还通过与Adapter(这些继承自ListAdapter的 Adapter 实现了的作用)将ItemViewViewModel与布局XML中绑定在一起,通过BindingAdapterXML中的数据做预处理(加载 imgUrl 中的图片到 ImageView 等等)。

中指定这些DataBinding之间以及ViewModel数据库之间的逻辑关系,这些数据与操作都受着的影响。

ViewModel的数据来源——Model在这里的实现是一个数据库。每个ViewModel有一个XXXViewModelFactory类,用来使用数据类XXXRepository类的实例创建对应的ViewModelXXXViewModelFactoryActivity等屏蔽了ViewModel的具体实现。

XXXRepository类的出现时为了将ViewModel与数据的具体实现解耦合,这样ViewModel只需要关心他要的操作而不必关系数据来源的具体实现。在本例中,XXXRepository类对应封装了这数据库AppDatabase中对两个表的操作。

数据库使用实现,从底层开始依次分为表Entity数据访问对象DAO数据库DataBase三个层次。每个 DAO 对应一个包装类XXXRepository类供ViewModel使用。

@Entity    GardenPlanting    //表,定义了存储的数据项及其格式
@Dao    GardenPlantingDao    //数据访问对象,定义了例如插入数据、查询数据等操作
 GardenPlantingRepository    //对DAO的封装,将数据的的具体实现与ViewModel对数据的操作解耦
@Database     AppDatabase    //数据库,包括表和对表的操作
则管理着一个从 Json 读取数据并加载到数据库中的后台任务SeedDatabaseWorker

具体实现分析

首先看一下**View**部分,Sunflower 只有简单的 3 个页面,全都是用Fragment实现,由Activity通过Navigation控制切换:

  • GardenActivity 主页面,唯一的一个 Activity
  • GardenFragment 我的花园 界面,会显示用户在植物目录中选择并种植的植物信息
  • PlantListFragment 植物目录 界面,所有的植物信息列表
  • PlantDetailFragment 植物介绍 界面,当在“我的花园”或“植物列表”选择了某个植物后,会进入该界面显示植物详细介绍

先看一下Navigationopen in new window的定义:

Navigation 是 APP 设计中的关键部分,可以用来定义用户从不同的界面切换、进入和推出的交互逻辑。

和布局文件一样,我们可以在编译器的可视化界面中,直接预览、设计不同界面切换效果。他可以负责FragmentActivityNavigation graphssubgraphs 以及Custom destination types,他们之间通过不同的action连接起来。

通过官方文档可知,Navigation可以和AppBarToolBar等组合起来控制 Fragment 显示,此外可以通过ViewModel在绑定到同一个ActivityFragment之间共享数据,或者也可以通过BundleSafe Argsopen in new window在两个Fragment之间传递数据。

那么,在SunflowerNavigation是怎么控制界面切换的呢?

首先,在res/navigation/目录下面新建一个嵌套导航图(Nested navigation graphs),定义各个界面之前的切换关系:

<navigation 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"
    app:startDestination="@+id/garden_fragment">
//app:startDestination定义了在这个导航图中首次启动展示的界面
    <fragment
        android:id="@+id/garden_fragment"
        android:name="com.google.samples.apps.sunflower.GardenFragment"
        android:label="@string/my_garden_title"
        tools:layout="@layout/fragment_garden">
//action定义了在各个界面的切换关系
        <action
            android:id="@+id/action_garden_fragment_to_plant_detail_fragment"
            app:destination="@id/plant_detail_fragment"
            app:enterAnim="@anim/slide_in_right"//enterAnim等指定执行action时的动画
          .../>
    </fragment>

    <fragment
        ...>
//argument定义了在切换界面时需要带的参数,需要androidx.navigation.safeargs的支持,具体见参考资料-Android Jetpack-Navigation 使用中参数的传递
        <argument
            android:name="plantId"
            app:argType="string" />//参数类型小写
    </fragment>
</navigation>

然后在 Activity 对应的 XML 中插入该导航:

<LinearLayout ...>
	<fragment
        android:id="@+id/garden_nav_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_garden" />

</LinearLayout>

之后就可以在 Activity 或者 Fragment 中获取该导航的实力,用来切换界面了:

//activity
val navController = Navigation.findNavController(this, R.id.garden_nav_fragment)
//fragment 或其他地方
val direction = GardenFragmentDirections//嵌套导航图中 Fragment 自动生成的类
.ActionGardenFragmentToPlantDetailFragment(plantId)
it.findNavController().navigate(direction)

DataBinding 绑定布局和数据

Navigation 解决了不同的布局间交互的逻辑,DataBinding 则充当布局 View 和数据(ViewModel、LiveData)之间的桥梁,将二者联系起来。

官网open in new window的表述中我们知道,DataBinding 使用在 XML 中声明的方式(而非编程的方式),将布局中的组件捆绑到 APP 中使用到的数据上,这样当数据更新时,布局也会随之自动更新。

DataBinding 在 XML 中的形式如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewmodel"
            type="com.myapp.data.ViewModel" />
    </data>
    <ConstraintLayout... /> <!-- UI layout's root element -->
</layout>

需要注意的是原先的页面布局信息<ConstraintLayout... />包裹在<layout... />中,同时多了一个数据域<data... />,我们可以在其中定义一些变量<variable... />,并在布局中使用:

<TextView
    android:text="@{viewmodel.userName}" />

除了常见的android:textandroid:onClick等通用的属性可以直接绑定外,我们还可以通过自定义Binding adaptersopen in new window支持更多形式的属性绑定:

@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
    view.visibility = if (visible) View.VISIBLE else View.GONE
}

上面的代码就支持了app:goneUnless的解析,我们只要在 XML 中为组件加上这个属性就可以实现相应的效果:

<TextView
          android:text="@{viewmodel.userName}"
          app:goneUnless="@{viewmodel.isGone}"/>

最后,我们需要在对应的 Activity 或 Fragment 中,用如下代码将布局与页面绑定到一起:

//setContentView(R.layout.activity_main)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)
binding.viewmodel = ...

这里的ActivityMainBinding类是DataBinding根据 XML 文件的名字自动替我们生成的,规律是XML文件名+Binding的驼峰命名。

在 Sunflower 中有类似的应用有很多处:

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google LLC ...
  -->
<layout 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">

    <data>
        <variable
                name="hasPlantings"
                type="boolean" />
    </data>

    <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/garden_list"
                app:isGone="@{!hasPlantings}"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                tools:listitem="@layout/list_item_garden_planting"/>

    </FrameLayout>

</layout>

ViewModel 管理数据与页面的交互

DataBinding通过标记的形式将数据和组件绑定,在这个过程中他使用的数据则是来自于ViewModel的。在页面Activity(或Fragment) 中,我们可以处理这两者之间的关系。

ViewModel是设计用来以一种可以感知生命周期(lifecycle)的方式存储和管理与 UI 相关的数据,它可以允许数据在诸如屏幕旋转的变化中存活下来,也就是说VideModule的数据生命周期可能要比他附着的ActivityFragment的生命周期长。

同时,UI controller可以在Activity等不再需要数据时,自动调用ViewModelonCleared()方法清除这些数据以避免内存泄漏。

下图是ViewModelActivity的生命周期对比:

此外,由于默认的获取 ViewModel 的方法只能调取无参构造函数,当需要向 ViewModel 传递参数时,就需要用到 Factory 工厂模式来创建 ViewModel:
val viewModel = ViewModelProviders.of(this, factory).get(GardenPlantingListViewModel::class.java)

class PlantDetailViewModelFactory(args:Any) : ViewModelProvider.NewInstanceFactory() {...}

还可以将ViewModelLiveData结合,这样在Activity等地方对LiveData进行订阅后,当LiveData的值发生变化时Activity等可以及时得到通知,而做出相应变化。此外ViewModellifecycle的结合可以保证在Activity等生命周期结束后数据得到及时的清理。

Room 保存数据

Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 强大功能的同时,能够流畅的访问数据库。——Android Developers

Room需要 3 个元素:

  • Database 数据库,可以提供对表格的操作方法@DAO。是一个继承自RoomDatabase的抽象类。
  • Entity 表格,规定了每个表格可以保存的数据格式。是一个普通类。
  • Dao 数据访问结构(Data Access Object),定义了对表格@Entity中的数据的操作。是一个接口或者抽象类。

此外,还可以对@DAO进行进一步的封装得到一个XXXRepository类,ViewModel通过这个XXXRepository类来操作数据,从而将其与数据的具体实现解耦。

WorkManager 管理任务

WorkManager用来管理即时或定时任务,官方定义是在指定约束条件成熟时可靠的在后台执行对应的任务。

具体使用可以参考这个GISTopen in new window

和他相关的有下面几个关键类:

  • Worker 定义要执行的任务内容
  • WorkRequest 代表一项单独的任务,明确具体要执行的任务内容(Worker)、任务的类型(WorkRequest.Builder 的子类,决定任务一次性还是重复的)以及任务执行的条件(Constraints,如联网、电池电量等等)
  • WorkManager 执行管理 WorkRequest,安排执行 Worker 中的工作内容。

参考资料

Android Jetpack 官网open in new window

Android Jetpack-Navigation 使用中参数的传递open in new window

文章标题:《从 Sunflower 开始学习优雅的 Jetpack 架构》
本文作者: JI,XIAOYONG
发布时间: 2019/01/24 19:48:31 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/27065732.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8