private val _waveCode = MutableLiveData()
val waveCode: LiveData = _waveCode
内部用MutableLiveData,可以修改值,对外暴露成LiveData类型,只能获取值,不能修改值。
LiveData 有一个实现了中介者模式的子类 ——MediatorLiveData,它可以把多个 LiveData 整合成一个,只要任何一个 LiveData 有数据变化,它的观察者就会收到消息:
val liveData1 = …
val liveData2 = …
val liveDataMerger = MediatorLiveData<>();
liveDataMerger.addSource(liveData1) { value -> liveDataMerger.setValue(value))
liveDataMerger.addSource(liveData2) { value -> liveDataMerger.setValue(value))
综上,我们汇总一下 LiveData 的使用场景:
LiveData - immutable 版本
MutableLiveData - mutable 版本
MediatorLiveData - 可汇总多个数据源
SingleLiveEvent - 事件总线
LiveData 只存储最新的数据,虽然用法类似 RxJava2 的 Flowable,但是它不支持背压(backpressure),所以不是一个流(stream),利用 LiveDataReactiveStreams 我们可以实现 Flowable 和 LiveData 的互换。
如果把异步获取到的数据封装成 Flowable,通过toLiveData方法转换成 LiveData,既利用了 RxJava 的线程模型,还消除了 Flowable 与 UI Controller 生命周期的耦合关系,借助 Data Binding 再把 LiveData 绑定到 xml UI 元素上,数据驱动 UI,妥妥的响应式。于是一幅如下模样的数据流向图就被勾勒了出来:
图中右上角的 Local Data 是 AAC 提供的另一个强大武器 —— ORM 框架 Room。
Room
数据库作为数据持久层,其重要性不言而喻,当设备处于离线状态时,数据库可用于缓存数据;当多个 App 需要共享数据时,数据库可以作为数据源,但是基于原生 API 徒手写 CRUD 实在是痛苦,虽然 Github 上出现了不少 ORM 框架,但是它们的易用性也不敢让人恭维,直到 Room 出来之后,Android 程序员终于可以像 mybatis 那样轻松地操纵数据库了。
Room 是 SQLite 之上的应用抽象层,而 SQLite 是一个位于 Android Framework 层的内存型数据库。虽然 Realm 也是一个优秀的数据库,但是它并没有内置于 Android 系统,所会增大 apk 的体积,使用 Room 则没有这方面烦恼。
Room 的结构抽象得非常简单,数据对象(表名 + 字段)用**@Entity注解来定义,数据访问用@Dao来注解,db 本身则用@Database来定义,如果要支持复杂类型,可以定义@TypeConverters**,然后在编译阶段,apt 会根据这些注解生成代码。
Entity 是一个数据实体,表示一条记录,它的用法如下:
@Entity(tableName = “actors”)
data class Actor(
@PrimaryKey @ColumnInfo(name = “id”)
val actorId: String,
val name: String,
val birthday: Date?,
val pictureUrl: String
)
Actor是一个用**@Entity注解的 data class,它会生成一个名字是actors的表,注意到有一个字段是@Date**?,但是 SQLite 本身不支持这种复杂类型(complex type),所以我们还需要写一个可以转换成基础类型的转换器:
class Converters {
@TypeConverter
fun timestampToDate(value: Long?) = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?) = date?.time
}
转换器通过**@TypeConverters可作用于 class、field、method、parameter,分别代表不同的作用域。比如作用在@Database类的上,那么它的作用域就是 db 中出现的所有@Dao和@Entity**。
@Database(entities = [Actor::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun actorDao(): ActorDao
}
代码出现的ActorDao定义了 CRUD 操作。用**@Dao**来注解,它既可以是一个接口,也可以是抽象类,用法如下:
@Dao
interface ActorDao {
@Query(“SELECT * FROM actors WHERE id = :actorId”)
fun getActor(actorId: String): LiveData
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(actors: List)
}
@Query中的 SQL 语句可以直接引用方法参数,而且它的返回值可以是LiveData类型,也支持Flowable类型,也就是说,Room 原生支持响应式,这是对数据驱动最有利的支持,也是 Room 区别于其他 ORM 框架的显著特征。
至此,我们可以确定,无论数据来自 Remote 还是来自本地 DB,架构蓝图中的 Repository 对 ViewModel 提供的数据可以永远是 LiveData 类型,接下来我们看一下 ViewModel 的妙用。
ViewModel
ViewModel 是一个多面手,因为它的生命周期比较长,可以跨越因为配置变动(configuration changed,比如屏幕翻转)引起的 Activity 重建,因此 ViewModel 不能持有对 Activity / Fragment 的引用。
如果 ViewModel 中要用到 context 怎么办呢?没关系,框架提供了一个 ViewModel 的子类AndroidViewModel,它在构造时需要传入Application实例。
既然 ViewModel 与 UI Controller 无关,当然可以用作 MVP 的 Presenter 层提供 LiveData 给 View 层,因为 LiveData 绑定了 Lifecycle,所以不存在内存泄露的问题。除此之外,ViewModel 也可以用做 MVVM 模式的 VM 层,利用 Data Binding 直接把 ViewModel 的 LiveData 属性绑定到 xml 元素上,xml 中声明式的写法避免了很多样板代码,数据驱动 UI 的最后一步,我们只需要关注数据的变化即可,UI 的状态会自动发生变化。
ViewModel 配合 Data Binding 的用法与 React 非常相似,ViewModel 实例相当于state,xml 文件就好比render函数,只要state数据发生变化,render就会重新渲染 UI,但是 data binding 还有更强大的一点,它支持双向绑定。举个例子,UI 需要展示一个评论框,允许展示评论,也允许用户修改,那么我们可以直接把EditText双向绑定到一个 LiveData 之上,只要用户有输入,我们就可以收到通知,完全不需要通过 Kotlin/Java 来操控 UI:
<TextInputEditText
android:text=“@={viewModel.commentText}” />
注意,如果要在 xml 中使用 LiveData,需要把 lifecycle owner 赋给 binding:
val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.main)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
因为 ViewModel 拿到的数据是 Repository 给的,可能不适用于 UI 元素,所以 ViewModel 还承担了数据适配的工作,有时候我们需要汇总 repository 的多个返回值一次性给到 UI,那么就可以使用 LiveData 的“操作符”Transformations.switchMap,用法可以认为等同于 Rx 的flatMap;如果只想对 LiveData 的 value 做一些映射,可以使用Transformations.map,目前 Transformations 只有这两个操作符,因为不管 Kotlin 还是 Java8,都提供了很多声明式的操作符,对流的支持都比较友好,而LiveData本身不是一个流,所以这两个操作符足矣。
除了数据适配之外,ViewModel 还有一个强大的用法 —— Fragment 之间共享数据,这样 ViewModel 又扮演了 FLUX 模式中的 store 这一角色,是多个页面(fragment)之间唯一的数据出口。
ViewModel 的用法也非常简单,通过ViewModelProviders.of可以获取 ViewModel 实例:
val viewModel = ViewModelProviders.of(requireActivity(), factory) .get(ActorViewModel::class.java)
一通操作猛如虎之后,UI controller 层变得薄如蝉翼,它只做了一件事情,把数据从左手(ViewModel)倒给了右手(使用了 Data Binding 的 xml)。
如果把 ViewModel 作为 SSOC(唯一真相源),多个 Fragment 之间共享数据,再利用 SingleLiveEvent 做总线,一个 Activity 配多个 Fragment 的写法就避免了 Activity 之间通过 Intent 传递数据的繁琐。但是 Fragment 的堆栈管理一直是一个让人头疼的问题,AAC 的 Navigation 不但完美解决了这个问题,而且还提供可视化的路由,只需拖拽一下就能生成类型安全的跳转逻辑。
Navigation
Navigation 用一个图(graph)来表示页面间的路由关系,图的节点(node)表示页面,边(edge)表示跳转关系。
页面与页面之间的连线叫 action,它可以配置进离场动画(Animations),也可以配置出栈行为(Pop Behavior),还支持 Single Top 的启动选项(Launch Options)。进离场动画和启动选项很好理解,出栈行为是一个比较强大的功能,action 箭头所指的方向表示目标页面入栈,箭头的反方向则表示目标页面出栈,而出栈的行为在 Navigation 编辑器中完全可控,我们可以指定要出栈到哪个页面,甚至可以指定目标页面是否也需要出栈:
针对页面节点,还可以定义它要接收的参数(arguments),支持默认值,从此 Fragment 之间的参数传递变得非常直观,非常安全。
看一下具体用法,首先在跳转发起页面,通过 apt 生成的跳转函数传入参数:
val actorId = getSelectedActorId()
val direction = ActorListFragmentDirections.showDetail(actorId)
findNavController().navigate(direction)
然后利用目标页面生成的*Args获取参数:
private val args: ActorDetailFragmentArgs by navArgs()
这里的 navArgs 是一个扩展函数,利用了 Kotlin 的 ReadWriteProperty。
几行代码就搞定了页面之间的跳转,而且还是可视化!从没有想过 Android 的页面跳转竟会变得如何简单,但是 Navigation 的方案并不是原创,iOS 的 Storyboard 很早就支持拖拽生成路由。当年 Android 推出 ConstraintLayout 之时,我们都认为是参考了 Storyboard 的页面拖拽,现在再配上 Navigation,从页面到跳转,一个完整的拖拽链路就形成了。平台虽然有差异化,但是使用场景一致的前提下,解决方案也就殊途同归了。
了解完了与生命周期有关的组件,接下来我们来看细节。
Paging
UI 没有办法一次性展示所有的数据,端上的系统资源(电量、内存)也有限制,不可能把所有数据都加载到内存中;而且大批量请求数据不但浪费带宽,在某些网络情况(弱网、慢网)下还会导致请求失败,所以分页是很多情景下的刚需。Github 上有各式各样的解决方案,这一次,Google 直接推出了官方的分页组件——Paging。
Paging 将分页逻辑拆解为三部分:
-
数据源 DataSource
-
数据块 PagedList
-
数据展示 PagedListAdapter
DataSource 的数据来源于后端服务或者本地数据库,并且用三个子类来表示三种分页模式:
PageKeyedDataSource - 单页数据以 page key 为标识,例如当前页的 Response 中包含了下一页的 url,这个 url 就是 page key。
ItemKeyedDataSource - 单页数据以 item key 为标识,比如下一页的请求要带当前页最后一个 item 的 id,这个 itemId 就是 item key。
PositionalDataSource - 单页数据以位置为标识,这种模式比较常见,Room 只支持这一种,因为数据库查询以 OFFSET 和 LIMIT 做分页。
PageKeyedDataSource 和 ItemKeyedDataSource 适用于内存型数据,比如直接从后端获取后需要展示的数据。PositionalDataSource 适用于本地 Room 数据或者使用 Room 做缓存的 Cache 数据。
数据流向的关系图如下所示:
LivePagedListBuilder利用DataSource.Factory和PageList.Config创建 LiveData,UI Controller 拿到数据之后交给PagedListAdapter展示到 RecyclerView。
上图表达了数据的流向,如果从 UI 层往回看,页面展示的数据存储在 PagedList 中,PagedList 只是 DataSource 的数据块(chunk),当 PagedList 需要更多数据时,DataSource 就会给更多,当 DataSource 一无所有时便会触发BoundaryCallback获取更多数据,直到数据全部展示完毕。
LivePagedListBuilder 会将PagedList包装成**LiveData**给到下游,它在整个数据交互链路中的位置
Repository 拿到 Dao 的 DataSource.Factory 之后,调用它的 toLiveData 方法并传入 PagedList.Config,然后生成一个分页的 LiveData 交给 ViewModel 层。
Paging 加上生命周期相关的架构组件解决了数据存储、数据流转和数据展示的问题。除此之外,AAC 还包括一个强大的异步任务执行器 WorkManager,它解决了任务执行的可靠性,无论 App 退出还是设备重启,交给 WorkerManager 的任务都会被执行。
WorkManager
WorkManager 虽然解决了任务执行可靠性的问题,但是它无法精确控制任务的执行时间,因为 WorkManager 要根据 OS 资源来选择执行任务。Android 自身提供了很多方案来解决后台任务执行的问题。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
写在最后
对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。
![
文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等
mages.jianshu.io/upload_images/22459598-3e1bbd9b84cc0ef9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
[外链图片转存中…(img-jkul3wgV-1711937623165)]
[外链图片转存中…(img-OFkPe7N5-1711937623166)]
文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等