- 什么是Jetpack
1.1 简介 Jetpack是一套库、工具和指南,可以帮助开发者更轻松地编写优质应用。这些组件可以帮助开发者遵循最佳做法、
让开发者摆脱编写样板代码的工作并简化复杂任务,以便开发者将精力集中放在所需的代码上。
1.2 特性 - 加速开发 组件可以单独采用(不过这些组件是为协同工作而构建的),同时利用Kotlin语言功能帮助开发者提高工 作效率.
- 消除样板代码 Android Jetpack可管理繁琐的Activity(如后台任务、导航和生命周期管理),以便开发者可以专注 于如何让自己的应用出类拔萃。
- 构建高质量的强大应用 Android Jetpack组件围绕现代化设计实践构建而成,具有向后兼容性,可以减少崩溃和内 存泄漏。
1.3 分类
Architecture(架构组件)可帮助您设计稳健、可测试且易维护的应用。 - Data Binding: 是一种支持库,借助该库,可以以声明方式将可观察数据绑定到界面元素。
- Lifecycles: 管理Activity 和 Fragment的生命周期,能够帮助开发者轻松的应对Activity/Fragment的生命周期 变化问题,帮助开发者生成更易于维护的轻量级代码。
- LiveData: 在底层数据库更改时通知视图,是可观察的数据持有者类。与常规的可观察对象不同,LiveData具 有生命周期感知功能(例如Activity,Fragment或Service的生命周期)。
- Navigation: 处理应用内导航所需的一切。
- Paging: 逐步从您的数据源按需加载信息,帮助开发者一次加载和显示小块数据。按需加载部分数据可减少网络带宽和系统资源的使用。
- Room: 流畅地访问 SQLite 数据库。在SQLite上提供了一个抽象层,以在利用SQLite的全部功能的同时允许更健壮的数据库访问。
- ViewModle: 以注重生命周期的方式管理界面相关的数据。ViewModel类允许数据幸免于配置更改(例如屏幕旋转)。通常和DataBinding配合使用,为开发者实现MVVM架构提供了强有力的支持
- WorkManager: 管理 Android 的后台作业,即使应用程序退出或设备重新启动,也可以轻松地调度预期将要运行的可延迟异步任务。
Foundation(基础组件)可提供横向功能,例如向后兼容性、测试和 Kotlin 语言支持。
- Android KTX: 编写更简洁、惯用的 Kotlin 代码,是一组Kotlin扩展程序。优化了供Kotlin使用的Jetpack和 Android平台API。旨在让开发者利用 Kotlin 语言功能(例如扩展函数/属性、lambda、命名参数和参数默认 值),以更简洁、更愉悦、更惯用的方式使用 Kotlin 进行 Android 开发。Android KTX 不会向现有的 Android API 添加任何新功能。
- AppCompat: 帮助较低版本的Android系统进行兼容。
- Auto: 有助于开发 Android Auto 应用的组件。是 Google推出的专为汽车所设计之 Android 功能,旨在取代汽车制造商之原生车载系统来执行 Android应用与服务并访问与存取Android手机内容。
- Benchmark: 从 Android Studio 中快速检测基于 Kotlin 或 Java 的代码。
- Multidex: 为具有多个 DEX 文件的应用提供支持。
- Security: 按照安全最佳做法读写加密文件和共享偏好设置。
- Test: 用于单元和运行时界面测试的 Android 测试框架。
- TV: 有助于开发 Android TV 应用的组件。
- Wear OS by Google: 有助于开发 Wear 应用的组件。
Behavior(行为组件)可帮助您的应用与标准 Android 服务(如通知、权限、分享和 Google 助理)相集成。
- CameraX: 简化相机应用的开发工作。它提供一致且易于使用的 API 界面,适用于大多数 Android 设备,并 可向后兼容至 Android 5.0(API 级别 21)。
- DownloadManager: 是一项系统服务,可处理长时间运行的HTTP下载。客户端可以请求将URI下载到特定的 目标文件。下载管理器将在后台进行下载,处理HTTP交互,并在出现故障或在连接更改和系统重新启动后重试 下载。
- Media & playback: 用于媒体播放和路由(包括 Google Cast)的向后兼容 API。
- Notifications: 提供向后兼容的通知 API,支持 Wear 和 Auto。
- Permissions: 用于检查和请求应用权限的兼容性 API。
- Preferences: 创建交互式设置屏幕,建议使用 AndroidX Preference Library 将用户可配置设置集成至应用中。
- Sharing: 提供适合应用操作栏的共享操作。
- Slices: 是UI模板,可以通过启用全屏应用程序之外的互动来帮助用户更快地执行任务,即可以创建在应用外部显示应用数据的灵活界面。
UI(界面组件)可提供微件和辅助程序,让您的应用不仅简单易用,还能带来愉悦体验。了解有助于简化界面开发的
Jetpack Compose。
- Animation & transitions: 使开发者可以轻松地为两个视图层次结构之间的变化设置动画。该框架通过随时 间更改其某些属性值在运行时为视图设置动画。该框架包括用于常见效果的内置动画,并允许开发者创建自定义动画和过渡生命周期回调。
- Emoji: 使Android设备保持最新的最新emoji表情,开发者的应用程序用户无需等待Android OS更新即可获取最新的表情符号。
- Fragment: Activity的模块化组成部分。
- Layout: 定义应用中的界面结构。可以在xml中声明界面元素,也可以在运行时实例化布局元素。
- Palette: 是一个支持库,可从图像中提取突出的颜色,以帮助开发者创建视觉上引人入胜的应用程序。开发者可以使用调色板库设计布局主题,并将自定义颜色应用于应用程序中的视觉元素。
1.4 应用架构
MVVM架构.png
- Android Jetpack - Navigation
2.1 前言
Navigation是一个可简化的Android导航库的插件,更确切的来说, 是用来管理 的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。
2.2优点
处理 Fragment 的切换(上文已说过) 默认情况下正确处理 Fragment 的前进和后退 为过渡和动画提供标准化的资源 实现和处理深层连接
可以绑定 Toolbar 、 BottomNavigationView 和 ActionBar 等 SafeArgs (Gradle插件) 数据传递时提供类型安全性 ViewModel 支持
可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图, NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个
地点(Destination)和通往各个地点的路径, NavHostFragment 可以到达地图中的各个目的地,但是决定到什么目 的地还是方向盘 NavController ,虽然它取决于开车人(用户)。
2.3.1 第一步 添加依赖
模块层的 build.gradle 文件需要添加:
ext.navigationVersion = "2.0.0"
dependencies {
implementation "androidx.navigation:navigation-fragment-
ktx:rootProject.navigationVersion"
}
如果你要使用 SafeArgs 插件,还要在项目目录下的 build.gradle 文件添加:
buildscript {
ext.navigationVersion = "2.0.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-
plugin:$navigationVersion"
} }
以及模块下面的 build.gradle 文件添加:
apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'
2.3.2 第二步 创建navigation导航
- 创建基础目录:资源文件 res 目录下创建 navigation 目录 -> 右击 navigation 目录New一个Navigation resource file
-
创建一个
Destination
,如果说navigation是我们的导航工具,Destination是我们的目的地,在此之前,我已经写好了一个WelcomeFragment,LoginFragment,RegisterFragment
,添加Destination的操作完成后如下:
destinations.png
除了可视化界面之外,我们仍然有必要看一下里面的内容组成, login_navigation.xml :
<navigation
...
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">
<fragment
android:id="@+id/login"
android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login"
/>
<fragment
android:id="@+id/welcome"
android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_welcome">
<action
.../>
<action
.../>
</fragment>
<fragment
android:id="@+id/register"
android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_register"
>
<argument
.../>
</fragment>
</navigation>
我在这里省略了一些不必要的代码。让我们看一下 navigation标签 的属性:
app:startDestination 默认的起始位置
2.3.3 第三步 建立 NavHostFragment
我们创建一个新的 LoginActivity ,在 activity_login.xml 文件中:
<androidx.constraintlayout.widget.ConstraintLayout
...>
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/login_navigation"
app:defaultNavHost="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
有几个属性需要解释一下:
android:name 值必须是 androidx.navigation.fragment.NavHostFragment ,声明这是一个 NavHostFragment
app:navGraph 存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph
app:defaultNavHost="true" 与系统的返回按钮相关联
2.3.4 第四步 界面跳转、参数传递和动画
在 WelcomeFragment 中,点击登录和注册按钮可以分别跳转到 LoginFragment 和 RegisterFragment 中。
WelcomeFragment.这里我使用了两种方式实现:
方式一 利用ID导航
目标: 携带 key 为 name 的数据跳转到 , 接收后显示。 按钮的点击事件如下:
btnLogin.setOnClickListener { // 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
} }
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf") findNavController().navigate(R.id.login, bundle,navOption)
后续 LoginFragment 的接收代码比较简单,直接获取Fragment中的 Bundle 即可,这里不再出示代码。最后的效果:
LoginFragment
方式二 利用 Safe Args
目标:WelcomeFragment通过Safe Args将数据传到RegisterFragment,RegisterFragment接收后显示。再
看一下已经展示过的 login_navigation.xml :
<navigation
...>
<fragment
...
/>
<fragment
android:id="@+id/welcome"
>
<action
android:id="@+id/action_welcome_to_login"
app:destination="@id/login"/>
<action
android:id="@+id/action_welcome_to_register"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/register"/>
</fragment>
<fragment
android:id="@+id/register"
...
>
<argument
android:name="EMAIL"
android:defaultValue="2005@qq.com"
app:argType="string"/>
</fragment>
</navigation>
细心的同学可能已经观察到 navigation 目录下的 login_navigation.xml 资源文件中的 action 标签和 argument 标签,这里需要解释一下:
action标签
app:destination 跳转完成到达的 fragment 的Id
app:popUpTo 将 fragment 从 栈 中弹出,直到某个Id的 fragment
argument标签
android:name 标签名字
app:argType 标签的类型
android:defaultValue 默认值
点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类:
RegisterFragmentArgs WelcomeFragmentDirections
系统生成的类 WelcomeFragment中的JOIN US按钮点击事件:
btnRegister.setOnClickListener {
val action = WelcomeFragmentDirections
.actionWelcomeToRegister()
.setEMAIL("TeaOf1995@Gamil.com")
findNavController().navigate(action)
}
RegisterFragment 中的接收:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val safeArgs:RegisterFragmentArgs by navArgs()
val email = safeArgs.email
mEmailEt.setText(email)
}
RegisterFragment需要提及的是,如果不用Safe Args,action
可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)
方式生成,感兴趣的同学可以自行编写。
4.2更多
Navigation可以绑定menus、drawers和bottom navigation,这里我们以bottom navigation为例,我先在 navigation 目录下新创建了 main_navigation.xml ,接着新建了 MainActivity ,
下面则是 activity_main.xml :
<LinearLayout
...>
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
app:navGraph="@navigation/main_navigation"
app:defaultNavHost="true"
android:layout_height="0dp"
android:layout_weight="1"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:itemIconTint="@color/colorAccent"
app:itemTextColor="@color/colorPrimary"
app:menu="@menu/menu_main"/>
</LinearLayout>
MainActivity 中的处理也十分简单:
class MainActivity : AppCompatActivity() {
lateinit var bottomNavigationView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
//...
val host: NavHostFragment =
supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
val navController = host.navController
initWidget()
initBottomNavigationView(bottomNavigationView,navController)
}
private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView,
navController: NavController) {
bottomNavigationView.setupWithNavController(navController)
}
private fun initWidget() {
bottomNavigationView = findViewById(R.id.navigation_view)
} }
2.5 总结
nav总结.png
上图概括了本文的一些知识点,当然还有一些知识点没有涉及,比如:深层连接等.
-
Android MVVM
3.2.1 MVVM介绍:MVVM(全称Model-View-ViewModel)同 MVC 和 MVP 一样,是逻辑分层解偶的模式
mvvm.png
MVVM结构图
从上图我们可以了解到MVVM的三要素,他们分别是:
View层:xml、Activity、Fragment、Adapter和View等
Model层:数据源(本地数据和网络数据等)
ViewModel层:View层处理数据以及逻辑处理
3.2.2 Data Binding介绍
Data Binding不算特别新的东西,2015年Google就推出了,但即便是现在,很多人都没有学习过它,我就是这些 工程师中的一位,因为我觉得MVP已经足够帮我处理日常的业务,Android Jetpack的出现,是我研究DataBinding的一个契机。
在进行下文之前,我有必要声明一下,MVVM和Data Binding是两个不同的概念,MVVM是一种架构模式,而Data Binding是一个实现数据和UI绑定的框架,是构建MVVM模式的一个工具。
Data Binding的实现
3.3.1 第一步 在app模块下的 文件添加内容
android { ...
dataBinding {
enabled true
} }
3.3.2 第二步 构建LoginModel
创建登录的 LoginModel , LoginModel 主要负责登录逻辑的处理以及两个输入框内容改变的时候数据更新的处理:
class LoginModel constructor(name: String, pwd: String, context: Context) {
val n = ObservableField<String>(name)
val p = ObservableField<String>(pwd)
var context: Context = context
/**
- 用户名改变回调的函数 /
fun onNameChanged(s: CharSequence) {
n.set(s.toString())
}
/* - 密码改变的回调函数 */
fun onPwdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
p.set(s.toString())
}
fun login() {
if (n.get().equals(BaseConstant.USER_NAME)
&& p.get().equals(BaseConstant.USER_PWD) ){
Toast.makeText(context, "账号密码正确", Toast.LENGTH_SHORT).show() val intent = Intent(context, MainActivity::class.java) context.startActivity(intent)
} }
}
我们可能会对 ObservableField 存在疑惑,那么 ObservableField 是什么呢?它其实是一个可观察的域,通过泛型来使用,可以使用的方法也就三个:
ObservableField(T value) 构造函数,设置可观察的域
T get() 获得可观察域的内容,可使用UI控件检测它的值
set(T value) 设置可观察的域,设置成功之后,会通知UI控件进行更新
不过,除了使用ObservableField之外,Data Binding为我们提供了基本类型的ObservableXXX(如 ObservableInt )和存放容器的 ObservableXXX (如 ObservableList<T> )等,同样,如果你想让你自定义的类变成
可观察状态,需要实现 Observable 接口。
我们再回头看看 LoginModel 这个类,它其实只有分别用来观察 name 和 pwd 的成员变量 n 和 p ,外加一个处理登录
逻辑的方法,非常简单。
3.3.3 第三步 创建布局文件
引入Data Binding之后的布局文件的使用方式会和以前的布局使用方式有很大的不同,且听我一一解释:
layout 用作布局的根节点,只能包裹一个View标签,且不能包裹merge标签。
data Data Binding的数据,只能存在一个data标签。
variable data 中使用,数据的变量标签, type 属性指明变量的类,如
com.joe.jetpackdemo.viewmodel.LoginModel 。 name 属性指明变量的名字,方便布局中使 用。
import data 中使用,需要使用静态方法和静态常量,如需要使用View.Visble属性的时候,则需导入
<import type="android.view.View"/>。type属性指明类的路径,如果两个import标签 导入的类名相同,则可以使用 alias 属性声明别名,使用的时候直接使用别名即可。
include View标签中使用,作用同普通布局中的 include 一样,需要使用 bind:<参数名> 传递参数
我们再看一下 LoginFragment 下的 fragment_login.xml 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<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="model"
type="com.joe.jetpackdemo.viewmodel.LoginModel"/>
<variable
name="activity"
type="androidx.fragment.app.FragmentActivity"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/txt_cancel"
android:onClick="@{()-> activity.onBackPressed()}"
/>
<TextView
android:id="@+id/txt_title"
app:layout_constraintTop_toTopOf="parent"
.../>
<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
android:onTextChanged="@{(text, start, before, count)-
model.onNameChanged(text)}"
...
true}"
/>
<EditText
android:id="@+id/et_pwd"
android:text="@{model.p.get()}"
android:onTextChanged="@{model::onPwdChanged}"
...
/>
<Button
android:id="@+id/btn_login"
android:text="Sign in"
android:onClick="@{() -> model.login()}"
android:enabled="@{(model.p.get().isEmpty()||model.n.get().isEmpty()) ? false :
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
variable 有两个:
model :类型为 com.joe.jetpackdemo.viewmodel.LoginModel ,绑定用户名详见 et_account EditText中 的 android:text="@{model.n.get()}" ,当EditText输入框内容变化的时候有如下处理
android:onTextChanged="@{(text, start, before, count)->model.onNameChanged(text)}",以及登 录按钮处理android:onClick="@{() -> model.login()}"。
activity :类型为 androidx.fragment.app.FragmentActivity ,主要用来返回按钮的事件处理,详见 txt_cancelTextView的android:onClick="@{()-> activity.onBackPressed()}"。
对于以上的内容,我仍然有知识点需要讲解:
-
属性的引用
如果想使用ViewModel中成员变量,如直接使用 model.p 。 -
事件绑定
事件绑定包括 方法引用 和 监听绑定 :
方法引用 :参数类型和返回类型要一致,参考 et_pwd EditText的 android:onTextChanged 引用。 监听绑定 :相比较于 方法引用 , 监听绑定 的要求就没那么高了,我们可以使用自行定义的函数,参考 et_account EditText的 android:onTextChanged 引用。 -
表达式
如果你注意到了 btn_login Button在密码没有内容的时候是灰色的:
是因为它在 android:enabled 使用了表达式:@{(model.p.get().isEmpty()||model.n.get().isEmpty())?false:true}
,它的意思是用户名和密码为空的时候登录的 enable 属性为false,这是普通的三元表达式,除了
上述的||和三元表达式之外,Data Binding还支持:
运算符 + - / * % 字符串连接 + 逻辑与或 && || 二进制 & | ^ 一元 + - ! ~
移位 >> >>> <<
比较 == > < >= <= (Note that < needs to be escaped as <) instanceof
Grouping ()
Literals - character, String, numeric, null
Cast
方法调用
域访问
数组访问
三元操作符
除了上述之外,Data Binding新增了空合并操作符??,例如android:text="@{user.displayName??user.lastName}",它等价于android:text=“@{(user.displayName != null?user.displayName:user.lastName)}”.
3.3.4 第四步 生成绑定类
我们的布局文件创建完毕之后,点击Build下面的Make Project,让系统帮我生成绑定类,生成绑定的类如下:
bind类.png
下面我们只需在LoginFragment完成绑定即可,绑定操作既可以使用上述生成的
FragmentLoginBinding也可以使用自带的DataBindingUtil完成:
-
使用DataBindingUtil
我们可以看一下DataBindingUtil的一些常用Api:
setContentView 用来进行Activity下面的绑定
inflate 用来进行Fragment下面的绑定
bind 用来进行View的绑定
LoginFragment 绑定代码如下:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentLoginBinding = DataBindingUtil.inflate(
inflater
, R.layout.fragment_login
, container
, false
)
loginModel = LoginModel("","",context!!)
binding.model = loginModel
binding.activity = activity
return binding.root
} -
使用生成的 FragmentLoginBinding
使用方法与第一种类似,仅需将生成方式改成val binding = FragmentLoginBinding.inflate( inflater,container , false )即可
运行一下代码,开始图的效果就出现了。
3.4 更多
Data Binding还有一些有趣的功能,为了让同学们了解到更多的知识,我们在这里有必要探讨一下:
3.4.1 布局中属性的设置
有属性有setter的情况
如果XXXView类有成员变量borderColor,并且XXXView类有setBoderColor(int color)方法,那么在布局中我
们就可以借助Data Binding直接使用app:borderColor这个属性,不太明白?没关系,以DrawerLayout为例,
DrawerLayout 没有声明 app:scrimColor 、 app:drawerListener ,但是 DrawerLayout 有 mScrimColor:int 、
mListener:DrawerListener 这两个成员变量并且具有这两个属性的 setter 的方法,他就可以直接使用
app:scrimColor 、 app:drawerListener 这两个属性,代码如下:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
没有setter但是有相关方法
还用XXXView为例,它有成员变量 borderColor ,这次设置 borderColor 的方法是 setBColor (总有程序员乱写方 法名~),强行用app:borderColor显然是行不通的,可以这样用的前提是必须有setBoderColor(int color)方 法,显然 setBColor 不匹配,但我们可以通过 BindingMethods 注解实现 app:borderColor 的使用,代码如下:
@BindingMethods(value = [
BindingMethod(
type = 包名.XXXView::class, attribute = "app:borderColor", method = "setBColor")])
自定义属性
这次不仅没 setter 方法,甚至连成员变量都需要自带(条件越来越刻苦~),这次我们的目标就是给EditText添加文
本监听器,先在 LoginModel 中自定义一个监听器并使用 @BindingAdapter 注解:
// SimpleWatcher 是简化了的TextWatcher
val nameWatcher = object : SimpleWatcher() {
override fun afterTextChanged(s: Editable) {
super.afterTextChanged(s)
n.set(s.toString())
}
}
@BindingAdapter("addTextChangedListener")
fun addTextChangedListener(editText: EditText, simpleWatcher: SimpleWatcher) {
editText.addTextChangedListener(simpleWatcher)
}
这样我们就可以在布局文件中对EditText愉快的使用 app:addTextChangedListener 属性了:
<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
app:addTextChangedListener="@{model.nameWatcher}"/>
效果与我们之前使用的时候一样
3.4.2 双向绑定
使用双向绑定可以简化我们的代码,比如我们上面的EditText在实现双向绑定之后既不需要添加 也 不需要用方法调用,怎么实现呢?代码如下:
<EditText
android:id="@+id/et_account"
android:text="@={model.n.get()}"/>
仅仅在将 @{model.n.get()} 替换为 @={model.n.get()} ,多了一个 = 号而已,需要注意的是,属性必须是可观察 的,可以使用上面提到的 ObservableField ,也可以自定义实现 BaseObservable 接口,双向绑定的时候需要注意 无限循环,更多关于双向绑定还请查看官方文档。
3.5 总结
databinding.png
4.1 LiveData
在讲 LiveData 之前,我们先看看 LiveData 和 ViewModel 的作用:
mvvm流程图.png
ViewModel和LiveData在整个MVVM架构中担当数据驱动的职责,这也是MVVM模式中的ViewModel层的作用。
4.1.1 介绍
从官网的介绍可以看到, LiveData 作用跟RxJava类似,是观察数据的类,相比RxJava,它能够在Activity、 Fragment和Service之中正确的处理生命周期。那么 LiveData 有什么优点呢?
数据变更的时候更新UI
没有内存泄漏
不会因为停止Activity崩溃
无需手动处理生命周期
共享资源
乍看之下 LiveData 挺鸡肋的,事实也确实如此,因为 LiveData 能够实现的功能 RxJava 也可以实现,而且与 LiveData 相比, RxJava 拥有着更加丰富的生态,当然,谷歌的官方架构仍然值得我们去学习。
4.1.2 使用方式
LiveData 常用的方法也就如下几个:
observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T>observer) 最常用的方法,需要提供Observer处理数据变更的处理.LifecycleOwner
则是我们能够正确处理生命周期的关健
setValue(T value) 设置数据
getValue():T 获取数据
postValue(T value) 在主线程中更新数据
4.1.3 使用场景
我看见绝大部分的LiveData都是配合其他Android Jetpack组件使用的,具体情况具体分析。
ViewModel : 见下文。
Room :先参考Demo,文章后续推出。
4.2 ViewModel
众所周知,MVVM层中ViewModel层用来作逻辑处理的,那么我们Android Jetpack组件中ViewModel的作用是否 也一致呢?
ViewModel 同样具有生命周期意识的处理跟UI相关的数据,并且,当设备的一些配置信息改变(例如屏幕旋转)它 的数据不会消失。
通常情况下,如果我们不做特殊处理,当屏幕旋转的时候,数据会消失,那 ViewModel 管理的数据为什么不会消失 呢,是因为 ViewModel 的生命周期:
viewModel.png
ViewModel的另一个特点是同一个Activity的Fragment之间使用ViewModel实现共享数据.
4.2.2 使用方法 继承 ViewModel 即可。
第一步:添加依赖
添加进 module 下面的 build.gradle :
ext.lifecycleVersion=’2.2.0-alpha01‘
dependencies{
//LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleVersion"
}
第二步 创建 ShoeModel
继承 ViewModel 类,分别创建对品牌名的观察对象 brand:MutableLiveData<String> 和对鞋子集合的观察对象
shoes: LiveData<List<Shoe>>:
class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
// 品牌的观察对象 默认观察所有的品牌
private val brand = MutableLiveData<String>().apply {
value = ALL }
// 鞋子集合的观察类
val shoes: LiveData<List<Shoe>> = brand.switchMap {
// Room数据库查询,只要知道返回的是LiveData<List<Shoe>>即可 if (it == ALL) {
shoeRepository.getAllShoes()
} else {
shoeRepository.getShoesByBrand(it)
}
}
//... 不重要的函数省略
companion object {
private const val ALL = "所有"
} }
第三步:获取ViewModel
无构造参数获取: 构造函数没有参数的情况下,获取 ShoeModel 很简单,
ViewModelProviders.of(this).get(ShoeModel::class.java) 这样就可以返回一个我们需要的 ShoeModel 了。
有构造参数获取 不过,上面的 ShoeModel 中我们在构造函数中需要一个 ShoeRepository 参数,上述方法是显然行
不通的,这种情况下我们需要自定义实现 Factory :
class ShoeModelFactory(
private val repository: ShoeRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ShoeModel(repository) as T
}
}
为了使用方便,又写了一个工具类 CustomViewModelProvider :
object CustomViewModelProvider { // ...省略无关代码
fun providerShoeModel(context: Context):ShoeModelFactory{
val repository:ShoeRepository = RepositoryProvider.providerShoeRepository(context)
return ShoeModelFactory(repository)
} }
最后在 ShoeFragment 中获取:
// by viewModels 需要依赖 "androidx.navigation:navigation-ui- ktx:$rootProject.navigationVersion"
private val viewModel: ShoeModel by viewModels {
CustomViewModelProvider.providerShoeModel(requireContext())
}
第四步:使用ViewModel
ViewModel 的使用需要结合具体的业务,比如我这里的 ShoeModel ,因为 ShoeFragment 的代码不多,我直接贴出来:
/**
- 鞋子集合的Fragment *
/
class ShoeFragment : Fragment() {
// by viewModels 需要依赖 "androidx.navigation:navigation-ui- ktx:$rootProject.navigationVersion"
private val viewModel: ShoeModel by viewModels {
CustomViewModelProvider.providerShoeModel(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentShoeBinding = FragmentShoeBinding.inflate(inflater, container,
false)
context ?: return binding.root ViewModelProviders.of(this).get(ShoeModel::class.java) // RecyclerView 的适配器 ShoeAdapter
val adapter = ShoeAdapter()
binding.recycler.adapter = adapter onSubscribeUi(adapter)
return binding.root
}
/* - 鞋子数据更新的通知 /
private fun onSubscribeUi(adapter: ShoeAdapter) {
viewModel.shoes.observe(viewLifecycleOwner, Observer {
if (it != null) {
adapter.submitList(it)
} })
}}
在 onSubscribeUi 方法中,我们使用 ShoeModel 的 LiveData 进行了观察通知,当鞋子集合更新的时候,会更新到 当前 RecyclerView 中的适配器。
布局文件fragment_shoe.xml很简单,虽使用了Data Binding,但是没有变量,且只有一个RecyclerView,这 里不再赘述。 ShoeAdapter 的实现同样简单,感兴趣的可以查看源码,这里同样不再赘述。
这样写完之后,本文一开始的图的效果就出现了~
4.3 更多
一个例子并不能展现所有的关于 LiveData 和 ViewModel 的内容。 LiveData 和 ViewModel 仍有一些知识需要我们 注意。
4.3.1 LiveData数据变换
LiveData 中数据变换方法有 map() 和 switchMap() ,关于 switchMap() ,我在上面实战的 ShoeModel 已经实践 过了:
// 本地数据仓库
class ShoeRepository private constructor(private val shoeDao: ShoeDao) {
fun getAllShoes() = shoeDao.getAllShoes()
/* - 通过品牌查询鞋子 返回 LiveData<List<Shoe>> /
fun getShoesByBrand(brand:String) = shoeDao.findShoeByBrand(brand)
/* - 插入鞋子的集合 返回 LiveData<List<Shoe>> /
fun insertShoes(shoes: List<Shoe>) = shoeDao.insertShoes(shoes)
// ... 单例省略 }
class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
// 品牌的观察对象 默认观察所有的品牌
private val brand = MutableLiveData<String>().apply {
value = ALL }
// 鞋子集合的观察类
val shoes: LiveData<List<Shoe>> = brand.switchMap {
} }
// Room数据库查询,只要知道返回的是LiveData<List<Shoe>>即可 if (it == ALL) {
shoeRepository.getAllShoes()
} else {
shoeRepository.getShoesByBrand(it)
}}}
map() 的使用我们借用官方的例子:
val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = Transformations.map(userLiveData) {
user -> "{user.lastName}"}
可以看到, map() 同样可以实现将A变成B,那么 switchMap() 和 map() 的区别是什么? map() 中只有一个 LiveData<A> ,他是在 LiveData<A> 发送数据的时候把A变成B,而 switchMap() 中同时存在 LiveData<A> 和 LiveData<B> , LiveData<A> 更新之后通知 LiveData<B> 更新。
4.3.2 LiveData如何共享数据
假设我们有这样的需求:注册页需要记录信息,注册完成跳转到登录页,并将账号和密码显示在登录页。这种情况 下,我们可以定义一个类然后继承 LiveData ,并使用单例模式即可:
// 登录信息
data class LoginInfo constructor(val account:String, val pwd:String, val email:String)
/* - 自定义单例LiveData */
class LoginLiveData:LiveData<LoginInfo>() {
companion object {
private lateinit var sInstance: LoginLiveData
@MainThread
fun get(): LoginLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else LoginLiveData()
return sInstance
}}}
需要实例的时候用单例创建即可。
4.3.3 使用ViewModel在同一个Activity中的Fragment之间共享数据
想要利用 ViewModel 实现Fragment之间数据共享,前提是 Fragment 中的 FragmentActivity 得相同,这里直接贴 上官方的代码:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
} }
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
} }
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}}}
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
} }
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
4.4 总结
LiveData.png
网友评论