依然饭特稀西

依然饭特稀西 查看完整档案

成都编辑重庆理工大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 github.com/pinguo-zhouwei 编辑
编辑

个人动态

依然饭特稀西 赞了文章 · 9月9日

Android 11 正式发布 | 开发者们的舞台已就绪

作者 / Stephanie Cuthbertson,产品管理总监

Android 11 来了!今天我们将源码推送至 Android 开源项目 (AOSP),正式为大家带来最新版本的 Android。我们在打造 Android 11 时,重点关注了三个主题: 以人为本 的沟通方式、让用户快速访问和 灵活控制 所有智能设备,以及让用户有更多方式控制设备上的数据如何共享的 隐私安全 。请阅读我们的 官方博客文章 了解详情。

对于开发者来说,Android 11 带来了大量的新功能,包括会话通知、设备和媒体控制、单次权限、增强的 5G 支持、IME 切换效果等,欢迎大家积极尝试。为了帮助您更快地推进开发工作,我们还添加了新的工具,如兼容性开关、ADB 增量安装、应用退出原因 API、数据访问审核 API、Kotlin 可空性注解等。这些工作都是为了让开发者们能喜爱 Android 11,我们非常期待在上面看到您的作品!

正式版本的 Android 11 也即将在您身边的设备上闪亮登场,Pixel 2、3、3a、4 和 4a 系列设备今天就能开始更新。请访问 Android 11 开发者网站 了解详情。

以人为本、灵活控制与隐私安全

以人为本

Android 11 致力于凸显人的要素,且善于沟通。我们重塑了您在手机上进行沟通的方式,也让操作系统能识别出那些对您来说更重要的人,让您能更快速地和他们联系。对于开发者来说,Android 11 可以帮助您在应用中实现更深入的会话和更个性化的互动体验。

  • 会话通知 会显示在通知栏顶部的专门区域,其设计更凸显联系对象,且提供了会话特定的操作,例如以 Bubbles 的形式打开聊天、在主屏幕中创建会话快捷方式,以及设置提醒。
  • Bubbles  可以让用户在手机上进行多任务切换时依然保持对话可见并且可交互。消息和聊天应用可以通过基于通知的 Bubbles API,在 Android 11 上提供这种全新体验。
  • 键盘提示整合功能 可以让自动填写应用以及 IME (输入法编辑器) 在 IME 建议栏中安全地向用户提供基于上下文的实体和字符串,使得输入更加便利。

△ Bubbles 和以人为本的会话

△ Bubbles 和以人为本的会话

灵活控制

Android 11 让用户们得以快速访问所有的智能设备,并集中控制它们。开发者们则可以通过全新的 API 来帮助用户控制智能设备和管理媒体播放:

  • 设备控制 ( Device Controls ) 让用户得以更快、更轻松地访问和控制他们连接的设备。只需长按电源按钮就可以调出设备控制菜单,一站式完成设备控制。应用也可以通过新的 API 出现在这个控制菜单中。详细信息请访问 官方文档
  • 媒体控制 ( Media Controls ) 让用户得以更快捷地切换音频和视频内容的播放设备——不论是耳机、麦克风还是电视。详细信息请访问官方文档


△ 设备控制和媒体控制

隐私安全

在 Android 11 中,我们为用户带来了更高的掌控能力,让他们能更好地管理敏感权限。我们还会通过更快速的更新来持续确保设备安全。

单次授权  - 用户现在可以授予应用一次性的权限来访问设备的麦克风、摄像头或者位置信息。应用下次被使用时需要再次请求权限。详细信息请访问 官方文档

Android 11 中的单次授权对话框

△ Android 11 中的单次授权对话框

后台位置  - 想访问后台位置信息现在需要用户在授予运行时权限外进行更进一步的操作。如果您的应用需要访问后台位置信息,系统会要求您必须先请求前台位置权限。您可以通过 单独的权限申请 来进一步要求访问后台位置信息,系统会将用户带到设置页面 (Settings) 中完成授权操作。

另外需要注意的是,我们在今年二月宣布,Google Play 开发者需要获得批准后才可以让应用在后台访问位置信息,以防止滥用。现在我们为开发者提供更长的时间来做出修改,在 2021 年之前我们不会强行要求现有的应用遵守本政策。详细信息请访问 官方文档

权限自动重置  - 如果用户在很长一段时间里未使用某应用,Android 11 将 "自动重置" 所有与该应用关联的运行时权限并通知用户。在用户下次使用该应用时,应用可以再次请求权限。详细信息请访问 官方文档

分区存储  - 我们一直在努力更好地保护外部存储上的应用和用户数据,还加入了更多的改进以便让开发者更轻松地进行迁移。详细信息请访问 官方文档

Google Play 系统更新  - 自去年发布以来,Google Play 系统更新让我们能更快速地更新操作系统核心组件,并覆盖 Android 生态系统中的众多设备。在 Android 11 中,可更新的模块数量增加了一倍有余,新增的 12 个可更新模块,为用户和开发者带来更好的隐私性、安全性和一致性。

BiometricPrompt API  - 开发者现在可以通过 BiometricPrompt API 来指定其应用所需的生物识别身份验证强度类型,用来解锁或者访问应用中的敏感内容。为了向下兼容,我们也将这些功能加入到了 Jetpack Biometric 开发库 中。随着工作的进展,我们会为大家带来进一步的更新。

身份认证 API ( Identity Credential API ) - 这个 API 会带来全新的使用场景,支持包括驾驶执照、国民身份证和数字身份证。我们正在与各政府机构和行业伙伴合作,以确保 Android 11 为数字化身份认证体验做好准备。

这里 阅读有关 Android 11 隐私功能的详细信息。

实用创新

更强的 5G 支持  - Android 11 可以让开发者利用 5G 网络更快的速度和更低的延迟。您可以知晓用户何时 连接到 5G 网络,查看 连接是否处于计费状态,并且 估测连接的带宽。为了帮助您即刻打造 5G 体验,我们也在 Android Emulator 中加入了 5G 支持。请访问 5G 开发者网页,了解如何在 Android 上使用 5G 功能。

△ 将高速体验带出家门,5G 可以让您的随行移动体验更加流畅,让您随时与周边环境、朋友、家人互动并满足工作的需要

△ 将高速体验带出家门,5G 可以让您的随行移动体验更加流畅,让您随时与周边环境、朋友、家人互动并满足工作的需要

新的屏幕类型  - 设备厂商们也在持续进行创新,将新的屏幕形态投入市场,包括挖孔屏和瀑布屏。Android 11 已经在平台中增加了对这些屏幕的支持,并提供了相应的 API 方便您优化应用。您可以通过现有的 Display Cutout API 来管理挖孔屏和瀑布屏。您可以通过设置 新的窗口布局属性 来使用整个瀑布屏,并通过 瀑布屏边衬区) (insets) API 来管理屏幕边缘附近的互动。

呼叫过滤服务  - Android 11 可以帮助呼叫过滤应用更好地管理骚扰电话。应用在呼叫详细信息中可以获取来电的 STIR/SHAKEN) 验证状态 (这个标准可以防止来电 ID 欺诈),并能报告拒接来电的原因。应用还可以自定义系统提供的 呼叫后屏幕 (post call screen),方便用户执行诸如 "将呼叫方标记为骚扰电话" 或 "添加到联系人" 之类的操作。

优化与品质

操作系统弹性  - 在 Android 11 中,我们通过对内存回收操作 (比如根据 RSS HWM 阈值强制用户无法感知的进程重启) 进行微调,使操作系统整体更具动态性和弹性。另外,为了改善性能和内存的使用,Android 11 还增加了 Binder 缓存,通过缓存那些检索相对静态数据的系统服务,优化了使用率高的 IPC 调用。Binder 缓存还通过减少 CPU 时间延长了电池寿命。

同步 IME 切换效果  - 这是一组全新的 API,让您可以在 IME (输入法编辑器,也叫软键盘) 和系统栏进出屏幕时同步调整应用中的内容,从而更轻松地创建出自然、直观、流畅的 IME 切换效果。为了确保切换时做到逐帧精确,新的 WindowInsetsAnimation.Callback API 会在系统栏或 IME 移动时逐帧告知应用边衬区的变化。此外,您可以通过新的 WindowInsetsAnimationController API 控制系统 UI,包括系统栏、IME、沉浸模式等。阅读 这篇博文 了解更多。


△ 左侧示意: 通过边衬区动画监听器实现 IME 同步切换效果 右侧示意: 通过 WindowInsetsAnimationController 实现应用驱动的 IME 体验

HEIF 动画可绘制对象  - ImageDecoder API 现在允许您解码和渲染存储在 HEIF 文件中的图像序列动画,方便您引入高品质的素材,同时最大程度地减少流量消耗和 APK 尺寸。相对于 GIF 动画,HEIF 图像序列可以显著减小文件尺寸。

原生图像解码器  - 应用可以使用新的 NDK API 来通过原生代码解码和编码图像 (如 JPEG、PNG、WebP),以便进行图形或后期处理,而且因为您无需捆绑外部代码库,从而得以保持较小的 APK 尺寸。原生解码器还可以从 Android 持续的平台安全更新中获益。我们提供了 NDK 样例代码 作为使用参考。

MediaCodec 中的低延迟视频解码  - 低延迟视频对于 Stadia 等实时视频流应用和服务至关重要。支持低延迟播放的视频编解码器会在解码开始后尽快返回流的第一帧。应用可以使用新 API 来针对特定编解码器 检查) 和 配置 低延迟播放。

可变刷新率  - 应用和游戏现在可以通过 新的 API) 为其窗口设置首选帧率。大多数 Android 设备以 60Hz 的刷新率更新屏幕,但是某些设备支持多种刷新率,例如 90Hz 和 60Hz,并可在运行时切换。在这些设备上,系统会基于首选帧率来为应用选择最佳刷新率。您可以通过 SDK 和 NDK 来使用该 API。详细信息请访问 官方文档

动态资源加载器  - Android 11 提供了一个新的公开 API 来让应用在运行时动态加载资源和素材。通过 Resource Loader 框架,您可以在应用或游戏中包含一套基本资源,然后在运行时根据需要加载其他资源,或更改已加载的资源。

Neural Networks API (NNAPI) 1.3  - 我们持续增加算子和控制,以支持 Android 设备上的机器学习。为了优化常见的使用场景,NNAPI 1.3 增加了优先级和超时、内存域 (memory domains) 以及异步指令队列的 API。新的算子支持包含有符号整数非对称量化以及分支和循环的高级模型,hard-swish 算子则可以用于加速下一代设备上视觉模型 (如 MobileNetV3)。

开发者体验

应用兼容性工具  - 我们努力将大多数 Android 11 行为变更设置为可选择开启,从而最大限度地减少对兼容性带来的影响,除非您将应用的 targetSdkVersion 设置为 30,否则这些变更不会生效。如果您是通过 Google Play 发布应用,则有一年多的时间来选择支持这些变更,但我们建议尽早开始测试。为了帮助您进行测试,Android 11 允许您单独开启或关闭其中的许多变更。详细信息请访问 官方文档

应用退出原因  - 了解应用退出的原因以及当时的状态十分重要——包括应用所在的设备类型、内存配置和运行场景。Android 11 通过 退出原因 API 让这个事情变得更加容易: 您可以使用该 API 来查看应用最近退出的 详细信息

数据访问审核  - 数据访问审核可以让您更好地了解自己的应用访问用户数据的情况,以及访问来自的用户流程。例如,它能帮您识别无意的私有数据访问,不论其来自于您自己的代码还是其他 SDK。详细信息请访问 官方文档

ADB 增量安装 ( ADB Incremental ) - 在开发过程中使用 ADB (Android Debug Bridge) 安装体积较大的 APK 可能会拖慢速度,影响您的工作效率,对 Android 游戏开发者而言尤其如此。Android 11 带来了 ADB Incremental,现在从开发机向 Android 11 设备上部署大型 APK (2GB 以上) 的速度可以提高 10 倍之多。详细信息请访问 官方文档

Kotlin 可空性注解  - Android 11 为公共 API 中的更多方法增加了可空性注解。而且,它将一些现有的注解从警告升级为错误。这可以帮助您在构建时就发现问题,不用等到运行时才出错。阅读 此文 了解更多。

让您的应用为 Android 11 做好准备

Android 11 即将抵达用户手中,现在是时候 完成您的兼容性测试并发布更新 了。

请首先关注 针对所有应用的行为变更:

下面是首先需要关注的行为变更 (无论您应用的 targetSdkVersion 是多少): 

  • 单次权限  - 现在,用户可以为位置信息、设备麦克风和摄像头授予单次使用权限。详细信息请访问 官方文档
  • 外部存储访问权限  - 应用无法再访问外部存储空间中其他应用的文件。详细信息请访问 官方文档
  • Scudo Hardened Allocator  - 现在它是应用内原生代码的堆内存分配器。详细信息请访问 官方文档
  • 文件描述符排查器  - 此功能现在默认启用,以检测应用原生代码的文件描述符处理错误。详细信息请访问 官方文档:

Android 11 中还有许多 可选择支持的行为变更 - 您的应用如果针对新平台发布,才会受到影响。我们建议在您发布应用的兼容版本后尽快评估这些变更。有关兼容性测试和工具的更多信息,请查看 Android 11 兼容性相关的资源,并访问 Android 11 开发者网站 了解技术细节。

使用新功能和 API 改进您的应用

准备就绪后,请深入研究 Android 11 并了解您可以使用的 新功能和 API。下面是一些您可以优先考虑的重点功能。

我们推荐所有应用支持这些功能:

  • 深色主题  (自 Android 10 开始支持) - 通过添加 Dark Theme (深色主题) 或启用 Force Dark,确保为启用全系统深色主题的用户提供一致的体验。
  • 手势导航  (自 Android 10 开始支持) - 请支持手势导航,包括提供边到边的沉浸式体验,以及确保自定义手势与默认手势配合良好。详细信息请访问 官方文档:
  • 共享快捷方式  (自 Android 10 开始支持) - 想要接收共享数据的应用应该使用 共享快捷方式 API 来创建共享目标。想要发送共享数据的应用应确保使用 Android Sharesheet
  • 同步 IME 切换效果  - 使用新的 WindowInsets 和相关 API 为用户提供流畅的切换效果。详细信息请阅读 这篇博文
  • 新的屏幕类型  - 对挖孔屏或瀑布屏设备,请确保根据需要针对这些屏幕测试和调整您的内容。详细信息请访问 官方文档

我们还推荐这些功能,如果它们和您的应用体验契合的话:

  • 会话  - 消息和通信应用可以通过提供长效 共享快捷方式 和在通知中呈现对会话来融入用户的对话体验。详细信息请访问 官方文档
  • 聊天气泡 ( Bubbles ) - Bubbles 可以在多任务切换时依然保持对话可见及可用。应用通过基于通知的 Bubbles API 来实现此功能。
  • 5G  - 如果您的应用或内容可以利用 5G 更快的速度和更低的延迟,请参考我们的 开发者资源,开始构建 5G 体验。
  • 设备控制  - 如果您的应用支持外部智能设备,请确保这些设备可以从新的 Android 11 设备控制菜单访问。详细信息请访问 官方文档
  • 媒体控制  - 对于媒体应用,我们建议支持 Android 11 媒体控制,这样用户就可以从快速设置 (Quick Settings) 菜单中管理媒体播放。详细信息请访问 官方文档

您可以前往 developer.android.google.cn/11 了解更多有关 Android 11 功能的信息。

Android 11 即将出现在您身边的设备上!

Android 11 将从今天开始在选定的 Pixel、一加、小米、OPPO 和 realme 手机上陆续推出,未来几个月将有更多合作伙伴推出以及升级设备。如果您拥有 Pixel 2、3、3a、4 或 4a 系列手机,包括那些参加了今年 Beta 测试计划的手机,请关注即将到来的 OTA 更新!

Pixel 设备的 Android 11 出厂系统映像也可以通过 Android Flash Tool 获得,您也可以在 这里 下载。和以往一样,您可以通过 Android Studio 中的 SDK Manager 获得最新的 Android 模拟器系统映像。如果您想在其他支持 Treble 的兼容设备上进行更广泛的测试,可以在 这里 获取通用系统映像 (GSI)。

如果您需要 Android 11 的源代码,可以在 Android 开源项目 repo 的 Android 11 分支下的 这个地址 获取。

下一步是什么?

我们很快会关闭预览版问题反馈通道,并清除针对开发者预览版或 Beta 版的开放 Bug,如果您再次发现了曾经在预览版时期提交过的问题,请在 AOSP 问题反馈表单中针对 Android 11 提交新的错误报告,希望您继续为我们提供反馈意见!

再次感谢今年参与预览计划的众多开发者和早期体验者!大家为我们提供了非常好的反馈,助力我们打磨产品。正是大家提交的数以千计的问题报告将 Android 11 打造成了一个更好、更适合用户的平台。

期待在 Android 11 上看到大家的应用!

查看原文

赞 7 收藏 0 评论 0

依然饭特稀西 赞了文章 · 9月6日

Android coder 需要理解的注解、反射和动态代理

注解我们经常使用它,很多框架也提供了很多注解给我们使用,如 ARouter@Route(path = "/test/activity")butterknife@BindView(R.id.user) EditText username; 等,但是,你有没有自定义过注解,写过自己的注解处理器呢?反射听起来很高大上,但是实际上你真的了解他之后,只是一些API的调用而已;动态代理其实只是在静态代理(代理模式)基础上使用了反射技术;本篇文章将带领大家对注解、反射及动态代理有更清晰的认知。


本篇文章的示例代码放在 Github 上,所有知识点,如图:

注解

注解(Annotations),元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。

注解有多种用途,例如:

  • 为编译器提供信息:编译器可以使用注解来检查错误或抑制警告
  • 编译或部署时处理:可以生成代码、XML、文件等
  • 运行时处理:注解可以在运行时检查

注解的格式

注解的格式如下:

@Persilee
class MyClass { ... }

注解已 @ 开头后面跟上内容,注解可以包含元素,例如:

@Persilee(id=666, value = "lsy")
class MyClass { ... }

如果,只有一个 value 元素,则可以省略该名称,如果,没有元素,则可以省略括号,例如

@Persilee("lsy") // 只有一个 value 元素
class MyClass { ... }

@Persilee // 没有元素
class MyClass { ... }

如果,注解有相同的类型,则是重复注解,如

@Persilee("lsy")
@Persilee("zimu")
class MyClass { ... }

注解声明

注解的定义类似于接口的定义,在关键字 interface 前加上 @,如:

@interface Persilee {
    int id();
    String value();
}

注解类型

int id()String value() 是注解类型(annotation type),它们也可以定义可选的默认值,如:

@interface Persilee {
    int id();
    String value() default "lsy";
}

在使用注解时,如果定义的注解的注解类型没有默认值,则必须进行赋值,如:

@Persilee(id = 666) // id 必须要赋值,如,@Persilee 会提示 id 必须赋值
class MyClass { ... }

元注解

在注解上面的注解称为元注解(meta-annotations),如

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
@interface Persilee {
    int id();
    String value() default "lsy";
}

java.lang.annotation 中定义了几种元注解类型(常使用的是 @Retention、@Target),如

@Retention 指定注解的存储方式,我们由 RetentionPolicy.java (是一个枚举)可知,如:

public enum RetentionPolicy {
    SOURCE, // 标记的注解仅保留在源级别中,并被编译器忽略。
    CLASS, // 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
    RUNTIME // 标记的注解由 JVM 保留,因此运行时环境可以使用它。
}

@Target 指定注解可以使用的范围,我们由 ElementType.java (是一个枚举)可知使用范围,如下:

public enum ElementType {
    TYPE, // 类
    FIELD, // 字段或属性
    METHOD, // 方法
    PARAMETER, // 参数
    CONSTRUCTOR, // 构造方法
    LOCAL_VARIABLE, // 局部变量
    ANNOTATION_TYPE, // 也可以使用在注解上
    PACKAGE, // 包
    TYPE_PARAMETER, // 类型参数
    TYPE_USE // 任何类型
}

对于 TYPE_PARAMETER (类型参数) 、 TYPE_USE (任何类型名称) 可能不是很好理解,如果把 Target 设置成 @Target({ElementType.TYPE_PARAMETER}),表示可以使用在泛型(上篇文章有介绍过泛型)的类型参数上,如:

public class TypeParameterClass<@Persilee T> {
    public <@Persilee T> T foo(T t) {
        return null;
    }
}

如果把 Target 设置成 @Target({ElementType.TYPE_USE}),表示可以使用在任何类型上,如:

TypeParameterClass<@Persilee String> typeParameterClass = new TypeParameterClass<>();
@Persilee String text = (@Persilee String)new Object();

@Documented 注解表示使用了指定的注解,将使用 Javadoc 工具记录这些元素。

@Inherited 注解表示注解类型可以从超类继承。

@Repeatable 注解表明标记的注解可以多次应用于同一声明或类型使用。

注解应用场景

根据 @Retention 元注解定义的存储方式,注解一般可以使用在以下3种场景中,如:

级别技术说明
源码APT在编译期能获取注解与注解声明的类和类中所有成员信息,一般用于生成额外的辅助类。
字节码      字节码增强      在编译出Class后,通过修改Class数据以实现修改代码逻辑目的,对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
运行时反射在程序运行时,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判断。

小案例(使用注解实现语法检查)

我们定义一个 weekDay 字段,类型是 WeekDay 枚举类型,方便我们设置枚举中指定的值,如:

class WeekDayDemo {

    private static WeekDay weekDay;

    enum WeekDay {
        SATURDAY,SUNDAY
    }

    public static WeekDay getWeekDay() {
        return weekDay;
    }

    public static void setWeekDay(WeekDay weekDay) {
        WeekDayDemo.weekDay = weekDay;
    }

    public static void main(String[] args) {
        setWeekDay(WeekDay.SATURDAY);
        System.out.println(getWeekDay());
    }
}

众所周知,在 Java 中枚举的实质是特殊的静态成员变量,在运行时候,所有的枚举会作为单例加载到内存中,非常消耗内存,那么,有没有什么优化的方案呢,在此,我们使用注解来取代枚举。

我们使用常量和 @intDef (语法检查)元注解去代替枚举,如:

class IntdefDemo {

    private static final int SATURDAY = 0;
    private static final int SUNDAY = 1;

    private static int weekDay;

    @IntDef({SATURDAY, SUNDAY})
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    @interface WeekDay { //自定义一个 WeekDay 注解

    }

    public static void setWeekDay(@WeekDay int weekDay) { // 使用 WeekDay 注解限制参数类型
        IntdefDemo.weekDay = weekDay;
    }

    public static void main(String[] args) {
        setWeekDay(SATURDAY); // 只能 传入 SATURDAY, SUNDAY
    }
}

APT注解处理器

APT(Annotation Processor Tools) 注解处理器,用于处理注解,编写好的 Java 文件,需要经过 Javac 的编译,编译为虚拟机能够加载的字节码(Class)文件,注解处理器是 Javac 自带的一个工具,用来在编译时期处理注解信息。

上文中我们已自定义好了 @Persilee 注解,下面我们来编写一个简单的注解处理器来处理 @Persilee 注解,我们可以新建一个 Java 的 Module,创建一个 PersileeProcessor 的类,如:

@SupportedAnnotationTypes("net.lishaoy.anreprdemo.Persilee")  //指定要处理的注解
public class PersileeProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Messager messager = processingEnv.getMessager(); //
        messager.printMessage(Diagnostic.Kind.NOTE, "APT working ...");
        for (TypeElement typeElement: set) {
            messager.printMessage(Diagnostic.Kind.NOTE,"===>" + typeElement.getQualifiedName());
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(typeElement);
            for (Element element: elements) {
                messager.printMessage(Diagnostic.Kind.NOTE,"===>" + element.getSimpleName());
            }
        }

        return false;
    }
}

然后,在 main 目录下新建 resources 目录,如图:

这个目录结构是规定死的,必须这样写,然后在 javax.annotation.processing.Processor 文件里注册需要处理的注解处理器,如

net.lishaoy.aptlib.PersileeProcessor

最后,在 appbuild.gradle 文件引入模块,如

dependencies {
  ...

  annotationProcessor project(':aptlib')
}

在你 Build 工程时候,会在 Task :app:compileDebugJavaWithJavac 任务打印我们在注解处理程序的日志信息,如:

注: APT working ...
注: ===>net.lishaoy.anreprdemo.Persilee
注: ===>MainActivity

因为,我们只在 MainActivity 中使用了 @Persilee 注解,如下:

@Persilee(id = 666, value = "lsy")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }
}

反射

一般情况下,我们使用某个类时必定知道它是什么类,用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

Cook cook = new Cook(); // 实例化一个对象,标准用法
cook.cookService("🍅");

反射是一开始并不知道初始化的类对象是什么,也不能使用 new 关键字来创建对象,反射是在运行的时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,调用对应的方法。

Java 反射机制主要提供了以下功能:

  • 在运行时构造任意一个类的对象
  • 在运行时获取或修改任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法(属性)

Class类

Class是一个类,封装了当前对象所对应的类的信息,我们写的每一个类都可以看成一个对象,是 java.lang.Class 类的对象,Class是用来描述类的类。

获得Class对象

Class对象的获取有3种方式,如下:

  • 通过类名获取 类名.class
  • 通过对象获取 对象名.getClass()
  • 通过全类名获取 Class.forName(全类名)
Cook cook = new Cook();
Class cookClass = Cook.class;
Class cookClass1 = cook.getClass();
Class cookClass2 = Class.forName("net.lishaoy.reflectdemo.Cook");

创建实例

我们可以通过反射来生成对象的实例,如:

Class cookClass = Cook.class;
Cook cook1 = (Cook) cookClass.newInstance();

获取构造器

获取构造器的方法有,如下:

  • Constructor getConstructor(Class[] params):获得使用特殊的参数类型的public构造函数(包括父类)
  • Constructor[] getConstructors():获得类的所有公共构造函数
  • Constructor getDeclaredConstructor(Class[] params):获得使用特定参数类型的构造函数(包括私有)
  • Constructor[] getDeclaredConstructors():获得类的所有构造函数(与接入级别无关)

我们来新建一个 Person ,以便我们的演示,如:

public class Person {

    public String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
        super();
    }

    public String getName() {
        System.out.println("get name: " + name);
        return name;
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("set name: " + this.name);
    }

    public int getAge() {
        System.out.println("get age: " + age);
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        System.out.println("set age: " + this.age);
    }

    private void privateMethod(){
        System.out.println("the private method!");
    }
}

很常规的一个类,里面有私有的属性和方法。

下面,我们新建一个 GetConstructor 的类来演示,获取构造器方法如何使用,如:

class GetConstructor {

    public static void main(String[] args) throws
            ClassNotFoundException,
            NoSuchMethodException,
            IllegalAccessException,
            InvocationTargetException,
            InstantiationException {

        String className = "net.lishaoy.reflectdemo.entity.Person";
        Class<Person> personClass = (Class<Person>) Class.forName(className);

        //获取全部的constructor对象
        Constructor<?>[] constructors = personClass.getConstructors();
        for (Constructor<?> constructor: constructors) {
            System.out.println("获取全部的constructor对象: " + constructor);
        }

        //获取某一个constructor对象
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        System.out.println("获取某一个constructor对象: " + constructor);

        //调用构造器的 newInstance() 方法创建对象
        Person person = constructor.newInstance("lsy", 66);
        System.out.println(person.getName() + ", " + person.getAge() );
    }

}

输出结果,如下:

获取全部的constructor对象: public net.lishaoy.reflectdemo.entity.Person(java.lang.String,int)
获取全部的constructor对象: public net.lishaoy.reflectdemo.entity.Person()
获取某一个constructor对象: public net.lishaoy.reflectdemo.entity.Person(java.lang.String,int)
lsy, 66

获取方法

获取方法的方法有,如下:

  • Method getMethod(String name, Class[] params):使用特定的参数类型,获得命名的公共方法
  • Method[] getMethods():获得类的所有公共方法
  • Method getDeclaredMethod(String name, Class[] params):使用特写的参数类型,获得类声明的命名的方法
  • Method[] getDeclaredMethods():获得类声明的所有方法

我们新创建一个 GetMethod 来演示如何来获取和调用方法,如:

class GetMethod {

    public static void main(String[] args) throws
            ClassNotFoundException,
            NoSuchMethodException,
            IllegalAccessException,
            InstantiationException,
            InvocationTargetException {

        Class<?> aClass = Class.forName("net.lishaoy.reflectdemo.entity.Person");

        //获取所有的public方法(包含从父类继承的方法)
        Method[] methods = aClass.getMethods();
        for (Method method: methods) {
            System.out.println("获取所有public方法: " + method.getName() + "()");
        }

        System.out.println("===========================");

        //获取所有方法(不包含父类方法)
        methods = aClass.getDeclaredMethods();
        for (Method method: methods) {
            System.out.println("获取所有方法: " + method.getName() + "()");
        }

        System.out.println("===========================");

        //获取指定的方法
        Method method = aClass.getDeclaredMethod("setAge", int.class);
        System.out.println("获取指定的方法:" + method);

        //调用方法
        Object instance = aClass.newInstance();
        method.invoke(instance, 66);

        //调用私有方法
        method = aClass.getDeclaredMethod("privateMethod");
        method.setAccessible(true); // 需要调用此方法且设置为 true
        method.invoke(instance);

    }

}

运行结果,如下:

获取所有public方法: getName()
获取所有public方法: setName()
获取所有public方法: setAge()
获取所有public方法: getAge()
获取所有public方法: wait()
获取所有public方法: wait()
获取所有public方法: wait()
获取所有public方法: equals()
获取所有public方法: toString()
获取所有public方法: hashCode()
获取所有public方法: getClass()
获取所有public方法: notify()
获取所有public方法: notifyAll()
===========================
获取所有方法: getName()
获取所有方法: setName()
获取所有方法: setAge()
获取所有方法: privateMethod()
获取所有方法: getAge()
===========================
获取指定的方法:public void net.lishaoy.reflectdemo.entity.Person.setAge(int)
set age: 66
the private method!

BUILD SUCCESSFUL in 395ms

获取成员变量

获取成员变量的方法有,如下:

  • Field getField(String name):获得命名的公共字段
  • Field[] getFields():获得类的所有公共字段
  • Field getDeclaredField(String name):获得类声明的命名的字段
  • Field[] getDeclaredFields():获得类声明的所有字段

我们再来新建一个 GetField 的类来演示如何获取成员变量,如下:

class GetField {

    public static void main(String[] args) throws
            ClassNotFoundException,
            NoSuchFieldException,
            IllegalAccessException,
            InstantiationException {

        Class<?> aClass = Class.forName("net.lishaoy.reflectdemo.entity.Person");

        // 获取所有字段(不包含父类字段)
        Field[] fields = aClass.getDeclaredFields();
        for (Field field: fields) {
            System.out.println("获取所有字段: " + field.getName());
        }

        System.out.println("================");

        // 获取指定字段
        Field name = aClass.getDeclaredField("name");
        System.out.println("获取指定字段: " + name.getName());

        // 设置指定字段的值
        Object instance = aClass.newInstance();
        name.set(instance, "per");

        // 获取指定字段的值
        Object o = name.get(instance);
        System.out.println("获取指定字段的值: " + o);

        // 设置和获取私有字段的值
        Field age = aClass.getDeclaredField("age");
        age.setAccessible(true); // 需要调用此方法且设置为 true
        age.set(instance, 66);
        System.out.println("获取私有字段的值: " + age.get(instance));

    }

}

运行结果,如下:

获取所有字段: name
获取所有字段: age
================
获取指定字段: name
获取指定字段的值: per
获取私有字段的值: 66

BUILD SUCCESSFUL in 395ms

使用注解和反射实现自动findViewById(案例)

我们已经对注解和反射有了更清晰的认知,下面我们通过一个小案例来巩固我们的学习:使用注解和反射完成类似 butterknife 的自动 findViewById 的功能。

新建一个空的 Android 工程,在工程目录下新建 inject 目录,在此目录下新建一个 InjectView 的类和 BindView 的自定义注解,如:

创建InjectView

InjectView 类通过反射完成 findViewById 功能:

public class InjectView {

    public static void init(Activity activity) {
        // 获取 activity 的 class 对象
        Class<? extends Activity> aClass = activity.getClass();
        // 获取 activity 的所以成员变量
        Field[] declaredFields = aClass.getDeclaredFields();
        // 变量所以成员变量
        for (Field field: declaredFields) {
            // 判断属性是否加上了 @BindView 注解
            if(field.isAnnotationPresent(BindView.class)){
                // 获取注解 BindView 对象
                BindView bindView = field.getAnnotation(BindView.class);
                // 获取注解类型元素 id
                int id = bindView.value();
                // 通过资源 id 找到对应的 view
                View view = activity.findViewById(id);
                // 设置可以访问私有字段
                field.setAccessible(true);
                try {
                    // 给字段赋值
                    field.set(activity,view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

创建@BindView注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value(); // @IdRes 只能传 id 资源
}

使用@BindView注解

MainActivity 里使用 @BindView 注解,如:

public class MainActivity extends AppCompatActivity {

    // 使用注解
    @BindView(R.id.text_view)
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        // 初始化 InjectView,完成自动 findViewById 功能
        InjectView.init(this);
        // 测试 R.id.text_view 是否自动赋值给 textView
        textView.setText("通过 @BindView 注解自动完成 findViewById");
    }
}

运行结果,如图:

是不是很简单,一个类就完成了自动 findViewById 的功能。

动态代理

在了解动态代理之前,我们先来回顾下静态代理。

静态代理

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,如,我们生活中常见的中介。

代理模式一般会有3个角色,如图:

  • 抽象角色:指代理角色和真实角色对外提供的公共方法,一般为一个接口
  • 真实角色:需要实现抽象角色接口,定义了真实角色所要实现的业务逻辑,以便供代理角色调用
  • 代理角色:需要实现抽象角色接口,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作

为什么要使用代理模式

  • 可以间接访问对象,防止直接访问对象来的不必要复杂性
  • 通过代理对象对访问进行控制

静态代理案例

场景如下:

小明可以在某网站上购买国内的东西,但是,不能买海外的东西,于是,他找了海外代购帮他买东西。

如何用代码描述呢?根据代理模式的3个角色,我们分别定义1个接口2个类,如:OrderService 接口(抽象角色)、ImplJapanOrderService 类(真实角色)、ProxyJapanOrder 类(代理角色)

OrderService 接口(抽象角色),代码如下:

public interface OrderService {
    int saveOrder();
}

ImplJapanOrderService 类(真实角色),代码如下:

// 实现抽象角色接口
public class ImplJapanOrderService implements OrderService {
    @Override
    public int saveOrder() {
        System.out.println("下单成功,订单号为:888888");
        return 888888;
    }
}

ProxyJapanOrder 类(代理角色),代码如下:

// 实现抽象角色接口
public class ProxyJapanOrder implements OrderService {

    private OrderService orderService; // 持有真实角色

    public OrderService getOrderService() {
        return orderService;
    }

    public void setOrderService(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public int saveOrder() {
        System.out.print("日本代购订单,");
        return orderService.saveOrder(); // 调用真实角色的行为方法
    }
}

在创建一个 Client 类来测试我们的代码,如下:

public class Client {

    public static void main(String[] args) {
        // 日本代购订单
        OrderService orderJapan = new ImplJapanOrderService();
        ProxyJapanOrder proxyJapanOrder = new ProxyJapanOrder();
        proxyJapanOrder.setOrderService(orderJapan);
        proxyJapanOrder.saveOrder();
    }
}

运行结果,如下:

日本代购订单,下单成功,订单号为:888888

BUILD SUCCESSFUL in 1s

如果,需要购买韩国的东西,需要新增一个 ImplKoreaOrderService 类(韩国服务商) 和 ProxyKoreaOrder 类(韩国代理),如还需要购买其他国家的东西,需要新增不同的类,则会出现静态代理对象量多、代码量大,从而导致代码复杂,可维护性差的问题,如是,我们需要使用动态代理。

动态代理

动态代理是在运行时才创建代理类和其实例,因此,我们可以传不同的真实角色,实现一个代理类完成多个真实角色的行为方法,当然,其效率比静态代理低。那么如何实现动态代理呢,JDK已为我们提供了 Proxy 类 和 InvocationHandler 接口来完成这件事情。

我们来创建一个 ProxyDynamicOrder 类(动态代理类),代码如下:

public class ProxyDynamicOrder implements InvocationHandler {

    private Object orderService; // 持有真实角色

    public Object getOrderService() {
        return orderService;
    }

    public void setOrderService(Object orderService) {
        this.orderService = orderService;
    }
    // 通过 Proxy 动态创建真实角色
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(
                orderService.getClass().getClassLoader(),
                orderService.getClass().getInterfaces(),
                this
                );
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        return method.invoke(orderService, objects); // 通过反射执行真实角色的行为方法
    }
}

在来看看,Client 类里如何调用,代码如下:

public class Client {

    public static void main(String[] args) {

        // 静态代理模式
        // 国内订单
        OrderService order = new ImplOrderService();
        order.saveOrder();
        // 日本代购订单
        OrderService orderJapan = new ImplJapanOrderService();
        ProxyJapanOrder proxyJapanOrder = new ProxyJapanOrder();
        proxyJapanOrder.setOrderService(orderJapan);
        proxyJapanOrder.saveOrder();
        // 韩国代购订单
        OrderService orderKorea = new ImplKoreaOrderService();
        ProxyKoreaOrder proxyKoreaOrder = new ProxyKoreaOrder();
        proxyKoreaOrder.setOrderService(orderKorea);
        proxyKoreaOrder.saveOrder();

        // 动态代理模式
        // 国内订单
        ProxyDynamicOrder proxyDynamicOrder = new ProxyDynamicOrder();
        OrderService orderService = new ImplOrderService();
        proxyDynamicOrder.setOrderService(orderService);
        OrderService orderService1 = (OrderService) proxyDynamicOrder.getProxyInstance();
        orderService1.saveOrder();

        // 日本代购订单
        OrderService japanOrderService = new ImplJapanOrderService();
        proxyDynamicOrder.setOrderService(japanOrderService);
        OrderService japanOrderService1 = (OrderService) proxyDynamicOrder.getProxyInstance();
        japanOrderService1.saveOrder();

        // 韩国代购订单
        OrderService koreaOrderService = new ImplKoreaOrderService();
        proxyDynamicOrder.setOrderService(koreaOrderService);
        OrderService koreaOrderService1 = (OrderService) proxyDynamicOrder.getProxyInstance();
        koreaOrderService1.saveOrder();

        // 生成动态代理生成的class文件
        //ProxyUtil.generateClassFile(koreaOrderService.getClass(), koreaOrderService1.getClass().getSimpleName());

    }
}

运行结果,如下:

下单成功,订单号为:666666
日本代购订单,下单成功,订单号为:888888
韩国代购订单,下单成功,订单号为:666888
下单成功,订单号为:666666
下单成功,订单号为:888888
下单成功,订单号为:666888

BUILD SUCCESSFUL in 1s

只需要一个 ProxyDynamicOrder 代理类即可完成 ImplOrderServiceImplJapanOrderServiceImplKoreaOrderService 真实角色提供的服务。

动态代理原理

我们在 proxyDynamicOrder.getProxyInstance() 代码上打个断点,通过调试模式发现,如图:

代理类的名字是 $Proxy0@507,为什么是这个名字,我们在编译后的目录里也找不到 $Proxy0@507 类文件,如图:

我们通过查看 Proxy.newProxyInstance 方法源码,可知,如:

@CallerSensitive
public static Object newProxyInstance(ClassLoader var0, Class<?>[] var1, InvocationHandler var2) throws IllegalArgumentException {
    Objects.requireNonNull(var2);
    Class[] var3 = (Class[])var1.clone();
    SecurityManager var4 = System.getSecurityManager();
    if (var4 != null) {
        checkProxyAccess(Reflection.getCallerClass(), var0, var3);
    }
    // 获取代理类的 class 对象
    Class var5 = getProxyClass0(var0, var3);

    try {
        if (var4 != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), var5);
        }
        // 获取代理类的构造器
        final Constructor var6 = var5.getConstructor(constructorParams);
        if (!Modifier.isPublic(var5.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    var6.setAccessible(true);
                    return null;
                }
            });
        }
        // 创建代理类的示例
        return var6.newInstance(var2);
    } catch (InstantiationException | IllegalAccessException var8) {
        throw new InternalError(var8.toString(), var8);
    } catch (InvocationTargetException var9) {
        Throwable var7 = var9.getCause();
        if (var7 instanceof RuntimeException) {
            throw (RuntimeException)var7;
        } else {
            throw new InternalError(var7.toString(), var7);
        }
    } catch (NoSuchMethodException var10) {
        throw new InternalError(var10.toString(), var10);
    }
}

然后,跟进 getProxyClass0(var0, var3) 看看是如何获取代理类的 class 对象的,点击进入,如下:

private static Class<?> getProxyClass0(ClassLoader var0, Class<?>... var1) {
    if (var1.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    } else {
        // 缓存了代理类的 class 对象
        return (Class)proxyClassCache.get(var0, var1);
    }
}

然后,我们来看看这个 var1 是个什么东西,我们往上找了找,果然发现,如下:

// var1 就是我们实现的 InvocationHandler 接口
protected Proxy(InvocationHandler var1) {
    Objects.requireNonNull(var1);
    this.h = var1;
}

然后,我们点进 proxyClassCache.get(var0, var1) 方法,如图:

使用关键代码 this.subKeyFactory.apply(var1, var2) 去获取我们的代理类的 class 对象,我们进入 apply 实现类 ProxyClassFactory,如:

public Class<?> apply(ClassLoader var1, Class<?>[] var2) {
    IdentityHashMap var3 = new IdentityHashMap(var2.length);
    Class[] var4 = var2;
    int var5 = var2.length;

    ...

    if (var16 == null) {
        var16 = "com.sun.proxy.";
    }

    long var19 = nextUniqueNumber.getAndIncrement();
    // 生成代理类的类名
    String var23 = var16 + "$Proxy" + var19;
    // 生成代理类的字节码
    byte[] var22 = ProxyGenerator.generateProxyClass(var23, var2, var17);

    try {
        // 生成代理类的 class 对象
        return Proxy.defineClass0(var1, var23, var22, 0, var22.length);
    } catch (ClassFormatError var14) {
        throw new IllegalArgumentException(var14.toString());
    }
}

然后,我们点进 Proxy.defineClass0 方法,如下:

private static native Class<?> defineClass0(ClassLoader var0, String var1, byte[] var2, int var3, int var4);

是一个 native 方法,所以涉及到 C 或 C++ ,我们就不往后追踪。

那么,代理的 Class 文件到底存在哪儿呢,由一个类的生命周期,如图:

代理的 Class 文件通过反射存在内存中,所以我们可以通过 byte[] 写入文件,我们新建一个工具类来把内存中的 class 字节码写入文件,如:

public class ProxyUtil {

    public static void generateClassFile(Class aClass, String proxyName) {

        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName,
                new Class[]{aClass}
        );
        String path = aClass.getResource(".").getPath();
        System.out.println(path);
        FileOutputStream outputStream = null;

        try {
            outputStream = new FileOutputStream(path + proxyName + ".class");
            outputStream.write(proxyClassFile);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

通过输出的 path 路径,找到文件,如:

/Users/lishaoying/Documents/APP/Android/practice/annotation_reflect/anRePrDemo/proxyDemo/build/classes/java/main/net/lishaoy/proxydemo/service/impl/

文件代码,如下:

// 继承了 Proxy 实现了 ImplKoreaOrderService 接口
public final class $Proxy0 extends Proxy implements ImplKoreaOrderService {

    // 生成了各种方法
    private static Method m1;
    private static Method m8;
    private static Method m3;
    private static Method m2;
    private static Method m5;
    private static Method m4;
    private static Method m7;
    private static Method m9;
    private static Method m0;
    private static Method m6;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    ...

    // 生成了 真实角色的 saveOrder 方法
    public final int saveOrder() throws  {
        try {
            // h 是什?,点进去发现就是我们 传入的 InvocationHandler 接口
            // m3 是什么? 下面 static 代码块,就是我们的 saveOrder 方法
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    ...

    public final Class getClass() throws  {
        try {
            return (Class)super.h.invoke(this, m7, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    ...

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m8 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("notify");
            m3 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("saveOrder");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m5 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("wait", Long.TYPE);
            m4 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("wait", Long.TYPE, Integer.TYPE);
            m7 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("getClass");
            m9 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("notifyAll");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m6 = Class.forName("net.lishaoy.proxydemo.service.impl.ImplKoreaOrderService").getMethod("wait");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

使用注解、反射、动态代理完成简单的Retrofit

由于文章篇幅已经很长,且使用注解、反射、动态代理完成简单的 Retrofit 的案例代码过多,所以就不再这里展示,感兴趣的小伙伴可以去 GitHub 查看源码。

最后附上博客和GitHub地址,如下:

博客地址:https://h.lishaoy.net
GitHub地址:https://github.com/persilee

查看原文

赞 4 收藏 3 评论 0

依然饭特稀西 发布了文章 · 9月3日

热门Android Studio 插件,这里是Top 20!

Android Studio是JetBrains公司开发的一款功能强大的开发工具,它具有构建出色Android应用所需要的一切。借助基于IntelliJ IDEA的强大的功能,插件非常丰富。

正确的使用插件可以帮助你提高工作效率,更智能,更快。但是,不断增长的插件列表可能也会让人不知所措。考虑到这一点,我在这里盘点了一个Android Studio 插件列表,以减轻选择的麻烦。对于使用其他IDE(例如WebStorm)的开发人员也同样适用。

让我们开始吧!

1. CodeGlance

与Sublime或Xcode相似,此插件在你的编辑器中嵌入了代码的缩略图。如下图所示,右边是代码的缩略小图,滚动条也变大了。 使用Codeglance预览代码模式,让你 快速导航至所需部分。

2. Rainbow Brackets

对于程序员来讲,嵌套括号让他们很痛苦,尤其是在缺少某些内容的时候,在代码审查中,它们可能真是令人头疼,因为你很难看出哪些括号是配对的。

这个插件为您的代码添加了漂亮的彩虹色,用于圆括号,方括号和大括号。对于刚刚起步的开发人员,或者对于发现自己陷入大量代码中的开发人员而言,这是一个救星。

除了Java和Kotlin外,此插件还支持其他十多种语言如-Objective-CScalaPythonHTMLSQL等。

3. SQLDelight

SQLDelight是一个著名的Kotlin多平台数据库库。它通过SQL生成Kotlin API,并负责根据架构本身创建数据库。

SQLDelight出自大名顶顶的Square(此外,Square开源了很多强大的流行库,比如:Retrofit、Okhttp、Picasso等),它具有IntelliJ和Android Studio插件,用于语法突出显示,代码完成和通过SQL查询导航。

4. ADB Idea

这是Android Studio和Intellij IDEA的插件,可加快你日常的Android开发速度。 ADB Idea提供了单击快捷方式命令,用于启动,卸载,终止应用程序,撤消权限以及清除应用程序数据。

要调用此插件,您可以导航到“工具”->“ Android”->“ ADB Idea”菜单,或从“查找操作”中搜索命令。

5. ADB Wifi

与iOS开发不同,Android开发人员通常必须确保在设备上进行调试时,永远不要断开USB的连接。尽管有一组ADB Shell命令可以通过WIFI建立连接,但是使用GUI快捷方式要方便的多。只需安装ADB Wifi插件即可。

确保你的电脑和手机连接在同一网络上,然后转到“Tools”→“ android”→“ ADB WIFI”→“ ADB USB to WIFI”开始连接,现在您无需USB连接,即可运行应用程序。

6. Material UI Theme

Android Studio为黑暗模式爱好者提供了一个可爱的 Dracula 主题,但是有时候,做一些更改并不不是坏事,Material UI Theme就是为此而设计。该插件具有令人印象深刻的主题调色板,提供漂亮的配色方案,并支持绝大多数编程语言,以及Material图标,填充和一系列自定义设置。

7. JSON To Kotlin Class

使用此插件,将JSON字符串转换为Kotlin数据类非常容易。此外,它还支持:

  • 一系列JSON注解库-GsonJacksonFastjsonMoshiLoganSquare
  • 使用默认值初始化属性,并使它们可以为空。
  • 将属性名称重命名为驼峰样式,并将类生成为内部或单个类。
  • 如果JSON字符串有效,则从本地文件/ Http URL加载JSON。

1.gif

8. Vector Drawable Thumbnails

要预览矢量drawable XML文件,我们通常必须重新构建项目。而使用Vector Drawable Thumbnails插件,我们只需单击一次,就可以预览所有vector drawables 。

9. Codota

Codota是一个基于AI的代码补全插件,它使用机器学习数百万个代码段(Java,Javascript,Python等),根据您的上下文建议补全代码。它还使你可以直接嵌入通用代码段,以提高开发技能并减少出错的机会。

如下图所示,它在自动完成建议列表中,显示了每个代码完成的概率,最有可能的代码已在编辑器中突出显示(只需按向右箭头)。

10. Name That Color

如果觉得Android代码库中的颜色命名令人头痛,别担心,你不是一个人!尽管编码通常被称为艺术,但并不是所有的开发人员都擅长颜色命名,尤其是不同的阴影。在这种情况下,请输入诸如red1blue_lighterred2之类的神秘名称

幸运的是,有一个很棒的插件可以帮你解决这个名字。你所需要做的就是,将十六进制代码粘贴到你的colors资源文件中,它将为你建议最匹配的 material颜色调色板名称。

11. String Manipulation

接下来,我们介绍一个提供各种字符串操作的插件-String Manipulation。从toggling casesswitching between camelsnakekebab cases再到 incrementing duplicates排序、转义/取消转义HTML,Java,SQL,PHP,XML字符串以及执行过滤器操作(如grep,字符串操作)等,一切触手可及。

12. Gradle Killer

通常,你后悔开始Gradle构建或只是希望立即将其关闭。您可以运行ps命令或在任务管理器中四处寻找Java.exe,但这会很麻烦。现在,令开发人员高兴的是,我们有一个插件,可以在你的Android Studio的运行菜单中添加一个Kill Gradle图标。要回收您的RAM,只需单击它!如下图:

13. Kotlin Fill Class

有一个常见的需求,就是快速创建具有默认属性的Kotlin类。此IntelliJ插件就是用于此目的。它为空的构造函数函数提供了意图操作,使您可以快速初始化参数。

14. TabNine

这是一个自动完成功能插件(代码提示),可利用深度学习来建议智能完成情况,让你更快地编写内容。

它支持20种编程语言,并接受了来自GitHub约200万个文件的培训。为了预测下一个“token”,它会寻找在训练数据集中找到的模式。这使得TabNine在惯用编程中特别有用。

15. Key Promoter X

这是一个IntelliJ IDE插件,可帮助你在工作时学习基本的快捷方式。当你在IDE内,把鼠标放按钮上时,Key Promoter X会提示你应该使用的快捷键

它还在侧窗格中显示了以前使用的鼠标操作,及其对应的键盘快捷键的列表。对于没有快捷方式的按钮,Key Promoter X会提示您直接创建一个快捷方式。

16. Clear Cache Plugin

通常,当开发者需要清除缓存时,他们必须遍历.gradle目录。那很费时间。您可以创建Gradle脚本来加速此过程,但是为什么要重新造轮子呢?

通过使用Clear Cache插件,我们可以检索具有给定前缀的所有软件包,并删除不再需要的软件包。演示如下:

17. FCM Push Sender

通过给Firebase设置Registration ID,我们可以使用此插件直接从Android Studio发送推送通知。该插件还具有使用Stetho dumpapp插件自动搜索Firebase Registration ID token 的应用内共享首选项的功能。

最突出的功能是,能将通知发送到多个可调试设备。我们可以选择发送数据消息或完整消息,如下所示:

18. SQLScout

这是对Android Studio和IntelliJ IDEA极好的SQLite支持,可让您实时管理数据库。这使得在调试应用程序时轻松实时执行SQL查询以更新表。

它还支持Room 持久性库-从现有数据库模式自动生成Room 实体DAOMigrationDatabase类。数据库图表,带有语法突出显示工具的SQL编辑器,可导出schema 为Excel等不同格式。

使用SQLScout插件可以实现所有这些功能。

19. Material Design Icon Generator

这个插件可以帮助你在Android应用程序中添加Material设计图标。导入assets,指定颜色大小密度非常简单。

20. NyanProgress

最后一个插件-NyanProgress,有趣的彩色进度条。

我们有一个不错的进度条,可让您在Gradle构建和重建过程中始终陪伴着您。无休止的等待时间会使任何开发人员感到沮丧。

幸运的是,NyanProgress将我们最喜欢的NyanCat带到了丰富多彩的进度条上,使等待时间变得更加有趣。不用再怀疑Gradle版本是否已冻结!

总结

记住,在IDE中使用过多的插件并不会真正提高你的工作效率,相反,可能会大大降低Android Studio的性能。最后,我建议选择其中一些插件,并将其纳入您的日常开发工作中。

在我们上面👆介绍的20个插件中,我最喜欢的三个是:Name That ColorSQLDelightCodeGlance。那你呢?喜欢哪些?欢迎在评论区留言。

查看原文

赞 8 收藏 5 评论 0

依然饭特稀西 收藏了文章 · 9月2日

Flutter 10天高仿大厂App及小技巧积累总结

2880040dd20bac8885929d5859b5e525.jpg

之前,也写过几篇关于 Flutter 的博文,最近,又花了一些时间学习研究 Flutter,完成了高仿大厂 App 项目 (项目使用的接口都是来自线上真实App抓包而来,可以做到和线上项目相同的效果),也总结积累了一些小技巧和知识点,所以,在这里记录分享出来,也希望 Flutter 生态越来越好 (flutter开发App效率真的很高,开发体验也是很好的 🙂)

以下博文会分为3个部分概述:

  • 项目结构分析
  • 项目功能详细概述(所用知识点)
  • 小技巧积累总结

项目结构分析

其次,梳理下项目的目录结构,理解每个文件都是干什么的,我们先来看看一级目录,如下:

├── README.md  # 描述文件
├── android    # android 宿主环境
├── build      # 项目构建目录,由flutter自动完成
├── flutter_ctrip.iml
├── fonts      # 自己创建的目录,用于存放字体
├── images     # 自己创建的目录,用于存放图片
├── ios        # iOS 宿主环境
├── lib        # flutter 执行文件,自己写的代码都在这
├── pubspec.lock # 用来记录锁定插件版本
├── pubspec.yaml # 插件及资源配置文件
└── test       # 测试目录

这个就不用多解释,大多是 flutter 生成及管理的,我们需要关注的是 lib 目录。

我们再来看看二级目录,如下 (重点关注下lib目录)

├── README.md
├── android
│   ├── android.iml
  ...
│   └── settings.gradle
├── build
│   ├── app
  ...
│   └── snapshot_blob.bin.d.fingerprint
├── flutter_ctrip.iml
├── fonts
│   ├── PingFang-Italic.ttf
│   ├── PingFang-Regular.ttf
│   └── PingFang_Bold.ttf
├── images
│   ├── grid-nav-items-dingzhi.png
  ...
│   └── yuyin.png
├── ios
│   ├── Flutter
  ...
│   └── ServiceDefinitions.json
├── lib
│   ├── dao           # 请求接口的类
│   ├── main.dart     # flutter 入口文件
│   ├── model         # 实体类,把服务器返回的 json 数据,转换成 dart 类
│   ├── navigator     # bottom bar 首页底部导航路由
│   ├── pages         # 所以的页面
│   ├── plugin        # 封装的插件
│   ├── util          # 工具类,避免重复代码,封装成工具类以便各个 page 调用
│   └── widget        # 封装的组件
├── pubspec.lock
├── pubspec.yaml
└── test
    └── widget_test.dart

再来看看,lib 目录下二级目录,看看整个项目创建了多少个文件,写了多少代码,如下 (其实,并不是很多)

├── dao/
│   ├── destination_dao.dart*
│   ├── destination_search_dao.dart*
│   ├── home_dao.dart
│   ├── search_dao.dart*
│   ├── trave_hot_keyword_dao.dart*
│   ├── trave_search_dao.dart*
│   ├── trave_search_hot_dao.dart*
│   ├── travel_dao.dart*
│   ├── travel_params_dao.dart*
│   └── travel_tab_dao.dart*
├── main.dart
├── model/
│   ├── common_model.dart
│   ├── config_model.dart
│   ├── destination_model.dart
│   ├── destination_search_model.dart
│   ├── grid_nav_model.dart
│   ├── home_model.dart
│   ├── sales_box_model.dart
│   ├── seach_model.dart*
│   ├── travel_hot_keyword_model.dart
│   ├── travel_model.dart*
│   ├── travel_params_model.dart*
│   ├── travel_search_hot_model.dart
│   ├── travel_search_model.dart
│   └── travel_tab_model.dart
├── navigator/
│   └── tab_navigater.dart
├── pages/
│   ├── destination_page.dart
│   ├── destination_search_page.dart
│   ├── home_page.dart
│   ├── my_page.dart
│   ├── search_page.dart
│   ├── speak_page.dart*
│   ├── test_page.dart
│   ├── travel_page.dart
│   ├── travel_search_page.dart
│   └── travel_tab_page.dart*
├── plugin/
│   ├── asr_manager.dart*
│   ├── side_page_view.dart
│   ├── square_swiper_pagination.dart
│   └── vertical_tab_view.dart
├── util/
│   └── navigator_util.dart*
└── widget/
    ├── grid_nav.dart
    ├── grid_nav_new.dart
    ├── loading_container.dart
    ├── local_nav.dart
    ├── sales_box.dart
    ├── scalable_box.dart
    ├── search_bar.dart*
    ├── sub_nav.dart
    └── webview.dart

整个项目就是以上这些文件了 (具体的就不一个一个分析了,如,感兴趣,大家可以 clone 源码运行起来,自然就清除了)

项目功能详细概述(所用知识点)

首先,来看看首页功能及所用知识点,首页重点看下以下功能实现:

  • 渐隐渐现的 appBbar
  • 搜索组件的封装
  • 语音搜索页面
  • banner组件
  • 浮动的 icon 导航
  • 渐变不规则带有背景图的网格导航

渐隐渐现的 appBbar

先来看看具体效果,一睹芳容,如图:

appBar

滚动的时候 appBar 背景色从透明变成白色或白色变成透明,这里主要用了 flutterNotificationListener 组件,它会去监听组件树冒泡事件,当被它包裹的的组件(子组件) 发生变化时,Notification 回调函数会被触发,所以,通过它可以去监听页面的滚动,来动态改变 appBar 的透明度(alpha),代码如下:

NotificationListener(
  onNotification: (scrollNotification) {
    if (scrollNotification is ScrollUpdateNotification &&
        scrollNotification.depth == 0) {
      _onScroll(scrollNotification.metrics.pixels);
    }
    return true;
  },
  child: ...

Tips:
scrollNotification.depth 的值 0 表示其子组件(只监听子组件,不监听孙组件)
scrollNotification is ScrollUpdateNotification 来判断组件是否已更新,ScrollUpdateNotification 是 notifications 的生命周期一种情况,分别有一下几种:

  • ScrollStartNotification 组件开始滚动
  • ScrollUpdateNotification 组件位置已经发生改变
  • ScrollEndNotification 组件停止滚动
  • UserScrollNotification 不清楚

这里,我们不探究太深入,如想了解可多查看源码。

_onScroll 方法代码如下:

  void _onScroll(offset) {
    double alpha = offset / APPBAR_SCROLL_OFFSET;  // APPBAR_SCROLL_OFFSET 常量,值:100;offset 滚动的距离

    //把 alpha 值控制值 0-1 之间
    if (alpha < 0) {
      alpha = 0;
    } else if (alpha > 1) {
      alpha = 1;
    }
    setState(() {
      appBarAlpha = alpha;
    });
    print(alpha);
  }

搜索组件的封装

搜索组件效果如图:

searchBar

以下是首页调用 searchBar 的代码:

SearchBar(
  searchBarType: appBarAlpha > 0.2  //searchBar 的类:暗色、亮色
      ? SearchBarType.homeLight
      : SearchBarType.home,
  inputBoxClick: _jumpToSearch,     //点击回调函数
  defaultText: SEARCH_BAR_DEFAULT_TEXT,   // 提示文字
  leftButtonClick: () {},           //左边边按钮点击回调函数
  speakClick: _jumpToSpeak,         //点击话筒回调函数
  rightButtonClick: _jumpToUser,    //右边边按钮点击回调函数
),

其实就是用 TextField 组件,再加一些样式,需要注意点是:onChanged,他是 TextField 用来监听文本框是否变化,通过它我们来监听用户输入,来请求接口数据;
具体的实现细节,请查阅源码: 点击查看searchBar源码

语音搜索页面

语音搜索页面效果如图:由于模拟器无法录音,所以无法展示正常流程,如果录音识别成功后会返回搜索页面,在项目预览视频中可以看到正常流程。

no-shadow

语音搜索功能使用的是百度的语言识别SDK,原生接入之后,通过 MethodChannel 和原生Native端通信,这里不做重点讲述(这里会涉及原生Native的知识)。

重点看看点击录音按钮时的动画实现,这个动画用了 AnimatedWidget 实现的,代码如下:

class AnimatedWear extends AnimatedWidget {
  final bool isStart;
  static final _opacityTween = Tween<double>(begin: 0.5, end: 0); // 设置透明度变化值
  static final _sizeTween = Tween<double>(begin: 90, end: 260);   // 设置圆形线的扩散值

  AnimatedWear({Key key, this.isStart, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;  // listenable 继承 AnimatedWidget,其实就是控制器,会自动监听组件的变化
    return Container(
      height: 90,
      width: 90,
      child: Stack(
        overflow: Overflow.visible,
        alignment: Alignment.center,
        children: <Widget>[
          ...
          // 扩散的圆线,其实就是用一个圆实现的,设置圆为透明,设置border
          Positioned(
            left: -((_sizeTween.evaluate(animation) - 90) / 2), // 根据 _sizeTween 动态设置left偏移值
            top: -((_sizeTween.evaluate(animation) - 90) / 2), //  根据 _sizeTween 动态设置top偏移值
            child: Opacity(
              opacity: _opacityTween.evaluate(animation),      // 根据 _opacityTween 动态设置透明值
              child: Container(
                width: isStart ? _sizeTween.evaluate(animation) : 0, // 设置 宽
                height: _sizeTween.evaluate(animation),              // 设置 高
                decoration: BoxDecoration(
                    color: Colors.transparent,
                    borderRadius: BorderRadius.circular(
                        _sizeTween.evaluate(animation) / 2),
                    border: Border.all(
                      color: Color(0xa8000000),
                    )),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

其他细节,如:点击时提示录音,录音失败提示,点击录音按钮出现半透明黑色圆边框,停止后消失等,请查看源码

banner组件

效果如图:

no-shadow

banner使用的是flutter的 flutter_swiper 插件实现的,代码如下:

Swiper(
  itemCount: bannerList.length,              // 滚动图片的数量
  autoplay: true,                            // 自动播放
  pagination: SwiperPagination(              // 指示器
      builder: SquareSwiperPagination(
        size: 6,                             // 指示器的大小
        activeSize: 6,                       // 激活状态指示器的大小
        color: Colors.white.withAlpha(80),   // 颜色
        activeColor: Colors.white,           // 激活状态的颜色
      ),
    alignment: Alignment.bottomRight,        // 对齐方式
    margin: EdgeInsets.fromLTRB(0, 0, 14, 28), // 边距
  ),
  itemBuilder: (BuildContext context, int index) { // 构造器
    return GestureDetector(
      onTap: () {
        CommonModel model = bannerList[index];
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => WebView(
              url: model.url,
            ),
          ),
        );
      },
      child: Image.network(
        bannerList[index].icon,
        fit: BoxFit.fill,
      ),
    );
  },
),

具体使用方法,可以去 flutter的官方插件库 pub.dev 查看:点击flutter_swiper查看

Tips:
需要注意的是,我稍改造了一下指示器的样式,flutter_swiper 只提供了 3 种指示器样式,如下:

  • dots = const DotSwiperPaginationBuilder(),圆形
  • fraction = const FractionPaginationBuilder(),百分数类型的,如:1/6,表示6页的第一页
  • rect = const RectSwiperPaginationBuilder(),矩形

并没有上图的激活状态的长椭圆形,其实就是按葫芦画瓢,自己实现一个长椭圆类型,如知详情,可点击查看长椭圆形指示器源码

浮动的 icon 导航

icon导航效果如图:

iconBar

icon导航浮动在banner之上,其实用的是 flutterStack 组件,Stack 组件能让其子组件堆叠显示,它通常和 Positioned 组件配合使用,布局结构代码如下:

ListView(
  children: <Widget>[
    Container(
      child: Stack(
        children: <Widget>[
          Container( ... ), //这里放的是banner的代码
          Positioned( ... ), //这个就是icon导航,通过 Positioned 固定显示位置
        ],
      ),
    ),
    Container( ... ), // 这里放的网格导航及其他
  ],
),

渐变不规则带有背景图的网格导航

网格导航效果如图:

gridNav

如图,网格导航分为三行四栏,而第一行分为三栏,每一行的第一栏宽度大于其余三栏,其余三栏均等,每一行都有渐变色,而且第一、二栏都有背景图;
flutterColumn 组件能让子组件竖轴排列, Row 组件能让子组件横轴排列,布局代码如下:

Column(                      // 最外面放在 Column 组件
  children: <Widget>[
    Container(               // 第一行包裹 Container 设置其渐变色
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xfffa5956),
          Color(0xffef9c76).withAlpha(45)
        ]),
      ),
      child: Row( ... ),    // 第一行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),  // 设置行直接的间隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xff4b8fed),
          Color(0xff53bced),
        ]),
      ),
      child: Row( ... ),  // 第二行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),   // 设置行直接的间隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xff34c2aa),
          Color(0xff6cd557),
        ]),
      ),
      child: Row( ... ),  // 第三行
    ),
  ],
),

其实,具体实现的细节还是很多的,比如:

  • 怎么设置第一栏宽度偏大,其他均等;
  • 第一行最后一栏宽度是其他的2倍;
  • 第一、二栏的别截图及浮动的红色气泡tip等;

在这里就不细讲,否则篇幅太长,如想了解详情 点击查看源码

其次,再来看看目的地页面功能及所用知识点,重点看下以下功能实现:

  • 左右布局tabBarListView
  • 目的地搜索页面

左右布局tabBarListView

具体效果如图:点击左边标签可以切换页面,左右滑动也可切换页面,点击展开显示更多等

no-shadow

其实官方已经提供了 tabBarTabBarView 组件可以实现上下布局的效果(旅拍页面就是用这个实现的),但是它无法实现左右布局,而且不太灵活,所以,我使用的是 vertical_tabs插件, 代码如下:

VerticalTabView(
    tabsWidth: 88,
    tabsElevation: 0,
    indicatorWidth: 0,
    selectedTabBackgroundColor: Colors.white,
    backgroundColor: Colors.white,
    tabTextStyle: TextStyle(
      height: 60,
      color: Color(0xff333333),
    ),
    tabs: tabs,
    contents: tabPages,
  ),
),

具体使用方法,在这里就不赘述了,点击vertical_tabs查看

Tips:
这里需要注意的是:展开显示更多span标签组件的实现,因为,这个组件在很多的其他组件里用到而且要根据接口数据动态渲染,且组件自身存在状态的变化,这种情况下,最好是把他单独封装成一个组件(widget),否则,很难控制自身状态的变化,出现点击没有效果,或点击影响其他组件。

目的地搜索页面

效果如图:点击搜索结果,如:点击‘一日游‘,会搜索到‘一日游‘的相关数据

no-shadow

目的地搜索页面,大多都是和布局和对接接口的代码,在这里就不再赘述。

然后就是旅拍页面功能及所用知识点,重点看下以下功能实现:

  • 左右布局tabBarListView
  • 瀑布流卡片
  • 旅拍搜索页

左右布局tabBarListView

效果如图:可左右滑动切换页面,上拉加载更多,下拉刷新等

no-shadow

这个是flutter 提供的组件,tabBarTabBarView,代码如下:

Container(
  color: Colors.white,
  padding: EdgeInsets.only(left: 2),
  child: TabBar(
    controller: _controller,
    isScrollable: true,
    labelColor: Colors.black,
    labelPadding: EdgeInsets.fromLTRB(8, 6, 8, 0),
    indicatorColor: Color(0xff2FCFBB),
    indicatorPadding: EdgeInsets.all(6),
    indicatorSize: TabBarIndicatorSize.label,
    indicatorWeight: 2.2,
    labelStyle: TextStyle(fontSize: 18),
    unselectedLabelStyle: TextStyle(fontSize: 15),
    tabs: tabs.map<Tab>((Groups tab) {
      return Tab(
        text: tab.name,
      );
    }).toList(),
  ),
),
Flexible(
    child: Container(
  padding: EdgeInsets.fromLTRB(6, 3, 6, 0),
  child: TabBarView(
      controller: _controller,
      children: tabs.map((Groups tab) {
        return TravelTabPage(
          travelUrl: travelParamsModel?.url,
          params: travelParamsModel?.params,
          groupChannelCode: tab?.code,
        );
      }).toList()),
)),

瀑布流卡片

瀑布流卡片 用的是 flutter_staggered_grid_view 插件,代码如下:

StaggeredGridView.countBuilder(
  controller: _scrollController,
  crossAxisCount: 4,
  itemCount: travelItems?.length ?? 0,
  itemBuilder: (BuildContext context, int index) => _TravelItem(
        index: index,
        item: travelItems[index],
      ),
  staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
  mainAxisSpacing: 2.0,
  crossAxisSpacing: 2.0,
),

如下了解更多相关信息,点击flutter_staggered_grid_view查看

旅拍搜索页

效果如图:首先显示热门旅拍标签,点击可搜索相关内容,输入关键字可搜索相关旅拍信息,地点、景点、用户等

no-shadow

旅拍搜索页,大多也是和布局和对接接口的代码,在这里就不再赘述。

小技巧积累总结

以下都是我在项目里使用的知识点,在这里记录分享出来,希望能帮到大家。

PhysicalModel

PhysicalModel 可以裁剪带背景图的容器,如,你在一个 Container 里放了一张图片,想设置图片圆角,设置 Container 的 decoration 的 borderRadius 是无效的,这时候就要用到 PhysicalModel,代码如下:

PhysicalModel(
  borderRadius: BorderRadius.circular(6),  // 设置圆角
  clipBehavior: Clip.antiAlias,            // 裁剪行为
  color: Colors.transparent,               // 颜色
  elevation: 5,                            // 设置阴影
  child: Container(
        child: Image.network(
          picUrl,
          fit: BoxFit.cover,
        ),
      ),
),

LinearGradient

给容器添加渐变色,在网格导航、appBar等地方都使用到,代码如下:

Container(
  height: 72,
  decoration: BoxDecoration(
    gradient: LinearGradient(colors: [
      Color(0xff4b8fed),
      Color(0xff53bced),
    ]),
  ),
  child: ...
),

Color(int.parse('0xff' + gridNavItem.startColor))

颜色值转换成颜色,如果,没有变量的话,也可直接这样用 Color(0xff53bced)

  • ox:flutter要求,可固定不变
  • ff:代表透明贴,不知道如何设置的话,可以用取色器,或者 withOpacity(opacity) 、 withAlpha(a)
  • 53bced: 常规的6位RGB值

Expanded、FractionallySizedBox

Expanded 可以让子组件撑满父容器,通常和 RowColumn 组件搭配使用;


FractionallySizedBox 可以让子组件撑满或超出父容器,可以单独使用,大小受 widthFactor 和 heightFactor 宽高因子的影响

MediaQuery.removePadding

MediaQuery.removePadding 可以移除组件的边距,有些组件自带有边距,有时候布局的时候,不需要边距,这时候就可以用 MediaQuery.removePadding,代码如下:

MediaQuery.removePadding(
  removeTop: true,
  context: context,
  child: ...
)

MediaQuery.of(context).size.width

MediaQuery.of(context).size.width 获取屏幕的宽度,同理,MediaQuery.of(context).size.height 获取屏幕的高度;
如,想一行平均3等分: 0.3 * MediaQuery.of(context).size.width,在目的地页面的标签组件就使用到它,代码如下:

Container(
  alignment: Alignment.center,
  ...
  width: 0.3*MediaQuery.of(context).size.width - 12, // 屏幕平分三等分, - 12 是给每份中间留出空间 
  height: 40,
  ...
  child: ...
),

Theme.of(context).platform == TargetPlatform.iOS

判断操作系统类型,有时候可能有给 Andorid 和 iOS 做出不同的布局,就需要用到它。

with AutomaticKeepAliveClientMixin

flutter 在切换页面时候每次都会重新加载数据,如果想让页面保留状态,不重新加载,就需要使用 AutomaticKeepAliveClientMixin,代码如下:(在旅拍页面就有使用到它,为了让tabBar 和 tabBarView在切换时不重新加载)

class TravelTabPage extends StatefulWidget {
  ...
  //需要重写 wantKeepAlive 且 设置成 true
  @override
  bool get wantKeepAlive => true;
}

暂时只能想到这些常用的知识点,以后如有新的会慢慢补充。

博客地址:https://lishaoy.net
博客Notes地址:https://h.lishaoy.net
项目GitHub地址:https://github.com/persilee/flutter_ctrip

查看原文

依然饭特稀西 赞了文章 · 9月2日

Flutter 10天高仿大厂App及小技巧积累总结

2880040dd20bac8885929d5859b5e525.jpg

之前,也写过几篇关于 Flutter 的博文,最近,又花了一些时间学习研究 Flutter,完成了高仿大厂 App 项目 (项目使用的接口都是来自线上真实App抓包而来,可以做到和线上项目相同的效果),也总结积累了一些小技巧和知识点,所以,在这里记录分享出来,也希望 Flutter 生态越来越好 (flutter开发App效率真的很高,开发体验也是很好的 🙂)

以下博文会分为3个部分概述:

  • 项目结构分析
  • 项目功能详细概述(所用知识点)
  • 小技巧积累总结

项目结构分析

其次,梳理下项目的目录结构,理解每个文件都是干什么的,我们先来看看一级目录,如下:

├── README.md  # 描述文件
├── android    # android 宿主环境
├── build      # 项目构建目录,由flutter自动完成
├── flutter_ctrip.iml
├── fonts      # 自己创建的目录,用于存放字体
├── images     # 自己创建的目录,用于存放图片
├── ios        # iOS 宿主环境
├── lib        # flutter 执行文件,自己写的代码都在这
├── pubspec.lock # 用来记录锁定插件版本
├── pubspec.yaml # 插件及资源配置文件
└── test       # 测试目录

这个就不用多解释,大多是 flutter 生成及管理的,我们需要关注的是 lib 目录。

我们再来看看二级目录,如下 (重点关注下lib目录)

├── README.md
├── android
│   ├── android.iml
  ...
│   └── settings.gradle
├── build
│   ├── app
  ...
│   └── snapshot_blob.bin.d.fingerprint
├── flutter_ctrip.iml
├── fonts
│   ├── PingFang-Italic.ttf
│   ├── PingFang-Regular.ttf
│   └── PingFang_Bold.ttf
├── images
│   ├── grid-nav-items-dingzhi.png
  ...
│   └── yuyin.png
├── ios
│   ├── Flutter
  ...
│   └── ServiceDefinitions.json
├── lib
│   ├── dao           # 请求接口的类
│   ├── main.dart     # flutter 入口文件
│   ├── model         # 实体类,把服务器返回的 json 数据,转换成 dart 类
│   ├── navigator     # bottom bar 首页底部导航路由
│   ├── pages         # 所以的页面
│   ├── plugin        # 封装的插件
│   ├── util          # 工具类,避免重复代码,封装成工具类以便各个 page 调用
│   └── widget        # 封装的组件
├── pubspec.lock
├── pubspec.yaml
└── test
    └── widget_test.dart

再来看看,lib 目录下二级目录,看看整个项目创建了多少个文件,写了多少代码,如下 (其实,并不是很多)

├── dao/
│   ├── destination_dao.dart*
│   ├── destination_search_dao.dart*
│   ├── home_dao.dart
│   ├── search_dao.dart*
│   ├── trave_hot_keyword_dao.dart*
│   ├── trave_search_dao.dart*
│   ├── trave_search_hot_dao.dart*
│   ├── travel_dao.dart*
│   ├── travel_params_dao.dart*
│   └── travel_tab_dao.dart*
├── main.dart
├── model/
│   ├── common_model.dart
│   ├── config_model.dart
│   ├── destination_model.dart
│   ├── destination_search_model.dart
│   ├── grid_nav_model.dart
│   ├── home_model.dart
│   ├── sales_box_model.dart
│   ├── seach_model.dart*
│   ├── travel_hot_keyword_model.dart
│   ├── travel_model.dart*
│   ├── travel_params_model.dart*
│   ├── travel_search_hot_model.dart
│   ├── travel_search_model.dart
│   └── travel_tab_model.dart
├── navigator/
│   └── tab_navigater.dart
├── pages/
│   ├── destination_page.dart
│   ├── destination_search_page.dart
│   ├── home_page.dart
│   ├── my_page.dart
│   ├── search_page.dart
│   ├── speak_page.dart*
│   ├── test_page.dart
│   ├── travel_page.dart
│   ├── travel_search_page.dart
│   └── travel_tab_page.dart*
├── plugin/
│   ├── asr_manager.dart*
│   ├── side_page_view.dart
│   ├── square_swiper_pagination.dart
│   └── vertical_tab_view.dart
├── util/
│   └── navigator_util.dart*
└── widget/
    ├── grid_nav.dart
    ├── grid_nav_new.dart
    ├── loading_container.dart
    ├── local_nav.dart
    ├── sales_box.dart
    ├── scalable_box.dart
    ├── search_bar.dart*
    ├── sub_nav.dart
    └── webview.dart

整个项目就是以上这些文件了 (具体的就不一个一个分析了,如,感兴趣,大家可以 clone 源码运行起来,自然就清除了)

项目功能详细概述(所用知识点)

首先,来看看首页功能及所用知识点,首页重点看下以下功能实现:

  • 渐隐渐现的 appBbar
  • 搜索组件的封装
  • 语音搜索页面
  • banner组件
  • 浮动的 icon 导航
  • 渐变不规则带有背景图的网格导航

渐隐渐现的 appBbar

先来看看具体效果,一睹芳容,如图:

appBar

滚动的时候 appBar 背景色从透明变成白色或白色变成透明,这里主要用了 flutterNotificationListener 组件,它会去监听组件树冒泡事件,当被它包裹的的组件(子组件) 发生变化时,Notification 回调函数会被触发,所以,通过它可以去监听页面的滚动,来动态改变 appBar 的透明度(alpha),代码如下:

NotificationListener(
  onNotification: (scrollNotification) {
    if (scrollNotification is ScrollUpdateNotification &&
        scrollNotification.depth == 0) {
      _onScroll(scrollNotification.metrics.pixels);
    }
    return true;
  },
  child: ...

Tips:
scrollNotification.depth 的值 0 表示其子组件(只监听子组件,不监听孙组件)
scrollNotification is ScrollUpdateNotification 来判断组件是否已更新,ScrollUpdateNotification 是 notifications 的生命周期一种情况,分别有一下几种:

  • ScrollStartNotification 组件开始滚动
  • ScrollUpdateNotification 组件位置已经发生改变
  • ScrollEndNotification 组件停止滚动
  • UserScrollNotification 不清楚

这里,我们不探究太深入,如想了解可多查看源码。

_onScroll 方法代码如下:

  void _onScroll(offset) {
    double alpha = offset / APPBAR_SCROLL_OFFSET;  // APPBAR_SCROLL_OFFSET 常量,值:100;offset 滚动的距离

    //把 alpha 值控制值 0-1 之间
    if (alpha < 0) {
      alpha = 0;
    } else if (alpha > 1) {
      alpha = 1;
    }
    setState(() {
      appBarAlpha = alpha;
    });
    print(alpha);
  }

搜索组件的封装

搜索组件效果如图:

searchBar

以下是首页调用 searchBar 的代码:

SearchBar(
  searchBarType: appBarAlpha > 0.2  //searchBar 的类:暗色、亮色
      ? SearchBarType.homeLight
      : SearchBarType.home,
  inputBoxClick: _jumpToSearch,     //点击回调函数
  defaultText: SEARCH_BAR_DEFAULT_TEXT,   // 提示文字
  leftButtonClick: () {},           //左边边按钮点击回调函数
  speakClick: _jumpToSpeak,         //点击话筒回调函数
  rightButtonClick: _jumpToUser,    //右边边按钮点击回调函数
),

其实就是用 TextField 组件,再加一些样式,需要注意点是:onChanged,他是 TextField 用来监听文本框是否变化,通过它我们来监听用户输入,来请求接口数据;
具体的实现细节,请查阅源码: 点击查看searchBar源码

语音搜索页面

语音搜索页面效果如图:由于模拟器无法录音,所以无法展示正常流程,如果录音识别成功后会返回搜索页面,在项目预览视频中可以看到正常流程。

no-shadow

语音搜索功能使用的是百度的语言识别SDK,原生接入之后,通过 MethodChannel 和原生Native端通信,这里不做重点讲述(这里会涉及原生Native的知识)。

重点看看点击录音按钮时的动画实现,这个动画用了 AnimatedWidget 实现的,代码如下:

class AnimatedWear extends AnimatedWidget {
  final bool isStart;
  static final _opacityTween = Tween<double>(begin: 0.5, end: 0); // 设置透明度变化值
  static final _sizeTween = Tween<double>(begin: 90, end: 260);   // 设置圆形线的扩散值

  AnimatedWear({Key key, this.isStart, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;  // listenable 继承 AnimatedWidget,其实就是控制器,会自动监听组件的变化
    return Container(
      height: 90,
      width: 90,
      child: Stack(
        overflow: Overflow.visible,
        alignment: Alignment.center,
        children: <Widget>[
          ...
          // 扩散的圆线,其实就是用一个圆实现的,设置圆为透明,设置border
          Positioned(
            left: -((_sizeTween.evaluate(animation) - 90) / 2), // 根据 _sizeTween 动态设置left偏移值
            top: -((_sizeTween.evaluate(animation) - 90) / 2), //  根据 _sizeTween 动态设置top偏移值
            child: Opacity(
              opacity: _opacityTween.evaluate(animation),      // 根据 _opacityTween 动态设置透明值
              child: Container(
                width: isStart ? _sizeTween.evaluate(animation) : 0, // 设置 宽
                height: _sizeTween.evaluate(animation),              // 设置 高
                decoration: BoxDecoration(
                    color: Colors.transparent,
                    borderRadius: BorderRadius.circular(
                        _sizeTween.evaluate(animation) / 2),
                    border: Border.all(
                      color: Color(0xa8000000),
                    )),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

其他细节,如:点击时提示录音,录音失败提示,点击录音按钮出现半透明黑色圆边框,停止后消失等,请查看源码

banner组件

效果如图:

no-shadow

banner使用的是flutter的 flutter_swiper 插件实现的,代码如下:

Swiper(
  itemCount: bannerList.length,              // 滚动图片的数量
  autoplay: true,                            // 自动播放
  pagination: SwiperPagination(              // 指示器
      builder: SquareSwiperPagination(
        size: 6,                             // 指示器的大小
        activeSize: 6,                       // 激活状态指示器的大小
        color: Colors.white.withAlpha(80),   // 颜色
        activeColor: Colors.white,           // 激活状态的颜色
      ),
    alignment: Alignment.bottomRight,        // 对齐方式
    margin: EdgeInsets.fromLTRB(0, 0, 14, 28), // 边距
  ),
  itemBuilder: (BuildContext context, int index) { // 构造器
    return GestureDetector(
      onTap: () {
        CommonModel model = bannerList[index];
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => WebView(
              url: model.url,
            ),
          ),
        );
      },
      child: Image.network(
        bannerList[index].icon,
        fit: BoxFit.fill,
      ),
    );
  },
),

具体使用方法,可以去 flutter的官方插件库 pub.dev 查看:点击flutter_swiper查看

Tips:
需要注意的是,我稍改造了一下指示器的样式,flutter_swiper 只提供了 3 种指示器样式,如下:

  • dots = const DotSwiperPaginationBuilder(),圆形
  • fraction = const FractionPaginationBuilder(),百分数类型的,如:1/6,表示6页的第一页
  • rect = const RectSwiperPaginationBuilder(),矩形

并没有上图的激活状态的长椭圆形,其实就是按葫芦画瓢,自己实现一个长椭圆类型,如知详情,可点击查看长椭圆形指示器源码

浮动的 icon 导航

icon导航效果如图:

iconBar

icon导航浮动在banner之上,其实用的是 flutterStack 组件,Stack 组件能让其子组件堆叠显示,它通常和 Positioned 组件配合使用,布局结构代码如下:

ListView(
  children: <Widget>[
    Container(
      child: Stack(
        children: <Widget>[
          Container( ... ), //这里放的是banner的代码
          Positioned( ... ), //这个就是icon导航,通过 Positioned 固定显示位置
        ],
      ),
    ),
    Container( ... ), // 这里放的网格导航及其他
  ],
),

渐变不规则带有背景图的网格导航

网格导航效果如图:

gridNav

如图,网格导航分为三行四栏,而第一行分为三栏,每一行的第一栏宽度大于其余三栏,其余三栏均等,每一行都有渐变色,而且第一、二栏都有背景图;
flutterColumn 组件能让子组件竖轴排列, Row 组件能让子组件横轴排列,布局代码如下:

Column(                      // 最外面放在 Column 组件
  children: <Widget>[
    Container(               // 第一行包裹 Container 设置其渐变色
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xfffa5956),
          Color(0xffef9c76).withAlpha(45)
        ]),
      ),
      child: Row( ... ),    // 第一行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),  // 设置行直接的间隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xff4b8fed),
          Color(0xff53bced),
        ]),
      ),
      child: Row( ... ),  // 第二行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),   // 设置行直接的间隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //设置渐变色
          Color(0xff34c2aa),
          Color(0xff6cd557),
        ]),
      ),
      child: Row( ... ),  // 第三行
    ),
  ],
),

其实,具体实现的细节还是很多的,比如:

  • 怎么设置第一栏宽度偏大,其他均等;
  • 第一行最后一栏宽度是其他的2倍;
  • 第一、二栏的别截图及浮动的红色气泡tip等;

在这里就不细讲,否则篇幅太长,如想了解详情 点击查看源码

其次,再来看看目的地页面功能及所用知识点,重点看下以下功能实现:

  • 左右布局tabBarListView
  • 目的地搜索页面

左右布局tabBarListView

具体效果如图:点击左边标签可以切换页面,左右滑动也可切换页面,点击展开显示更多等

no-shadow

其实官方已经提供了 tabBarTabBarView 组件可以实现上下布局的效果(旅拍页面就是用这个实现的),但是它无法实现左右布局,而且不太灵活,所以,我使用的是 vertical_tabs插件, 代码如下:

VerticalTabView(
    tabsWidth: 88,
    tabsElevation: 0,
    indicatorWidth: 0,
    selectedTabBackgroundColor: Colors.white,
    backgroundColor: Colors.white,
    tabTextStyle: TextStyle(
      height: 60,
      color: Color(0xff333333),
    ),
    tabs: tabs,
    contents: tabPages,
  ),
),

具体使用方法,在这里就不赘述了,点击vertical_tabs查看

Tips:
这里需要注意的是:展开显示更多span标签组件的实现,因为,这个组件在很多的其他组件里用到而且要根据接口数据动态渲染,且组件自身存在状态的变化,这种情况下,最好是把他单独封装成一个组件(widget),否则,很难控制自身状态的变化,出现点击没有效果,或点击影响其他组件。

目的地搜索页面

效果如图:点击搜索结果,如:点击‘一日游‘,会搜索到‘一日游‘的相关数据

no-shadow

目的地搜索页面,大多都是和布局和对接接口的代码,在这里就不再赘述。

然后就是旅拍页面功能及所用知识点,重点看下以下功能实现:

  • 左右布局tabBarListView
  • 瀑布流卡片
  • 旅拍搜索页

左右布局tabBarListView

效果如图:可左右滑动切换页面,上拉加载更多,下拉刷新等

no-shadow

这个是flutter 提供的组件,tabBarTabBarView,代码如下:

Container(
  color: Colors.white,
  padding: EdgeInsets.only(left: 2),
  child: TabBar(
    controller: _controller,
    isScrollable: true,
    labelColor: Colors.black,
    labelPadding: EdgeInsets.fromLTRB(8, 6, 8, 0),
    indicatorColor: Color(0xff2FCFBB),
    indicatorPadding: EdgeInsets.all(6),
    indicatorSize: TabBarIndicatorSize.label,
    indicatorWeight: 2.2,
    labelStyle: TextStyle(fontSize: 18),
    unselectedLabelStyle: TextStyle(fontSize: 15),
    tabs: tabs.map<Tab>((Groups tab) {
      return Tab(
        text: tab.name,
      );
    }).toList(),
  ),
),
Flexible(
    child: Container(
  padding: EdgeInsets.fromLTRB(6, 3, 6, 0),
  child: TabBarView(
      controller: _controller,
      children: tabs.map((Groups tab) {
        return TravelTabPage(
          travelUrl: travelParamsModel?.url,
          params: travelParamsModel?.params,
          groupChannelCode: tab?.code,
        );
      }).toList()),
)),

瀑布流卡片

瀑布流卡片 用的是 flutter_staggered_grid_view 插件,代码如下:

StaggeredGridView.countBuilder(
  controller: _scrollController,
  crossAxisCount: 4,
  itemCount: travelItems?.length ?? 0,
  itemBuilder: (BuildContext context, int index) => _TravelItem(
        index: index,
        item: travelItems[index],
      ),
  staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
  mainAxisSpacing: 2.0,
  crossAxisSpacing: 2.0,
),

如下了解更多相关信息,点击flutter_staggered_grid_view查看

旅拍搜索页

效果如图:首先显示热门旅拍标签,点击可搜索相关内容,输入关键字可搜索相关旅拍信息,地点、景点、用户等

no-shadow

旅拍搜索页,大多也是和布局和对接接口的代码,在这里就不再赘述。

小技巧积累总结

以下都是我在项目里使用的知识点,在这里记录分享出来,希望能帮到大家。

PhysicalModel

PhysicalModel 可以裁剪带背景图的容器,如,你在一个 Container 里放了一张图片,想设置图片圆角,设置 Container 的 decoration 的 borderRadius 是无效的,这时候就要用到 PhysicalModel,代码如下:

PhysicalModel(
  borderRadius: BorderRadius.circular(6),  // 设置圆角
  clipBehavior: Clip.antiAlias,            // 裁剪行为
  color: Colors.transparent,               // 颜色
  elevation: 5,                            // 设置阴影
  child: Container(
        child: Image.network(
          picUrl,
          fit: BoxFit.cover,
        ),
      ),
),

LinearGradient

给容器添加渐变色,在网格导航、appBar等地方都使用到,代码如下:

Container(
  height: 72,
  decoration: BoxDecoration(
    gradient: LinearGradient(colors: [
      Color(0xff4b8fed),
      Color(0xff53bced),
    ]),
  ),
  child: ...
),

Color(int.parse('0xff' + gridNavItem.startColor))

颜色值转换成颜色,如果,没有变量的话,也可直接这样用 Color(0xff53bced)

  • ox:flutter要求,可固定不变
  • ff:代表透明贴,不知道如何设置的话,可以用取色器,或者 withOpacity(opacity) 、 withAlpha(a)
  • 53bced: 常规的6位RGB值

Expanded、FractionallySizedBox

Expanded 可以让子组件撑满父容器,通常和 RowColumn 组件搭配使用;


FractionallySizedBox 可以让子组件撑满或超出父容器,可以单独使用,大小受 widthFactor 和 heightFactor 宽高因子的影响

MediaQuery.removePadding

MediaQuery.removePadding 可以移除组件的边距,有些组件自带有边距,有时候布局的时候,不需要边距,这时候就可以用 MediaQuery.removePadding,代码如下:

MediaQuery.removePadding(
  removeTop: true,
  context: context,
  child: ...
)

MediaQuery.of(context).size.width

MediaQuery.of(context).size.width 获取屏幕的宽度,同理,MediaQuery.of(context).size.height 获取屏幕的高度;
如,想一行平均3等分: 0.3 * MediaQuery.of(context).size.width,在目的地页面的标签组件就使用到它,代码如下:

Container(
  alignment: Alignment.center,
  ...
  width: 0.3*MediaQuery.of(context).size.width - 12, // 屏幕平分三等分, - 12 是给每份中间留出空间 
  height: 40,
  ...
  child: ...
),

Theme.of(context).platform == TargetPlatform.iOS

判断操作系统类型,有时候可能有给 Andorid 和 iOS 做出不同的布局,就需要用到它。

with AutomaticKeepAliveClientMixin

flutter 在切换页面时候每次都会重新加载数据,如果想让页面保留状态,不重新加载,就需要使用 AutomaticKeepAliveClientMixin,代码如下:(在旅拍页面就有使用到它,为了让tabBar 和 tabBarView在切换时不重新加载)

class TravelTabPage extends StatefulWidget {
  ...
  //需要重写 wantKeepAlive 且 设置成 true
  @override
  bool get wantKeepAlive => true;
}

暂时只能想到这些常用的知识点,以后如有新的会慢慢补充。

博客地址:https://lishaoy.net
博客Notes地址:https://h.lishaoy.net
项目GitHub地址:https://github.com/persilee/flutter_ctrip

查看原文

赞 62 收藏 43 评论 8

依然饭特稀西 发布了文章 · 8月31日

Jetpack Compose 重磅更新!最全的新组件上手指南!

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你

Jetpack Compose 是Google发布的一个Android原生现代UI工具包,它完全采用Kotlin编写,可以使用Kotlin语言的全部特性,可以帮助你轻松、快速的构建高质量的Android应用程序。如果你还不了解Jetpack Compose是什么?建议你读一下我前面的2篇文章:

Android Jetpack Compose 最全上手指南

Jetpack Compose,不止是一个UI框架!

去年的Google IO 大会上,Google宣布了Jetpack Compose的面世,但是在去年11月份,它才发布第一个预览版-Developer Preview1,此后,基本保持每两周发布一个小版本,到现在,半年的时间过去了,中间发布了十多个小版本,今天,终于迎来了重大更新,Developer Preview2 发布了。

Jetpack Compose Developer Preview1发布后,开发者最关心的几个问题是,没有Compose版本的RecyclerView、Constriantlayout、动画等一系列问题。这些问题在Preview2都解决了。

当然,从Preview1 到现在发布的Preview2,变化非常大,甚至很多API都已经变了,有的属性或者类的增加或者删除。具体的变换化太多,就不在这里一一讲解,感兴趣的可以看看官方的每个小版本的更新日志。今天就带大家一起看看PreView2增加的一些重磅功能。

  • 1、Modifier
  • 2、RecyclerView
  • 3、Constriantlayout
  • 4、动画
  • 5、原生View引入Compose

好戏开场了!

1、Modifier

首先,说一下Modifier(修改器),在Preview1版本,就已经有了modifier,不过使用的地方不多,并且对于它的定位比较模糊,令人困惑,因为modifier能干的事儿,通过组合函数也能做到。但是我们发现了一件事,例如,要在Compose函数中增加padding的时候,会产生大量的嵌套,因为要给嵌套一个容器才能设置padding,因此,现在将很多功能都移动到了Modifier,它现在使用非常广泛,可以修饰一个元素、一个布局或者一些其他行为。如何使用Modifier?先来看一个例子:

首先,我们写一个Compose函数(即Compose组件),展示一张图片

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop )
}

图片显示的是原来的尺寸,然后给图片指定一个大小,比如:256dp,此时就需要使用Modifier了。

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp))
}

修改后如下,宽高都为256dp。

modifier中有很多可以配的参数,比如,增加一个padding,将图片裁剪成一个圆形

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
        )
}

效果就成了这样:

还可以再圆形头像加一个border,代码如下:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
        )
}

效果如下:

还可以同时添加多个border,比如我再增加2个:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
         .drawBorder(12.dp,MaterialTheme.colors.secondary,shape = shape)
         .drawBorder(18.dp,MaterialTheme.colors.background,shape = shape)
        )
}

效果就成这样了:

设置点击事件也是再modifier中,比如我们要在点击这个图片后,改变形状,以前的View可麻烦了,但是Jetpack compose 却非常简单,modifier中增加如下代码:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
         .drawBorder(12.dp,MaterialTheme.colors.secondary,shape = shape)
         .drawBorder(18.dp,MaterialTheme.colors.background,shape = shape)
         .clickable { // 点击事件
             setShape(
                 if(shape == CircleShape)
                     CutCornerShape(topLeft = 32.dp,bottomRight = 32.dp)
                else
                     CircleShape
             )
         }
        )
}

上面的代码中,我们还增加了判断,如果当前shape是CircleShape,我们就改变形状,否则就设置为CircleShape,效果就是点击图片,形状在这两种效果之间来回切换。

效果如下:

2. Jetpack Compose 中的RecyclerView

RecyclerView是我们Android开发中用来展示大数据列表的常用组件,它能帮助我们回收复用视图,有很好的性能体验。在Jetpack Developer PreView1 刚出来的时候,我就在官网或者代码库中找这个组件。很遗憾翻遍了所有资料都每找到,是确实没有,最终只找到了一个叫做VerticalScroller的组件你。它可以用来展示列表,但是它不是RecyclerView,它类似我们的ScrollView,也就是说,展示少量数据的列表是可以的,因为它没有复用机制,展示大列表时,内存堪忧,会OOM。

但是在这次的Preview2中,RecyclerView终于被盼来了,组件名字叫做:AdapterList,它就对应我们原生Android开发的RecyclerView。以前我们要写一个列表是非常复杂的,用写xml,Adapter,ViewHolder等,最终还要在Activity/Fragment初始化和绑定数据,非常麻烦。Jetpack Compose中的列表使用则是非常简单,简单到令人发指。来看一下我们如何展示一个列表:

@Composable
fun generateList(context: Context) {
    val list = mutableListOf<String>()
    //准备数据
    for (i in 1..100) {
        list.add(i.toString())
    }
    AdapterList(data = list) {
        Card(
            shape = MaterialTheme.shapes.medium,
            modifier = Modifier
                .preferredSize(context.resources.displayMetrics.widthPixels.dp, 60.dp)
                .padding(10.dp)
        ) {

            Box(gravity = ContentGravity.Center) {
                ProvideEmphasis(EmphasisAmbient.current.medium) {
                    Text(
                        text = it,
                        style = MaterialTheme.typography.body2
                    )
                }
            }

        }
        Spacer(modifier = Modifier.preferredHeight(10.dp))
    }
}

看到了没,就是这样几行代码,我们的列表就完成了,解释一下代码:最开始的准备数据没啥说的,向list中添加了100个数据,然后将数据源传给AdapterList,列表的每一个Item是一个卡片,用的是Card组件,卡片里展示了一个Text文本,最后的Spacer用来设置item之间的间距,相当于ItemDecoration,看一下效果:

3. Constriantlayout

Constriantlayout是一个功能非常强大的布局,也是现在Android开发中最受欢迎的布局之一,当Jetpack Compose Preview1版本才出来的时候,很多开发者都有一个疑问,Compose 中该如何使用Constriantlayout呢?它将如何运作,这确实是个有意思的问题。因为在Jetpack Compose中,所有的组件都是组合函数,获取不到View饮用,如何约束彼此之间的关系确实是一个难题。好在现在这个难题解决了,下面通过几个小例子一起来看看Compose中的Constriantlayout使用。

如下图所示,有两个View,A和B,ViewB在ViewA的右边,顶部和ViewA的底部对齐,如何使用Constriantlayout 描述它们的位置关系?

代码:

@Composable
fun GreetConstraintLayout(context: Context) {
    ConstraintLayout(constraintSet = ConstraintSet {
        val viewA = tag("ViewTagA").apply {
            left constrainTo parent.left
            centerVertically()
        }
       val viewB =  tag("ViewTagB").apply {
            left constrainTo viewA.right
            centerVertically()
            top constrainTo viewA.bottom
        }
    }, modifier = Modifier.preferredSize(context.screenWidth().dp,400.dp).drawBackground(Color.LightGray)) {

        Box(
            modifier = Modifier.tag("ViewTagA").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Blue,
            gravity = ContentGravity.Center
        ) {
            Text(text = "A")
        }
        Box(
            modifier = Modifier.tag("ViewTagB").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Green,
            gravity = ContentGravity.Center
        ) {
            Text(text = "B")
        }
    }
}

解释一下上面的代码:在ConstraintSet中来定义约束,使用Tag来创建一个约束,后面我们就可以通过这个tag来使用我们定义的约束,返回的是一个ConstrainedLayoutReference,ViewA的左边与父组件的左边对齐,垂直居中。ViewB的左边与ViewA的右边对齐,top与ViewA的底部对齐。也垂直居中。

比如ViewB中就是使用ViewA来作为约束条件了。

后面使用的时候,直接用Modifier.tag()应用约束到组件上。

这还不是最牛逼,还有一个强大的功能是可以在布局约束中添加逻辑,比如:我有一个ViewC 它的位置可能有两种情况:

  • 1、ViewC 的左边与ViewA的右边对齐
  • 2、View C的左边与ViewB的右边对齐

该怎么写代码?先定一个一个Boolean 变量叫hasFlag(随便其的名,它的值根据你的业务逻辑某些情况是true,某些情况是false)

 val hasFlag = true // 它的值根据你的业务逻辑某些情况是true,某些情况是false
 
 tag("ViewC").apply {
            // 根据判断条件改变,约束也改变
            left constrainTo (if(hasFlag) viewA else viewB).right
            bottom constrainTo viewB.top
        }

完整代码如下:

@Composable
fun GreetConstraintLayout(context: Context) {
    ConstraintLayout(constraintSet = ConstraintSet {
        val hasFlag = true // 它的值根据你的业务逻辑某些情况是true,某些情况是false
        val viewA = tag("ViewTagA").apply {
            left constrainTo parent.left
            centerVertically()
        }
       val viewB =  tag("ViewTagB").apply {
            left constrainTo viewA.right
            centerVertically()
            top constrainTo viewA.bottom
        }
        tag("ViewC").apply {
            // 根据判断条件改变,约束也改变
            left constrainTo (if(hasFlag) viewA else viewB).right
            bottom constrainTo viewB.top
        }
    }, modifier = Modifier.preferredSize(context.screenWidth().dp,400.dp).drawBackground(Color.LightGray)) {

        Box(
            modifier = Modifier.tag("ViewTagA").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Blue,
            gravity = ContentGravity.Center
        ) {
            Text(text = "A")
        }
        Box(
            modifier = Modifier.tag("ViewTagB").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Green,
            gravity = ContentGravity.Center
        ) {
            Text(text = "B")
        }
        Box(
            modifier = Modifier.tag("ViewC").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Red,
            gravity = ContentGravity.Center
        ) {
            Text(text = "C")
        }
    }
}

hasFlag=true 效果如下:

hasFlag=false 效果如下:

其他的一些约束布局属性同现在我们使用的ConstraintLayout相同,有兴趣的可以去试试。

4. 动画

Jetpack Compose对动画的支持也是开发者非常关心的一个问题,这一小节就看看Compose中,动画的使用,还是来看一个小例子,先看效果图:

如上,一个简单的属性动画,图片有选中/未选中两种状态,由未选中->选中时,有一个正方形->圆形的动画,并且伴随着alpha动画。

代码如下:

@Composable
fun GreetAnimate(){
    //
    val (selected,onValueChange) = state { false }
    // radius 变化
    val radius = animate(if(selected) 100.dp else 8.dp)
    // alpha 动画
    val selectAlpha = animate(if(selected) 0.4f else 1.0f)

   Surface(shape = RoundedCornerShape(
       topLeft = radius,
       topRight = radius,
       bottomRight = radius,
       bottomLeft = radius
   )) {
       Toggleable(
           value = selected,
           onValueChange = onValueChange,
           modifier = Modifier.ripple()
       ) {

           Image(
               asset = imageResource(R.drawable.androidstudio),
               modifier = Modifier.preferredSize(200.dp,200.dp),
               contentScale = ContentScale.Crop,
               alpha = selectAlpha
           )
       }
   }
}

动画使用animate Compose函数来完成,只需要为它提供不同的target的值,它就能帮你完成之间的变化,一旦动画创建,它就和普通的Compose函数是一样的。

注意一点animate创建的动画是不能被取消的,要创建可以被取消的动画可以使用animatedValue。还有其他两个相似动画函数:animatedFloat,animatedColor

啥?你说看起来有点熟悉?那可不是嘛,ObjectAnimator,ValueAnimator, 你细品,更多关于动画的使用方式这里不展开了,有兴趣的同学下来自己动手试试。

4. 与原生View 的兼容

一门新的语言,一个新的框架,考虑兼容是很有必要的,就像Kotlin那样,我们使用Kotlin不必一下子重写整个项目,你可以添加一个新的类,一个新的模块中使用Kotlin,因为它们与Java 完全相互调用。

Jetpack Compose 借鉴了经验,我们要使用Jetpack Compose,也可以慢慢来,以前的代码不用动,在你的新模块中一点一点的添加,这就涉及到与原来的View的兼容,在Compose中,可以使用AndroidView来兼容以前的Views。

比如我的Jetpack Compose 中要使用到Webview,而它本身也没有提供,该如何是好?别担心,用原来的就行。

首先,创建一个xml文件webview.xml,里面添加Webview 布局:

<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</WebView>

然后写一个compose 函数,使用AndroidView 来加载:

@Composable
fun androidView(){
    AndroidView(R.layout.webview){ view ->
        val webView = view as WebView
        webView.settings.javaScriptEnabled =true
        webView.settings.allowFileAccess = true
        webView.settings.allowContentAccess = true
        webView.loadUrl("https://juejin.im/")
    }
}

加载了一个原生的Webview,然后在webview中加载了掘金的网址,效果如下:

看一下AndroidView函数签名:

@Composable
// TODO(popam): support modifiers here
fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) {
    AndroidViewHolder(
        postInflationCallback = postInflationCallback,
        resId = resId
    )
}

接受一个布局文件资源id,和一个回调postInflationCallback,当View被inflate出来后,会调用这个回调,然后你就可以在回调中使用它了。

但是,注意:回调通常是在主线程被调用。

5.总结

总的来说,这次Developer PreView2 更新比较多,并且很多API发生了变化,增加了一些关键的组件如AdapterList,ConstraintLayout动画组件等,使用方式也与Preview1有很多不同。可以来看一下Google关于Jetpack Compose 上的时间表:

  • 2019.5 宣布Jetpack Compose
  • 2019.11 第一个 Developer Preview
  • 2020.6 第二个 Developer Preview
  • 2020 夏天将发布Alpha版本
  • 2021 将发布release 1.0版本

但是,要说的是,现在很多API还不是最终版本,可以看到,每一个打版本的变化还是蛮大的,现在仍然不能用在商用项目上。但是就jetpack Compose 本身来说,个人还是比较期待的,从上面的时间表就可以看到,大概明年就能出第一个release版本,敬请期待吧!

对了,最新版本的Jetpack Compose 需要Android Studio 4.2以上版本才能使用,想要体验的同学先安卓Android Studio 4.2 Canary ​版本。​去官网下载!

小版本日志列表请看:https://developer.android.com...

youtobe视频介绍请看:https://www.youtube.com/watch...

查看原文

赞 3 收藏 2 评论 0

依然饭特稀西 发布了文章 · 8月28日

【译】使用Kotlin从零开始写一个现代Android 项目-Part4,系列终结篇!

这是本系列的第四篇文章,还没有看过前面三篇的读者可以先看看:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

【译】使用Kotlin从零开始写一个现代Android 项目-Part3

正文开始!

什么是依赖注入

让我们先看看GitRepoRepository类:

class GitRepoRepository(private val netManager: NetManager) {

    private val localDataSource = GitRepoLocalDataSource()
    private val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(): Observable<ArrayList<Repository>> {
      ...
    }
}

我们可以说GitRepoRepository依赖三个对象,分别是netManagerlocalDataSourceremoteDataSource。通过构造函数提供netManager时,数据源在GitRepoRepository中被初始化。换句话说,我们将netManager注入到GitRepoRepository

依赖注入是一个非常简单的概念:你需要什么,其他人就给你提供什么。

让我们看看,我们在哪里构造GitRepoRepository类(Mac上用cmd + B,Windows上用alt + B):

如你所见,GitRepoRepository类在MainViewModel中被构造,NetManager也是在这儿被构造,是否也应该将它们注入ViewModel?是的。应该将GitRepoRepository实例提供给ViewModel,因为GitRepoRepository可以在其他ViewModel中使用。

另一方面,我们确定整个应用程序仅应创建一个NetManager实例。让我们通过构造函数提供它。我们期望有这样的东西:

class MainViewModel(private var gitRepoRepository: GitRepoRepository) : ViewModel() {
  ...
}

请记住,我们没有在MainActivity中创建MainViewModel。我们从ViewModelProviders来获得它:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        ...
    }
    
  ...
}

如前所述,ViewModelProvider将创建新的ViewModel或返回现有的ViewModel。现在,我们必须将GitRepoRepository作为参数。该怎么做?

我们需要为MainViewModel设置特殊的工厂(Factory)类,因为我们不能使用标准的类:

class MainViewModelFactory(private val repository: GitRepoRepository) 
         : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(repository) as T
        }

        throw IllegalArgumentException("Unknown ViewModel Class")
    }

}

因此,现在我们可以在构造它时,设置参数,

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
    ....

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val repository = GitRepoRepository(NetManager(applicationContext))
        val mainViewModelFactory = MainViewModelFactory(repository)
        val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
                            .get(MainViewModel::class.java)

        ...
    }

    ...
}

等等!我们还是没有解决问题,我们真的应该在MainActivity中创建一个MainViewModelFactory实例吗?不因该的,这里应该使用依赖注入来解决。

让我们创建一个Injection类,它具有将提供所需实例的方法:

object Injection {

    fun provideMainViewModelFactory(context: Context) : MainViewModelFactory{
        val netManager = NetManager(context)
        val repository = GitRepoRepository(netManager)
        return MainViewModelFactory(repository)
    }
}

现在,我们可以将其从此类注入MainActivity.kt

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    private lateinit var mainViewModelFactory: MainViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
      
        mainViewModelFactory = Injection.provideMainViewModelFactory(applicationContext)
        val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
                            .get(MainViewModel::class.java)
        
        ...

    }
    ...
}

因此,现在我们的Activity不知道来自应用程序数据层的repositories。这样的代码组织对我们有很大帮助,尤其是在测试应用程序方面。这样,我们将UI逻辑与业务逻辑分开。

我们可以在Injection.kt中应用更多的依赖注入概念:

object Injection {
    
    private fun provideNetManager(context: Context) : NetManager {
        return NetManager(context)
    }

    private fun gitRepoRepository(netManager: NetManager) :GitRepoRepository {
        return GitRepoRepository(netManager)
    }

    fun provideMainViewModelFactory(context: Context): MainViewModelFactory {
        val netManager = provideNetManager(context)
        val repository = gitRepoRepository(netManager)
        return MainViewModelFactory(repository)
    }
    
}

现在,每个类都有获取它们实例的方法了,如果你仔细看,你会发现,所有的这些方法在我们调用它们时,都会返回一个新的实例,真的应该这样?每当我们某个Repository类中需要时,都要创建NetManager的新实例?当然不是,每个应用程序只需要一个NetManager实例。可以说NetManager应该是单例。

在软件工程中,单例模式是一种将类的实例化限制为一个对象的软件设计模式。

让我们实现它:

object Injection {

    private var NET_MANAGER: NetManager? = null

    private fun provideNetManager(context: Context): NetManager {
        if (NET_MANAGER == null) {
            NET_MANAGER = NetManager(context)
        }
        return NET_MANAGER!!
    }

    private fun gitRepoRepository(netManager: NetManager): GitRepoRepository {
        return GitRepoRepository(netManager)
    }

    fun provideMainViewModelFactory(context: Context): MainViewModelFactory {
        val netManager = provideNetManager(context)
        val repository = gitRepoRepository(netManager)
        return MainViewModelFactory(repository)
    }
}

这样,我们确保每个应用程序只有一个实例。换句话说,我们可以说NetManager实例具有Application同样的生命周期范围。

让我们看看依赖图:

为什么我们需要Dagger?

如果看一下上面的注入,您会发现,如果图中有很多依赖项,那么我们将需要做大量工作。 Dagger帮助我们以简单的方式管理依赖项及其范围。

让我们先引入dagger:

...
dependencies {
    ...
    
    implementation "com.google.dagger:dagger:2.14.1"
    implementation "com.google.dagger:dagger-android:2.14.1"
    implementation "com.google.dagger:dagger-android-support:2.14.1"
    kapt "com.google.dagger:dagger-compiler:2.14.1"
    kapt "com.google.dagger:dagger-android-processor:2.14.1"
    
    ...
}

要使用dragger,我们需要新建一个Application继承自DaggerApplication类,我们创建一个DaggerApplication:

class ModernApplication : DaggerApplication(){

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        TODO("not implemented")
    }

}

在继承DaggerApplication()时,它需要实现applicationInjector()方法,该方法应返回AndroidInjector的实现。稍后我将介绍AndroidInjector。

不要忘了在AndroidManifest.xml注册application:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.mladenrakonjac.modernandroidapp">
  
    ...

    <application
        android:name=".ModernApplication"
        ...>
       ...
    </application>

</manifest>

首先,创建AppModule,Modules是具有@Provides注解功能的类。我们说这些方法是提供者,因为它们提供了实例。要将某个类作为模块,我们需要使用@Module注解对该类进行注解。这些注解可帮助Dagger制作和验证图形。我们的AppModule将仅具有提供应用程序上下文的函数:

@Module
class AppModule{

    @Provides
    fun providesContext(application: ModernApplication): Context {
        return application.applicationContext
    }
}

现在,我们创建一个component:

@Singleton
@Component(
        modules = [AndroidSupportInjectionModule::class,
            AppModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

Component是一个接口,我们在其中指定应从哪些模块中将实例注入哪些类中。这个例子中,我们指定AppModuleAndroidSupportInjectionModule

AndroidSupportInjectionModule是可帮助我们将实例注入Android生态系统类的模块,这些类包括 ActivityFragmentServiceBroadcastReceiversContentProviders

因为我们要使用我们的组件来注入这些类,因此AppComponent必须继承AndroidInjector <T>。对于T,我们使用ModernApplication类。如果打开AndroidInjector接口,则可以看到:

abstract class Builder<T> implements AndroidInjector.Factory<T> {
    @Override
    public final AndroidInjector<T> create(T instance) { ... }
    public abstract void seedInstance(T instance);
    ...
  }
}

Builder有两个方法:create(T instance) 用于创建AndroidInjector,而seedInsance(T instance) 方法则用于提供实例。

在我们的例子中,我们将创建具有ModernApplication实例的AndroidInjector,并将在需要的地方提供该实例。

@Component.Builder
abstract class Builder : AndroidInjector.Builder<ModernApplication>()

关于我们的AppComponent,总结一下:

  • 我们拥有AppComponent,它是继承与AndroidInjector的应用程序的主要组件
  • 当我们要构建Component时,我们将需要使用ModernApplication类的实例作为参数。
  • 将以AppComponent中使用的模块形式,向所有其他@Provides方法提供ModernApplication的实例。例如,将向AppModule中的providerContext(application:ModernApplication)方法提供ModernApplication的实例。

现在,我们编译一下项目

当构建结束,Dragger将自动生成一些新的类,对于AppComponent,Dragger将会生成一个DaggerAppComponent类。

让我们回到ModernApplication并创建应用程序的主要组件。创建的组件应在applicationInjector()方法中返回。

class ModernApplication : DaggerApplication(){

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.builder().create(this)
    }

现在,我们完成了Dagger所需的标准配置。

当我们想将实例注入MainActivity类时,我们需要创建MainActivityModule

@Module
internal abstract class MainActivityModule {

    @ContributesAndroidInjector()
    internal abstract fun mainActivity(): MainActivity

}

@ContributesAndroidInjector注解可帮助Dagger连接所需的内容,以便我们可以将实例注入指定的Activity中。

如果返回到我们的Activity,可以看到我们使用Injection类注入了MainViewModelProvider。因此,我们需要在MainActivityModule中提供provider方法,该方法将提供MainViewModelProvider

@Module
internal abstract class MainActivityModule {
    
    @Module
    companion object {

       @JvmStatic
       @Provides
       internal fun providesMainViewModelFactory(gitRepoRepository: GitRepoRepository)
        : MainViewModelFactory {
          return MainViewModelFactory(gitRepoRepository)
       }
     }

    @ContributesAndroidInjector()
    internal abstract fun mainActivity(): MainActivity

}

但是,谁将提供GitRepoRepository给providesMainViewModelFactoty方法呢?

有两个选择:我们可以为其创建provider方法并返回新实例,或者可以使用@Inject注解它的构造函数

让我们回到我们的GitRepoRepository并使用@Inject注解来标注其构造函数:

class GitRepoRepository @Inject constructor(var netManager: NetManager) {
  ...
}

因为GitRepoRepository需要NetManager,因此,同样标注NetManager的构造函数

@Singleton
class NetManager @Inject constructor(var applicationContext: Context) {
   ...
}

我们使用@Singleton注解设置NetManager为单例。另外,NetManager需要applicationContext。 AppModule中有一个方法来提供它。

不要忘记将MainActivityModule添加到AppComponent.kt中的模块列表中:


@Component(
        modules = [AndroidSupportInjectionModule::class,
            AppModule::class,
            MainActivityModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

最后,我们需要将其注入到我们的MainActivity中。为了使Dagger在那里工作,我们的MainActivity需要继承DaggerAppCompatActivity

class MainActivity : DaggerAppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
    ...
    @Inject lateinit var mainViewModelFactory: MainViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProviders.of(this, mainViewModelFactory)
                .get(MainViewModel::class.java)
        ...
       }

    ...
}

要注入MainViewModelFactory实例,我们需要使用@Inject注解。

重要说明:mainViewModelFactory变量必须是公共的。

到这儿就完成了!

不再需要从“注入”类进行注入:

mainViewModelFactory = Injection.provideMainViewModelFactory(applicationContext)

实际上,我们可以删除Injection类了,因为我们现在正在使用Dagger了。

一步步回头看看
  • 我们想把MainViewModelFactory注入MainActiivty
  • 为了使Dragger能在MainActivity中正常工作,MainActivity需要继承自DaggerAppCompatActivity
  • 我们需要使用@Inject注解对mainViewModelFactory进行标注
  • Dagger搜索带有@ContributesAndroidInjector注解的方法的模块,该方法返回MainActivity。
  • Dagger搜索返回MainViewModelFactory实例的provider,或带@Inject注解的构造函数。
  • provideMainViewModelFactory() 返回实例,但是为了创建它,需要GitRepoRepository实例
  • Dagger搜索provider或@Inject带注解的构造函数,该构造函数返回GitRepoRepository实例。
  • GitRepoRepository类具有带@Inject注解的构造函数。但是该构造函数需要NetManager实例
  • Dagger搜索返回NetManager实例的provider或带@Inject注释的构造函数。
  • Dagger搜索返回Application Context实例的provider。
  • AppModule具有返回application context 的provider方法。但是该构造函数需要ModernApplication实例。
  • AndroidInjector具有provider。

就是这样!

有一种更好的自动化方法来提供ViewModelFactory

问题:对于每个具有参数的ViewModel,我们都需要创建ViewModelFactory类。在Chris Banes的Tivi应用程序源代码中,我发现了一种非常好的自动方法。

创建ViewModelKey.kt :

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

然后添加一个DaggerAwareViewModelFactory类:

class DaggerAwareViewModelFactory @Inject constructor(
        private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                    break
                }
            }
        }
        if (creator == null) {
            throw IllegalArgumentException("unknown model class " + modelClass)
        }
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

创建ViewModelBuilder module:

@Module
internal abstract class ViewModelBuilder {

    @Binds
    internal abstract fun bindViewModelFactory(factory: DaggerAwareViewModelFactory):
            ViewModelProvider.Factory
}

添加ViewModelBuilderAppComponent:

@Singleton
@Component(
        modules = [AndroidSupportInjectionModule::class,
            AppModule::class,
            ViewModelBuilder::class,
            MainActivityModule::class])
interface AppComponent : AndroidInjector<ModernApplication> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<ModernApplication>()
}

MainViewModel类添加@Injec:

class MainViewModel @Inject constructor(var gitRepoRepository: GitRepoRepository) : ViewModel() {
  ...
}

从现在开始,我们只需要将其绑定到Activity模块即可:

@Module
internal abstract class MainActivityModule {

    @ContributesAndroidInjector
    internal abstract fun mainActivity(): MainActivity

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel::class)
    abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}

不需要MainViewModelFactory provider 。实际上,根本不需要MainViewModelFactory.kt,因此可以将其删除。

最后,在MainActivity.kt中对其进行更改,以便我们使用ViewModel.Factory类型而不是MainViewModelFactory

class MainActivity : DaggerAppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
  
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory

    override fun onCreate(savedInstanceState: Bundle?) {
      ...

        val viewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(MainViewModel::class.java)
       ...
    }
    ...
}

感谢Chris Banes 这个神奇的解决方案!

译者注:本来,这个系列还有一篇文章,讲Retrofit + Room的运用,不过好像原作者断更了😂😂😂,因此本篇就将是最后一篇了,本系列总共4篇,建议大家看完,你会有收获的!

以上就是本文的全部内容,感谢你的阅读!

查看原文

赞 3 收藏 3 评论 0

依然饭特稀西 发布了文章 · 8月27日

【译】使用Kotlin从零开始写一个现代Android 项目-Part3


这是《使用Kotlin开发一个现代的APP》系列文章的第三部分,还没看过前2部分的,可以先看一下:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

正文开始!

什么是RxJava ?

关于RxJava,一个广泛的概念是-RxJava是用于异步编程的API的Java实现,它具有可观察流和响应式的API。实际上,它是这三个概念的结合:观察者模式、迭代器模式和函数式编程。这里也有其他编程语言实现的库,如:RxSwift、RxJs 、RxNet等。

我RxJava上手很难,有时,它确实很令人困惑,如果实施不当,可能会给您带来一些问题。尽管如此,我们还是值得花时间学习它。我将尝试通过简单的步骤来解释RxJava。

首先,让我们回答一些简单的问题,当您开始阅读有关RxJava时,可能会问自己:

我们真的需要它吗?

答案是否定的,RxJava只是可以在Android开发中使用的又一个库。如果使用Kotlin开发,它也不是必须的,我希望你明白我说的,它只一个很帮助你的库,就像你使用的所以其他库一样。

要学习RxJava2,必须先学RxJava1吗?

你可以直接从RxJava2开始,不过,作为Android开发人员,知道这两种情况对你还是有好处的,因为你可能会参与维护其他人的RxJava1代码。

我看到有RxAndroid,应该使用RxAndroid还是RxJava?

RxJava能用在任何Java开发平台,不仅仅是Android,比如,对于后端开发来说,RxJava 可以与Spring等框架一起使用,RxAndroid是一个库,其中包含在Android中使用RxJava所需的库。因此,如果要在Android开发中使用RxJava,则必须再添加RxAndroid。稍后,我将解释RxAndroid基于RxJava所添加的内容。

我们使用Kotlin开发,为什么不用RxKotin呢?

我们没有必要另外再添加一个Rx 库了,因为Kotlin与Java是完全兼容的,这里确实有一个RxKotin库:https://github.com/ReactiveX/... ,不过该库是在RxJava之上编写的。它只是将Kotlin功能添加到RxJava。您可以将RxJava与Kotlin一起使用,而无需使用RxKotlin库。为了简单起见,在这一部分中我将不使用RxKotlin。

如何将Rxjava2添加到项目中?

要使用RxJava,你需要在build.gradle中添加如下代码:

dependencies {
    ... 
    implementation "io.reactivex.rxjava2:rxjava:2.1.8"
    implementation "io.reactivex.rxjava2:rxandroid:2.0.1"
    ...
}

然后,点击sync,下载Rxjava库。

RxJava包含了些啥?

我想把RxJava分为以下三部分:

  • 1、用于观察者模式和数据流的类:ObservablesObservers
  • 2、Schedulers
  • 3、数据流操作符
ObservablesObservers

我们已经解释了这种模式。您可以将Observable视为数据的源(被观察者),将Observer视为接收数据的源(观察者)。

有很多创建Observables的方式,最简单的方法是使用Observable.just()来获取一个项目并创建Observable来发射该项目。

让我们转到GitRepoRemoteDataSource类并更改getRepositories方法,以返回Observable:

class GitRepoRemoteDataSource {

    fun getRepositories() : Observable<ArrayList<Repository>> {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First from remote", "Owner 1", 100, false))
        arrayList.add(Repository("Second from remote", "Owner 2", 30, true))
        arrayList.add(Repository("Third from remote", "Owner 3", 430, false))

        return Observable.just(arrayList).delay(2,TimeUnit.SECONDS)
    }
}

Observable <ArrayList <Repository >>表示Observable发出Repository对象的数组列表。如果要创建发出Repository对象的Observable <Repository>,则应使用Observable.from(arrayList)

.delay(2,TimeUnit.SECONDS)表示延迟2s后才开始发射数据。

但是,等等!我们并没有高数Observable何时发射数据啊?Observables通常在一些Observer订阅后就开始发出数据。

请注意,我们不再需要以下接口了

interface OnRepoRemoteReadyCallback {
    fun onRemoteDataReady(data: ArrayList<Repository>)
}

GitRepoLocalDataSource:类中做同样的更改

class GitRepoLocalDataSource {

    fun getRepositories() : Observable<ArrayList<Repository>> {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First From Local", "Owner 1", 100, false))
        arrayList.add(Repository("Second From Local", "Owner 2", 30, true))
        arrayList.add(Repository("Third From Local", "Owner 3", 430, false))

        return Observable.just(arrayList).delay(2, TimeUnit.SECONDS)
    }

    fun saveRepositories(arrayList: ArrayList<Repository>) {
        //todo save repositories in DB
    }
}

同样的,也不需要这个接口了:

interface OnRepoLocalReadyCallback {
    fun onLocalDataReady(data: ArrayList<Repository>)
}

现在,我们需要在repository中返回Observable

class GitRepoRepository(private val netManager: NetManager) {

    private val localDataSource = GitRepoLocalDataSource()
    private val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(): Observable<ArrayList<Repository>> {

        netManager.isConnectedToInternet?.let {
            if (it) {
                //todo save those data to local data store
                return remoteDataSource.getRepositories()
            }
        }

        return localDataSource.getRepositories()
    }
}

如果网络已连接,我们从远程数据源返回Observable,否则,从本地数据源返回Observable,同样的,我们也不再需要OnRepositoryReadyCallback接口。

如你所料,我们需要更改在MainViewModel中获取数据的方式。现在我们应该从gitRepoRepository获取Observable并订阅它。一旦我们向Observer订阅了该Observable,Observable将开始发出数据:

class MainViewModel(application: Application) : AndroidViewModel(application) {
    ...

    fun loadRepositories() {
        isLoading.set(true)
        gitRepoRepository.getRepositories().subscribe(object: Observer<ArrayList<Repository>>{
            override fun onSubscribe(d: Disposable) {
                //todo
            }

            override fun onError(e: Throwable) {
               //todo
            }

            override fun onNext(data: ArrayList<Repository>) {
                repositories.value = data
            }

            override fun onComplete() {
                isLoading.set(false)
            }
        })
    }
}

一旦Observer订阅了Observable,onSubscribe方法将被调用,主要onSubscribe的参数Disposable,稍后将讲到它。

每当Observable发出数据时,将调用onNext()方法。当Observable完成s数据发射时,onComplete()将被调用一次。之后,Observable终止。

如果发生某些异常,onError()方法将被回调,然后Observable终止。这意味着Observable将不再发出数据,因此onNext()不会被调用,也不会调用onComplete()

另外,请注意。如果尝试订阅已终止的Observable,则将收到IllegalStateException

那么,RxJava如何帮助我们?

  • 首先,我们摆脱了这些接口,它是所有repository和数据源建立的样板接口。
  • 如果我们使用接口,并且在数据层中发生某些异常,则我们的应用程序可能会崩溃。使用RxJava错误将在onError()方法中返回,因此我们可以向用户显示适当的错误消息。
  • 因为我们始终将RxJava用于数据层,它更清晰。
  • 我之前没有告诉过你:以前的方法可能会导致内存泄漏。
使用RxJava2和ViewModel时,如何防止内存泄漏

我们再一次看一下ViewModel的生命周期图

一旦Activity销毁,ViewModel的onCleared方法将被调用,在onCleared方法中,我们需要取消所有订阅

class MainViewModel(application: Application) : AndroidViewModel(application) {
    ...
    
    lateinit var disposable: Disposable

    fun loadRepositories() {
        isLoading.set(true)
        gitRepoRepository.getRepositories().subscribe(object: Observer<ArrayList<Repository>>{
            override fun onSubscribe(d: Disposable) {
                disposable = d
            }

            override fun onError(e: Throwable) {
                //if some error happens in our data layer our app will not crash, we will
                // get error here
            }

            override fun onNext(data: ArrayList<Repository>) {
                repositories.value = data
            }

            override fun onComplete() {
                isLoading.set(false)
            }
        })
    }

    override fun onCleared() {
        super.onCleared()
        if(!disposable.isDisposed){
            disposable.dispose()
        }
    }
}

我们可以优化一下上面的代码:

首先,使用DisposableObserver替换Observer,它实现了Disposable并且有dispose()方法,我们不再需要onSubscribe()方法,因为我们可以直接在DisposableObserver实例上调用dispose()

第二步,替换掉返回Void的.subscribe()方法,使用.subscribeWith() 方法,他能返回指定的Observer

class MainViewModel(application: Application) : AndroidViewModel(application) {
    ...

    lateinit var disposable: Disposable

    fun loadRepositories() {
        isLoading.set(true)
        disposable = gitRepoRepository.getRepositories().subscribeWith(object: DisposableObserver<ArrayList<Repository>>() {

            override fun onError(e: Throwable) {
               // todo
            }

            override fun onNext(data: ArrayList<Repository>) {
                repositories.value = data
            }

            override fun onComplete() {
                isLoading.set(false)
            }
        })
    }

    override fun onCleared() {
        super.onCleared()
        if(!disposable.isDisposed){
            disposable.dispose()
        }
    }
}

上面的代码还可以继续优化:

我们保存了一个Disposable实例,因此,我们才可以在onCleared()回调中调用dispose(),但是等等!我们需要为每一个调用都这样做吗?如果有10个回调,那么我们得保存10个实例,在onCleared()中取消10次订阅?显然不可能,这里有更好的方法,我们应该将它们全部保存在一个存储桶中,并在调用onCleared()方法时,将它们全部一次处理。我们可以使用CompositeDisposable

CompositeDisposable:可容纳多个Disposable的容器

因此,每次创建一个Disposable,都需要将其添加到CompositeDisposable中:

class MainViewModel(application: Application) : AndroidViewModel(application) {
    ...
  
    private val compositeDisposable = CompositeDisposable()

    fun loadRepositories() {
        isLoading.set(true)
        compositeDisposable.add(gitRepoRepository.getRepositories().subscribeWith(object: DisposableObserver<ArrayList<Repository>>() {

            override fun onError(e: Throwable) {
                //if some error happens in our data layer our app will not crash, we will
                // get error here
            }

            override fun onNext(data: ArrayList<Repository>) {
                repositories.value = data
            }

            override fun onComplete() {
                isLoading.set(false)
            }
        }))
    }

    override fun onCleared() {
        super.onCleared()
        if(!compositeDisposable.isDisposed){
            compositeDisposable.dispose()
        }
    }
}

感谢Kotlin的扩展函数,我们还可以更进一步:

与C#和Gosu相似,Kotlin提供了使用新功能扩展类的能力,而不必继承该类,也就是扩展函数。

让我们创建一个新的包,叫做extensions,并且添加一个新的文件RxExtensions.kt

import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

operator fun CompositeDisposable.plusAssign(disposable: Disposable) {
    add(disposable)
}

现在我们可以使用+ =符号将Disposable对象添加到CompositeDisposable实例:

class MainViewModel(application: Application) : AndroidViewModel(application) {
    ...

    private val compositeDisposable = CompositeDisposable()

    fun loadRepositories() {
        isLoading.set(true)
        compositeDisposable += gitRepoRepository.getRepositories().subscribeWith(object : DisposableObserver<ArrayList<Repository>>() {

            override fun onError(e: Throwable) {
                //if some error happens in our data layer our app will not crash, we will
                // get error here
            }

            override fun onNext(data: ArrayList<Repository>) {
                repositories.value = data
            }

            override fun onComplete() {
                isLoading.set(false)
            }
        })
    }

    override fun onCleared() {
        super.onCleared()
        if (!compositeDisposable.isDisposed) {
            compositeDisposable.dispose()
        }
    }
}

现在,我们运行程序,当你点击Load Data按钮,2s之后,程序crash,然后,如果查看日志,您将看到onNext方法内部发生错误,并且异常的原因是:

java.lang.IllegalStateException: Cannot invoke setValue on a background thread

为何会发生这个异常?

Schedulers

RxJava附带有调度器(Schedulers),使我们可以选择在哪个线程代码上执行。更准确地说,我们可以选择使用subscribeOn()方在哪个线程执行,observeOn()方法可以观察哪个线程观察者。通常情况下,我们所有的数据层代码都应该在后台线程执行,例如,如果我们使用Schedulers.newThread(),每当我们调用它时,调度器都会给我们分配一个新的线程,为了简单起见,Scheduler中还有其他一些方法,我将不在本博文中介绍。

可能您已经知道所有UI代码都是在Android 主线程上完成的。 RxJava是Java库,它不了解Android主线程,这就是我们使用RxAndroid的原因。 RxAndroid使我们可以选择Android Main线程作为执行代码的线程。显然,我们的Observer应该在Android Main线程上运行。

让我们更改一下代码:

...
fun loadRepositories() {
        isLoading.set(true)
        compositeDisposable += gitRepoRepository
                .getRepositories()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(object : DisposableObserver<ArrayList<Repository>>() {
              ...
        })
    }
...

然后再运行代码,一切都正常了,nice~

其他 observables types

这里还有一些其他的observable 类型

  • Single<T>: 被观察者仅发射一个数据,或者是一个异常
  • Maybe<T>: 被观察者不发射数据,或者仅发射一个数据,或者是一个异常
  • Completable : 发射onSuccess()事件或者异常
  • Flowable<T>:和 Observable<T> 一样,不发射数据,或者发射n个数据,或者发射异常,但是Observable不支持背压,而Flowable却支持。
什么是背压(backpressure)?

为了记住一些概念,我喜欢将它们与现实中的一些例子类比

把它类比成一个通道,如果你向通道中塞入瓶颈能够接受的最多的商品,这将会变得很糟,这里也是同样的,有时,你的观察者无法处理其收到的事件数量,因此需要放慢速度。

你可以看看RxJava 关于背压的文档:https://github.com/ReactiveX/...

操作符

RxJava中,最牛逼的就是它的操作符了,仅用一行代码即可在RxJava中解决一些通常需要10行或更多行的问题。这些是操作符可以帮我们做的:

  • 合并observables
  • 过滤
  • 按条件来做操作
  • 将observables 转换为其他类型

我给你举一个例子,让我们将数据保存到GitRepoLocalDataSource中。因为我们正在保存数据,所以我们需要Completable来模拟它。假设我们还想模拟1秒的延迟。天真的方法是:

fun saveRepositories(arrayList: ArrayList<Repository>): Completable {
    return Completable.complete().delay(1,TimeUnit.SECONDS)
}

为什么说天真?

Completable.complete()返回一个Completable实例,该实例在订阅后立即完成。

一旦Completable 完成后,它将终止。因此,之后将不执行任何运算符(延迟是运算符之一)。在这种情况下,我们的Completable不会有任何延迟。让我们找解决方法:

fun saveRepositories(arrayList: ArrayList<Repository>): Completable {
    return Single.just(1).delay(1,TimeUnit.SECONDS).toCompletable()
}

为什么是这种方式?

Single.just(1)创建一个Single实例,并且仅发射一个数字1,因为我们用了delay(1,TimeUnit.SECONDS) ,因此发射操作延迟1s。

toCompletable()返回一个Completable,它丢弃Single的结果,并在此Single调用onSuccess时调用onComplete

因此,上面的代码将返回Completable,并且1s后调用onComplete()

现在,我们应该更改我们的GitRepoRepository。让我们回顾一下逻辑。我们检查互联网连接。如果有互联网连接,我们从远程数据源获取数据,将其保存在本地数据源中并返回数据。否则,我们仅从本地数据源获取数据。看一看:

fun getRepositories(): Observable<ArrayList<Repository>> {

    netManager.isConnectedToInternet?.let {
        if (it) {
            return remoteDataSource.getRepositories().flatMap {
                return@flatMap localDataSource.saveRepositories(it)
                        .toSingleDefault(it)
                        .toObservable()
            }
        }
    }

    return localDataSource.getRepositories()
}

使用了.flatMap,一旦remoteDataSource.getRepositories()发射数据,该项目将被映射到发出相同项目的新Observable。我们从Completable创建的新Observable发射的相同项目保存在本地数据存储中,并且将其转换为发出相同发射项的Single。因为我们需要返回Observable,所以我们必须将Single转换为Observable。

很疯狂,huh? 想象一下RxJava还能为我们做些啥!

RxJava是一个非常有用的工具,去使用它,探索它,我相信你会爱上它的!

以上就是本文得全部内容,下一篇文章将是本系列的最后一篇文章,敬请期待!

本系列已更新完毕:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

【译】使用Kotlin从零开始写一个现代Android 项目-Part3

【译】使用Kotlin从零开始写一个现代Android 项目-Part4

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你

查看原文

赞 0 收藏 0 评论 0

依然饭特稀西 发布了文章 · 8月24日

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

接着上一篇文章:使用Kotlin从零开始写一个现代Android 项目-Part1

5. MVVM架构+Repository模式+Android Manager

5.1 关于Android中的架构

长期以来,Android开发的项目中很少有架构,但是在过去几年,架构在各大Android社区广泛宣传。Activity即一切的时代过去了,Google发布了一个仓库叫做 Android Architecture Blueprints,它包含了许多示例和不同架构的说明。最后,在Google IO/17大会上,发布了 Android Architecture Components系列架构组件,可以帮助我们写更简洁、高质量的应用程序。你可以使用一个全部组件或者其中一个来构建你的应用程序,不过,我发现它们都非常有用,因此,本文剩下的部分和后面2部分中,我将介绍如何使用这些组件。首先,我将写一些有问题的代码,然后使用这些组件来重构,以看看这些库能帮我们解决什么问题。

这里主要有两种架构模式

  • MVP
  • MVVM

很难说它两谁更好,你应该都试试以后再决定。我个人更喜欢带有生命周期组件的MVVM架构,本系列将围绕它来介绍,如果你还没有使用过MVP架构,Medium上有很多关于它的好文章,你可以去看看。

5.2 什么是MVVM模式?

MVVM模式是一种架构模式。它代表Model-View-ViewModel。我认为这个名称会使开发人员感到困惑。如果我来命名它的话,我会将其命名为View-ViewModel-Model,因为ViewModel是连接View和Model的中间人。

其中View是对你的Activity/Fragment/或者其他自定义View的抽象名称,请注意,不要将它与Android 的View混为一谈,这非常重要。View应该是干净的,在View中,不应该包含任何逻辑代码,也不应该持有任何数据,他应该持有一个ViewModel实例,所有的数据都应该从实例中去获取。此外,View应该观察这些数据,并且当ViewModel中的数据更改时,布局也应该刷新一次。总之,View的职责是:布局如何查找不同的数据和状态。

ViewModel是保存数据的类的抽象名称,并具有何时应获取数据以及应何时显示数据的逻辑。 ViewModel保持当前状态。此外,ViewModel应该保持一个或者多个Model实例,所有的数据都应该从这些Model实例获取。例如,ViewModel不应该知道数据是来自数据库还是远程服务器。此外,ViewModel完全不应该了解View。而且,ViewModel也完全不应该了解Android框架层的东西。

Model是数据层的抽象名称。这是我们将从远程服务器获取数据并将其缓存在内存中或保存在本地数据库中的类。但是请注意,这里的Model和CarUserSquare 这些model类是不一样,这些数据模型类仅仅只保持数据,而Model是Repository模式的实现,在后文将介绍,并且Model不应该了解ViewModel。

如果正确实施,MVVM是分离代码并使其更具可测试性的好方法。它有助于我们遵循SOLID原则,因此我们的代码更易于维护。

代码示例

现在,我将写一个最简单的例子来说明它是如何工作的

首先,让我们创建一个简单的Model,该Model返回一些字符串:

class RepoModel {

    fun refreshData() : String {
        return "Some new data"
    }
}

通常,获取数据是异步调用,因此我们必须等待加载数据。为了模拟它,我将类更改为以下内容:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

首先,我们创建了一个接口OnDataReadyCallback,它有一个方法onDataReady,然后将OnDataReadyCallback作为refreshData的参数,用Handler来模拟等待,当2000ms后,调用接口实例的onDataReady方法。

让我们看一下ViewModel:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false
}

如你所见,这里有一个RepoModel实例,一个我们要显示的 text,和一个保存状态的boolean值isLoading。现在,我们创建一个refresh方法,该方法负责获取数据

class MainViewModel {
    ...

    val onDataReadyCallback = object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            isLoading.set(false)
            text.set(data)
        }
    }

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(onDataReadyCallback)
    }
}

refresh方法调用了repoModel的refreshData方法,传递了一个onDataReadyCallback。但是等一会,object是什么鬼?每当你要实现某个接口或扩展某些类而不创建子类时,都将使用对象声明。如果要使用它作为匿名类怎么办?在这种情况下,您必须使用对象表达式

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false

    fun refresh() {
        repoModel.refreshData( object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            text = data
        })
    }
}

当我们调用refresh时,我们应该将视图更改为加载状态,一旦数据到来,就应该将isLoading设置为false

另外,我们应该将text更改为ObservableField <String>,并将isLoading更改为ObservableField <Boolean>。 ObservableField是Data Binding库中的一个类,我们可以使用它代替创建Observable对象。它包装了我们想要观察的对象。

class MainViewModel {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField<String>()

    val isLoading = ObservableField<Boolean>()

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(object : OnDataReadyCallback {
            override fun onDataReady(data: String) {
                isLoading.set(false)
                text.set(data)
            }
        })
    }
}

注意,我使用val而不是var,因为我们仅更改字段中的值,而不更改字段本身,如果要初始化它,则应该执行以下操作:

 val text = ObservableField("old data")
 val isLoading = ObservableField(false)

我们更改布局,以让它可以观察textisLoading,首先,我们将绑定MainViewModel而不是Repository:

<data>
    <variable
        name="viewModel"
        type="me.mladenrakonjac.modernandroidapp.MainViewModel" />
</data>

然后,做一下操作:

  • 更改TextView以观察MainViewModel实例上的text
  • 添加仅在isLoadingtrue时可见的ProgressBar
  • 单击的add按钮将从MainViewModel实例调用refresh函数,并且仅在isLoadingfalse时才可单击
...

        <TextView
            android:id="@+id/repository_name"
            android:text="@{viewModel.text}"
            ...
            />

        ...
        <ProgressBar
            android:id="@+id/loading"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            ...
            />

        <Button
            android:id="@+id/refresh_button"
            android:onClick="@{() -> viewModel.refresh()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            />
...

如果此时你运行程序,将会报错,原因是,如果未导入View,则无法使用View.VISIBLEView.GONE。因此,我们必须导入它:

<data>
        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
</data>

ok,布局就到此完成,接下来该完成绑定了,如我们所说,View应该持有一个ViewModel实例:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}

最后,我们可以运行它了。

您可以看到旧数据已更改为新数据

这是最简单的MVVM示例。

对此有一个问题,让我们现在旋转手机:

新数据又变回了旧数据。这怎么可能呢?看一下Activity的生命周期:

旋转屏幕后,将创建Activity的新实例,并调用onCreate()方法。现在,看看我们的Activity:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}

如您所见,一旦创建了一个新的Activity实例,便也会创建一个新的MainViewModel实例。如果以某种方式,我们可以为每个重新创建的MainActivity具有相同的MainViewModel实例,那会很好吗?

Lifecycle-aware 组件介绍

由于许多开发人员都遇到了这个问题,因此Android Framework Team的开发人员决定开发可帮助我们解决这个问题的库。 ViewModel类就是其中之一。它是我们所有ViewModels都应该扩展的类。

让我们的MainViewModel 继承自有生命周期感知的组件ViewModel,首先,我们应该在build.gradle文件中添加该生命周期感知组件库(译者注:版本不是最新,使用时更新最新版本):

dependencies {
    ... 

    implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
    implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
    kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}

MainViewModel继承自ViewModel,如下:

package me.mladenrakonjac.modernandroidapp

import android.arch.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    ...
}

在Activity的onCreate方法中,你应该改为:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.executePendingBindings()

    }
}

请注意,我们并没有创建一个新的MainViewModel实例,我们从ViewModelProvider中获取它,ViewModelProviders是一个工具类,它有获取ViewModel实例的方法。与范围相关,如果你在Activity中调用ViewModelProviders.of(this),则ViewModel会一直存在,知道Activity被彻底销毁(销毁没有被重建),同样的,如果你在Fragment中调用,ViewModel会一直存在,直到Fragment被彻底销毁。看看下图:

ViewModelProvider负责在第一次调用时创建新实例,或在重新创建Activity / Fragment时返回旧实例。

请勿与以下内容混淆:

MainViewModel::class.java

在Kotlin中,如果你向下面这样写:

MainViewModel::class

它将返回一个KClass,它与Java中的Class不同。因此,我们需要加一个.java后缀。

返回与给定KClass实例相对应的Java Class实例。

让我们看一下,旋转屏幕会发生什么?

我们拥有与之前旋转时相同的数据。

在上一篇文章中,我说过我们的应用程序将获取Github仓库列表并显示它。为此,我们必须添加getRepositories函数,该函数将返回mock的仓库列表:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First", "Owner 1", 100 , false))
        arrayList.add(Repository("Second", "Owner 2", 30 , true))
        arrayList.add(Repository("Third", "Owner 3", 430 , false))
        
        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data : ArrayList<Repository>)
}

与之对应,在ViewModel中,也应该有一个函数,该函数调用RepoModel中的getRepositories函数。

class MainViewModel : ViewModel() {
    ...
    var repositories = ArrayList<Repository>()

    fun refresh(){
        ...
    }

    fun loadRepositories(){
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback{
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories = data
            }
        })
    }
}

最后,我们应该在RecyclerView中显示这仓库列表。为此,我们将必须:

  • 添加一个rv_item_repository.xml布局
  • activity_main.xml添加RecyclerView
  • 添加RepositoryRecyclerViewAdapter适配器
  • 给RecycklerView设置Adapter

为了让rv_item_repository.xml使用CardView,需要在build.gradle中添加(译者注:最新的请使用androidx):

implementation 'com.android.support:cardview-v7:26.0.1'

然后,布局像下面这样:

<?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>

        <import type="android.view.View" />

        <variable
            name="repository"
            type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" />
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:layout_margin="8dp">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/repository_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryName}"
                android:textSize="20sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintHorizontal_bias="0.0"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.083"
                tools:text="Modern Android App" />

            <TextView
                android:id="@+id/repository_has_issues"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@string/has_issues"
                android:textStyle="bold"
                android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
                app:layout_constraintBottom_toBottomOf="@+id/repository_name"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1.0"
                app:layout_constraintStart_toEndOf="@+id/repository_name"
                app:layout_constraintTop_toTopOf="@+id/repository_name"
                app:layout_constraintVertical_bias="1.0" />

            <TextView
                android:id="@+id/repository_owner"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryOwner}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_name"
                app:layout_constraintVertical_bias="0.0"
                tools:text="Mladen Rakonjac" />

            <TextView
                android:id="@+id/number_of_starts"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@{String.valueOf(repository.numberOfStars)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_owner"
                app:layout_constraintVertical_bias="0.0"
                tools:text="0 stars" />

        </android.support.constraint.ConstraintLayout>

    </android.support.v7.widget.CardView>

</layout>

下一步是将RecyclerView添加到main_activity.xml,在这之前,别忘了添加:

implementation 'com.android.support:recyclerview-v7:26.0.1'
<?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>

        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="me.fleka.modernandroidapp.MainActivity">

        <ProgressBar
            android:id="@+id/loading"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/repository_rv"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/rv_item_repository" />

        <Button
            android:id="@+id/refresh_button"
            android:layout_width="160dp"
            android:layout_height="40dp"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:onClick="@{() -> viewModel.loadRepositories()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            android:text="Refresh"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0" />

    </android.support.constraint.ConstraintLayout>

</layout>

请注意,我们从之前布局删除了一些TextView元素,并且按钮现在触发loadRepositories函数,而不是refresh:

<Button
    android:id="@+id/refresh_button"
    android:onClick="@{() -> viewModel.loadRepositories()}" 
    ...
    />

然后,我们删除MainViewModel中的refresh函数和RepoModel中的refreshData函数,因为我们不再需要它们了。

现在,我们添加一个Adapter

class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
                                    private var listener: OnItemClickListener)
    : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent?.context)
        val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int)
            = holder.bind(items[position], listener)

    override fun getItemCount(): Int = items.size

    interface OnItemClickListener {
        fun onItemClick(position: Int)
    }

    class ViewHolder(private var binding: RvItemRepositoryBinding) :
            RecyclerView.ViewHolder(binding.root) {

        fun bind(repo: Repository, listener: OnItemClickListener?) {
            binding.repository = repo
            if (listener != null) {
                binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
            }

            binding.executePendingBindings()
        }
    }

}

请注意,ViewHolder 持有的是RvItemRepositoryBinding类型的实例而不是View类型,这样我们就可以在Item中使用Data Binding了,另外,不要被下面这行代码搞迷惑了:

override fun onBindViewHolder(holder: ViewHolder, position: Int)            = holder.bind(items[position], listener)

它只是下面这个函数的简写:

override fun onBindViewHolder(holder: ViewHolder, position: Int){
    return holder.bind(items[position], listener)
}

items [position]是索引运算符的实现。它与items.get(position)相同。

另一行代码也可能让你困惑:

binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })

你可以将参数用_替换,很酷,是吧?

我们添加了适配器,但仍未在MainActivity中将其设置给recyclerView:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)

    }

    override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

很奇怪,这里发生了什么?

  • Activity被创建,因此使用实际为空的repositories创建了新适配器
  • 我们点击了按钮
  • 调用loadRepositories函数,显示进度
  • 2秒后,我们得到了repositories,进度被隐藏了,但是列表却没有显示。这是因为未在适配器上调用notifyDataSetChanged
  • 旋转屏幕后,将创建新的Activity,因此将使用带有一些内容的repositories参数创建新的适配器

因此,MainViewModel应该如何通知MainActivity有关新Item的信息,以便我们可以调用notifyDataSetChanged

这不应该是ViewModel来做的

这非常重要,MainViewModel应该了解MainActivity,MainActivity是拥有MainViewModel实例的,因此它是应该侦听更改并通知Adapter有关更改。

那?到底该如何做呢?

我们要可以观察repositories,当他改变的时候,通知列表更新。

该解决方案有什么问题没?

我们看一下以下场景:

  • 在MainActivity中观察repositories,一旦它放生更改,我们调用notifyDataSetChanged
  • 我们点击按钮
  • 在我们等待数据更改时,由于配置更改,可以重新创建MainActivity。
  • 但我们的MainViewModel仍然存在。
  • 2秒后,“repositories”字段获取新项目,并通知观察者数据已更改
  • 观察者尝试对不再存在的适配器执行notifyDataSetChanged,因为重新创建了MainActivity。

然后程序就崩了,因此上面的方案不够好。我们得引入一个新的组件你LiveData

LiveData介绍

LiveData是另一个生命周期感知组件,它可以观察View的生命周期,因此,一旦Activity由于配置更改而被销毁时,LiveData就会感知到它,然后它就会从被销毁的Activity中取消观察者的订阅。

让我们在MainViewModel中实现它:

class MainViewModel : ViewModel() {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories() {
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}

并观察MainActivity的变化:

class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    private lateinit var binding: ActivityMainBinding
    private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this)


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = repositoryRecyclerViewAdapter
        viewModel.repositories.observe(this,
                Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} })

    }

    override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

上面的it关键字代表什么呢?在Kotlin中,如果,函数只有一个参数,那么它默认你会被替换成it,假设我们有一个乘以2的lamuda表达式:

((a) -> 2 * a) 

可以写成下面这样 :

(it * 2)

现在你运行程序,一切都正常工作了!

我为什么喜欢MVVM而不是MVP

  • 没有那些提供给View的无聊接口,因为ViewModel没有对View的引用
  • 也没有提供给Presenter的接口,因为不需要
  • 它处理配置更改是如此简单
  • 使用MVVM,我们Activity/Fragment中的代码更简洁

Repository 模式

正如我前面所说,Model只是数据层的一个抽象,通常来说,它包含repositories和数据类,每一个实体(data)类应该有一个对应的Repository类。例如,我们有一个User和一个Post数据类,那么也应该对应有UserRepository和PostRepository,所有的数据都应该从Repository处获取。我们不应该在View或者ViewModel中直接使用Shared Preferences或者DB。

因此,我们可以将RepoModel重命名为GitRepoRepository,其中GitRepo来自Github存储库,而Repository来自Repository模式。

class GitRepoRepository {

    fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First", "Owner 1", 100, false))
        arrayList.add(Repository("Second", "Owner 2", 30, true))
        arrayList.add(Repository("Third", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000)
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

好的,MainViewModel从GitRepoRepsitories获取Github 仓库列表,但是GitRepoRepositories数据从何而来?

你可以直接在Repository中对客户端实例或数据库实例进行调用,但这仍然不是一个好习惯。你的应用程序应尽可能模块化。如果你决定使用其他Client,用Retrofit代替Volley怎么办?如果你有一些逻辑在里面,将很难对其进行重构。你的存储库不需要知道你要使用哪个客户端来获取远程数据。

  • repository 仅需要知道数据是来自远程还是本地,而不需要知道是如何从远程或者本地获取
  • ViewModel仅只需要数据
  • View 仅需要显示数据

当我刚开始开发Android的时候,我就在想,APP如何在离线情况下工作?数据同步是怎样实现的?好的架构是我们很容易做到这些。比如,当loadRepositories在ViewModel中被调用的时候,如果网络链接正常,GitRepoRepositories从远程获取数据,并且将数据保存到本地数据源,一旦手机处于离线模式,GitRepoRepositories可以从本地数据源获取数据,因此,Repositories需要有持有远程数据源实例RemoteDataSource和本地数据源实例LocalDataSource和处理数据从哪里来的逻辑。

让我们添加一个本地数据源(local data source):

class GitRepoLocalDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First From Local", "Owner 1", 100, false))
        arrayList.add(Repository("Second From Local", "Owner 2", 30, true))
        arrayList.add(Repository("Third From Local", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000)
    }

    fun saveRepositories(arrayList: ArrayList<Repository>){
        //todo save repositories in DB
    }
}

interface OnRepoLocalReadyCallback {
    fun onLocalDataReady(data: ArrayList<Repository>)
}

在这里,我们有两个方法:第一个返回伪造的本地数据,第二个用于保存伪造数据。

让我们添加远程数据源:

class GitRepoRemoteDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First from remote", "Owner 1", 100, false))
        arrayList.add(Repository("Second from remote", "Owner 2", 30, true))
        arrayList.add(Repository("Third from remote", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000)
    }
}

interface OnRepoRemoteReadyCallback {
    fun onRemoteDataReady(data: ArrayList<Repository>)
}

它仅有一个方法返回远程模拟数据

接下来为repository添加一些逻辑:

class GitRepoRepository {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
       remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback {
           override fun onDataReady(data: ArrayList<Repository>) {
               localDataSource.saveRepositories(data)
               onRepositoryReadyCallback.onDataReady(data)
           }

       })
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

因此,分离数据源,我们可以轻松地在本地保存数据。

如果你只需要来自网络的数据该怎么办》?还需要使用Repository模式吗?是。它使您的代码更易于测试,其他开发人员可以更好地理解您的代码,并且可以更快地对其进行维护! :)

Android Manager 包装器

如果要在GitRepoRepository中检查Internet连接,以便可以知道要查询哪个数据源,该怎么办?我们已经说过,我们不应该在ViewModels和Models中放置任何与Android相关的代码,那么如何处理这个问题呢?

我们为网络连接写一个包装器

class NetManager(private var applicationContext: Context) {
    private var status: Boolean? = false

    val isConnectedToInternet: Boolean?
        get() {
            val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            val ni = conManager.activeNetworkInfo
            return ni != null && ni.isConnected
        }
}

上面的代码需要我们在Manifest中添加权限之后才能工作

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

但是,怎么在Repository创建实例呢?因为我们没有context啊,当然,可以从构造方法传入

class GitRepoRepository (context: Context){

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()
    val netManager = NetManager(context)

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                localDataSource.saveRepositories(data)
                onRepositoryReadyCallback.onDataReady(data)
            }

        })
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

在我们为ViewModel创建一个新的GitRepoRepository实例之前呢,我们如何得到一个NetManager呢?因为我们需要给NetManager传一个Context,你可以使用生命周期感知组件中的AndroidViewModel,它带有Contenxt,它的Context是一个Application Context而不是一个Activity的Context。

class MainViewModel : AndroidViewModel  {

    constructor(application: Application) : super(application)

    var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication()))

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories() {
        isLoading.set(true)
        gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}

这行代码:

constructor(application: Application) : super(application)

我们正在为MainViewModel定义构造函数。这是必需的,因为AndroidViewModel在其构造函数中要求一个Application实例。因此,在我们的构造函数中,需要调用AndroidViewModel的构造函数的super方法,以便可以调用我们继承类的构造函数。

注意:我们可以简写成一行:

class MainViewModel(application: Application) : AndroidViewModel(application) {
... 
}

现在,在我们的GitRepoRepository中有一个NetManager实例了,可以检查网络连接状态。

class GitRepoRepository(val netManager: NetManager) {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {

        netManager.isConnectedToInternet?.let {
            if (it) {
                remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
                    override fun onRemoteDataReady(data: ArrayList<Repository>) {
                        localDataSource.saveRepositories(data)
                        onRepositoryReadyCallback.onDataReady(data)
                    }
                })
            } else {
                localDataSource.getRepositories(object : OnRepoLocalReadyCallback {
                    override fun onLocalDataReady(data: ArrayList<Repository>) {
                        onRepositoryReadyCallback.onDataReady(data)
                    }
                })
            }
        }
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

因此,如果我们有网络连接,我们将获取远程数据并将其保存在本地。另一方面,如果没有网络连接,我们将获取本地数据。

Kotlin提示: let运算符可以检查可控性,并且从其中返回一个值
预告

在后面的文章中,我将介绍依赖项注入,为什么在ViewModel中创建存储库实例很糟糕,以及如何避免使用AndroidViewModel。另外,到目前为止,写的代码中有一些问题,这样写是有原因的,我试图让你面对这些问题,以便你可以理解为什么所有这些库都很受欢迎以及为什么要使用它。

最后,感谢你的阅读!

本系列已更新完毕:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

【译】使用Kotlin从零开始写一个现代Android 项目-Part3

【译】使用Kotlin从零开始写一个现代Android 项目-Part4

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你

查看原文

赞 0 收藏 0 评论 0

依然饭特稀西 发布了文章 · 8月21日

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

前言

经常在medium.com上看到一些高质量的技术帖子,但是由于国内的上网环境或者有的同学对于看英文比较排斥,错过了不少好文章。因此,西哥决定弄一个《优质译文专栏》,花一些时间翻译一些优质技术文给大家。这篇文章是一个小系列,用Kotlin开发现代Android APP,总共四篇,后面的会陆续翻译!以下是正文。

现在,真的很难找到一个涵盖所有Android新技术的项目,因此我决定自己来写一个,在本文中,我们将用到如下技术:

  • 0 、Android Studio
  • 1、Kotlin 语言
  • 2、构建变体
  • 3、ConstraintLayout
  • 4、DataBinding库
  • 5、MVVM+repository+Android Manager架构模式
  • 6、RxJava2及其对架构的帮助
  • 7、Dagger 2.11,什么是依赖注入?为什么要使用它?
  • 8、Retrofit + RxJava2 实现网络请求
  • 9、RooM + RxJava2 实现储存
我们的APP最终是什么样子?

我们的APP是一个非常简单的应用程序,它涵盖了上面提到的所有技术。只有一个简单的功能:从Github 获取googlesamples用户下的所有仓库,将数据储存到本地数据库,然后在界面展示它。

我将尝试解释更多的代码,你也可以看看你Github上的代码提交。

Github:https://github.com/mladenrako...

让我们开始吧。

0、Android Studio

首先安卓Android Studio 3 beta 1(注:现在最新版为Android Studio 4.0),Android Studio 已经支持Kotlin,去到Create Android Project界面,你将在此处看到新的内容:带有标签的复选框include Kotlin support。默认情况下选中。按两次下一步,然后选择EmptyActivity,然后完成了。 恭喜!你用Kotlin开发了第一个Android app)

1、Kotlin

在刚才新建的项目中,你可以看到一个MainActivity.kt:

package me.mladenrakonjac.modernandroidapp

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

.kt后缀代表了这是一个Kotlin文件

MainActivity : AppCompatActivity() 表示我们的MainActivity继承自AppCompatActivity

此外,所有的方法都必须有一个关键字fun,在Kotlin 中,你不能使用@override注解,如果你要表明方法是复写父类或者接口的方法的话,直接使用override关键字,注意:它和Java不一样,不是一个注解了。

然后,savedInstanceState: Bundle? 中的?代表什么呢?它代表了savedInstanceState这个参数可以是Bundle或者null。Kotlin是一门null 安全语言,如果你像下面这样写:

var a : String

你将会得到一个编译错误。因为a变量必须被初始化,并且不能为null,因此你要像这样写:

var a : String = "Init value"

并且,如果你执行以下操作,也会报编译错误:

a = null

要想使a变量为null ,你必须这样写:

var a : String?

为什么这是Kotlin语言的一个重要功能呢?因为它帮我们避免了NPE,Androd开发者已经对NPE感到厌倦了,甚至是null的发明者-Tony Hoare先生,也为发明它而道歉。假设我们有一个可以为空的nameTextView。如果为null,以下代码将会发生NPE:

nameTextView.setEnabled(true)

但实际上,Kotlin做得很好,它甚至不允许我们做这样的事情。它会强制我们使用?或者!!操作符。如果我们使用?操作符:

nameTextView?.setEnabled(true)

仅当nameTextView不为null时,这行代码才会继续执行。另一种情况下,如果我们使用!!操作符:

nameTextView!!.setEnabled(true)

如果nameTextView为null,它将为我们提供NPE。它只适合喜欢冒险的家伙)

这是对Kotlin的一些介绍。我们继续进行,我将停止描述其他Kotlin特定代码。

2、构建变体

通常,在开发中,如果你有两套环境,最常见的是测试环境和生产环境。这些环境在服务器URL图标名称目标api等方面可能有所不同。通常,在开始的每个项目中我都有以下内容:

  • finalProduction: 上传Google Play 使用
  • demoProduction:该版本使用生产环境服务器Url,并且它有着GP上的版本没有的新功能,用户可以在Google play 旁边安装,然后可以进行新功能测试和提供反馈。
  • demoTesting:和demoProduction一样,只不过它用的是测试地址
  • mock: 对于我来说,作为开发人员和设计师而言都是很有用的。有时我们已经准备好设计,而我们的API仍未准备好。等待API准备就绪后再开始开发可不是好的解决方案。此构建变体为提供有mock数据,因此设计团队可以对其进行测试并提供反馈。对于保证项目进度真的很有帮助,一旦API准备就绪,我们便将开发转移到demoTesting环境。

在此应用程序中,我们将拥有所有这些变体。它们的applicationId和名称不同。 gradle 3.0.0 flavourDimension中有一个新的api,可让您混合不同的产品风味,因此您可以混合demominApi23风味。在我们的应用程序中,我们将仅使用“默认” 的flavorDimension。早app的build.gradle中,将此代码插入android {}下:

flavorDimensions "default"
    
productFlavors {

    finalProduction {
        dimension "default"
        applicationId "me.mladenrakonjac.modernandroidapp"
        resValue "string", "app_name", "Modern App"
    }

    demoProduction {
        dimension "default"
        applicationId "me.mladenrakonjac.modernandroidapp.demoproduction"
        resValue "string", "app_name", "Modern App Demo P"
    }

    demoTesting {
        dimension "default"
        applicationId "me.mladenrakonjac.modernandroidapp.demotesting"
        resValue "string", "app_name", "Modern App Demo T"
    }


    mock {
        dimension "default"
        applicationId "me.mladenrakonjac.modernandroidapp.mock"
        resValue "string", "app_name", "Modern App Mock"
    }
}

打开string.xml文件,删掉app_namestring资源,因此,我们才不会发生资源冲突,然后点击Sync Now,如果转到屏幕左侧的“构建变体”,则可以看到4个不同的构建变体,其中每个都有两种构建类型:“Debug”和“Release”,切换到demoProduction构建变体并运行它。然后切换到另一个并运行它。您就可以看到两个名称不同的应用程序。

3、ConstraintLayout

如果你打开activity_main.xml ,你可以看到跟布局是ConstraintLayout,如果你开发过iOS应用程序,你可能知道AutoLayoutConstraintLayout和它非常的相似,他们甚至用了相同的 Cassowary 算法。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Constraints可以帮我们描述View之间的关系。对于每一个View来说,应该有4个约束,每一边一个约束,在这种情况下,我们的View就被约束在了父视图的每一边了。

在Design Tab中,如果你将Hello World文本稍微向上移动,则在TextTab中将增加下面这行代码:

app:layout_constraintVertical_bias="0.28"

Design tab 和 Text tab是同步的,我们在Design中移动视图,则会影响Text中的xml,反之亦然。垂直偏差描述了视图对其约束的垂直趋势。如果要使视图垂直居中,则应使用:

app:layout_constraintVertical_bias="0.28"

我们让Activity只显示一个仓库,它有仓库的名字,star的数量,作者,并且还会显示是否有issue

要得到上面的布局设计,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">

    <TextView
        android:id="@+id/repository_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.083"
        tools:text="Modern Android app" />

    <TextView
        android:id="@+id/repository_has_issues"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:text="@string/has_issues"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/repository_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toEndOf="@+id/repository_name"
        app:layout_constraintTop_toTopOf="@+id/repository_name"
        app:layout_constraintVertical_bias="1.0" />

    <TextView
        android:id="@+id/repository_owner"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/repository_name"
        app:layout_constraintVertical_bias="0.0"
        tools:text="Mladen Rakonjac" />

    <TextView
        android:id="@+id/number_of_starts"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/repository_owner"
        app:layout_constraintVertical_bias="0.0"
        tools:text="0 stars" />

</android.support.constraint.ConstraintLayout>

不要被tools:text搞迷惑了,它的作用仅仅是让我们可以预览我们的布局。

我们可以注意到,我们的布局是扁平的,没有任何嵌套,你应该尽量少的使用布局嵌套,因为它会影响我们的性能。ConstraintLayout也可以在不同的屏幕尺寸下正常工作。

我有种预感,很快就能达到我们想要的布局效果了。

上面只是一些关于ConstraintLayout的少部分介绍,你也可以看一下关于ConstraintLayout使用的google code lab: https://codelabs.developers.g...

4. Data binding library

当我听到Data binding 库的时候,我的第一反应是:Butterknife已经很好了,再加上,我现在使用一个插件来从xml中获取View,我为啥要改变,来使用Data binding呢?但当我对Data binding有了更多的了解之后,我的它的感觉就像我第一次见到Butterknife一样,无法自拔。

Butterknife能帮我们做啥?

ButterKnife帮助我们摆脱无聊的findViewById。因此,如果您有5个视图,而没有Butterknife,则你有5 + 5行代码来绑定您的视图。使用ButterKnife,您只有我行代码就搞定。就是这样。

Butterknife的缺点是什么?

Butterknife仍然没有解决代码可维护问题,使用ButterKnife时,我经常发现自己遇到运行时异常,这是因为我删除了xml中的视图,而没有删除Activity/Fragment类中的绑定代码。另外,如果要在xml中添加视图,则必须再次进行绑定。真的很不好维护。你将浪费大量时间来维护View绑定。

那与之相比,Data Binding 怎么样呢?

有很多好处,使用Data Binding,你可以只用一行代码就搞定View的绑定,让我们看看它是如何工作的,首先,先将Data Binding 添加到项目:

// at the top of file 
apply plugin: 'kotlin-kapt'


android {
    //other things that we already used
    dataBinding.enabled = true
}
dependencies {
    //other dependencies that we used
    kapt "com.android.databinding:compiler:3.0.0-beta1"
}

请注意,数据绑定编译器的版本与项目build.gradle文件中的gradle版本相同:

classpath 'com.android.tools.build:gradle:3.0.0-beta1'

然后,点击Sync Now,打开activity_main.xml,将Constraint Layout 用layout标签包裹

<?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">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">

        <TextView
            android:id="@+id/repository_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:textSize="20sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.083"
            tools:text="Modern Android app" />

        <TextView
            android:id="@+id/repository_has_issues"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:text="@string/has_issues"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@+id/repository_name"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toEndOf="@+id/repository_name"
            app:layout_constraintTop_toTopOf="@+id/repository_name"
            app:layout_constraintVertical_bias="1.0" />

        <TextView
            android:id="@+id/repository_owner"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/repository_name"
            app:layout_constraintVertical_bias="0.0"
            tools:text="Mladen Rakonjac" />

        <TextView
            android:id="@+id/number_of_starts"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/repository_owner"
            app:layout_constraintVertical_bias="0.0"
            tools:text="0 stars" />

    </android.support.constraint.ConstraintLayout>

</layout>

注意,你需要将所有的xml移动到layout 标签下面,然后点击Build图标或者使用快捷键Cmd + F9,我们需要构建项目来使Data Binding库为我们生成ActivityMainBinding类,后面在MainActivity中将用到它。

如果没有重新编译项目,你是看不到ActivityMainBinding的,因为它在编译时生成。

我们还没有完成绑定,我们只是定义了一个非空的 ActivityMainBinding 类型的变量。你会注意到我没有把? 放在 ActivityMainBinding 的后面,而且也没有初始化它。这怎么可能呢?lateinit 关键字允许我们使用非空的延迟被初始化的变量。和 ButterKnife 类似,在我们的布局准备完成后,初始化绑定需要在 onCreate 方法中进行。此外,你不应该在 onCreate 方法中声明绑定,因为你很有可能在 onCreate 方法外使用它。我们的 binding 不能为空,所以这就是我们使用 lateinit 的原因。使用 lateinit 修饰,我们不需要在每次访问它的时候检查 binding 变量是否为空。

我们初始化binding变量,你需要替换:

setContentView(R.layout.activity_main)

为:

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

就是这样,你成功的绑定了所有View,现在你可以访问它并且做一些更改,例如,我们将仓库名字改为Modern Android Medium Article:

binding.repositoryName.text = "Modern Android Medium Article"

如你所见,现在我们可以通过bingding变量来访问main_activity.xml的所有View了(前提是它们有id),这就是Data Binding 比ButterKnife 好用的原因。

kotlin的 Getters 和 setters

大概,你已经注意到了,我们没有像Java那样使用.setText(),我想在这里暂停一下,以说明与Java相比,Kotlin中的getter和setter方法如何工作的。

首先,你需要知道,我们为什么要使用getters和setters,我们用它来隐藏类中的变量,仅允许使用方法来访问这些变量,这样我们就可以向用户隐藏类中的细节,并禁止用户直接修改我们的类。假设我们用 Java 写了一个 Square 类:

public class Square {
  private int a;
  
  Square(){
    a = 1;
  }

  public void setA(int a){
    this.a = Math.abs(a);
  }
  
  public int getA(){
    return this.a;
  }
  
}

使用setA()方法,我们禁止了用户向Square类的a变量设置一个负数,因为正方形的边长一定是正数,要使用这种方法,我们必须将其设为私有,因此不能直接设置它。这也意味着我们不能直接获得a,需要给它定一个get方法来返回a,如果有10个变量,那么我们就得定义10个相似的get方法,写这样无聊的样板代码,通常会影响我们的心情。

Kotling使我们的开发人员更轻松了。如果你调用下面的代码:

var side: Int = square.a

这并不意味着你是在直接访问a变量,它和Java中调用getA()是相同的

int side = square.getA();

因为Kotlin自动生成默认的getter和setter。在Kotlin中,只有当您有特殊的setter或getter时,才应指定它。否则,Kotlin会为您自动生成:

var a = 1
   set(value) { field = Math.abs(value) }

field ? 这又是个什么东西?为了更清楚明白,请看下面代码:

var a = 1
   set(value) { a = Math.abs(value) }

这表明你在调用set方法中的set(value){},因为Kotlin的世界中,没有直接访问属性,这就会造成无限递归,当你调用a = something,会自动调用set方法。使用filed就能避免无限递归,我希望这能让你明白为什么要用filed关键字,并且了解getters和setters是如何工作的。

回到代码中继续,我将向你介绍Kotlin语言的另一个重要功能:apply函数:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.apply {
            repositoryName.text = "Medium Android Repository Article"
            repositoryOwner.text = "Mladen Rakonjac"
            numberOfStarts.text = "1000 stars"
            
        }
    }
}

apply 允许你在一个实例上调用多个方法,我们仍然还没有完成数据绑定,还有更棒的事儿,让我们为仓库定义一个UI模型(这个是github仓库的数据模型Repository,它持有要展示的数据,请不要和Repository模式的中的Repository搞混淆了哈),要创建一个Kotlin class,点击New -> Kotlin File/Class :

class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)

在Kotlin中,主构造函数是类头的一部分,如果你不想定义次构造函数,那就是这样了,数据类到此就完成了,构造函数没有参数分配给字段,没有setters和getters,整个类就一行代码。

回到MainActivity.kt,为Repository创建一个实例。

var repository = Repository("Medium Android Repository Article",
        "Mladen Rakonjac", 1000, true)

你应该注意到了,创建类实例,没有用new

现在,我们在activity_main.xml 中添加data标签。

<data>
      <variable
        name="repository"
        type="me.mladenrakonjac.modernandroidapp.uimodels.Repository"
        />
</data>

我们可以在布局中访问存储的变量repository,例如,我们可以如下使用id是repository_name的TextView,如下:

android:text="@{repository.repositoryName}"

repository_name文本视图将显示从repository变量的属性repositoryName获取的文本。剩下的唯一事情就是将repository变量从xml绑定到MainActivity.kt中的repository。

点击Build使DataBinding 为我们生成类,然后在MainActivity中添加两行代码:

binding.repository = repository
binding.executePendingBindings()

如果你运行APP,你会看到TextView上显示的是:“Medium Android Repository Article”,非常棒的功能,是吧?

但是,如果我们像下面这样改一下呢?

Handler().postDelayed({repository.repositoryName="New Name"}, 2000)

新的文本将会在2000ms后显示吗?不会的,你必须重新设置一次repository,像这样:

Handler().postDelayed({repository.repositoryName="New Name"
    binding.repository = repository
    binding.executePendingBindings()}, 2000)

但是,如果我们每次更改一个属性都要这么写的话,那就非常蛋疼了,这里有一个更好的方案叫做Property Observer

让我们首先解释一下什么是观察者模式,因为在rxJava部分中我们也将需要它:

可能你已经听说过 http://androidweekly.net/,这是一个关于Android开发的周刊。如果您想接收它,则必须订阅它并提供您的电子邮件地址。过了一段时间,如果你不想看了,你可以去网站上取消订阅。

这就是一个观察者/被观察者的模式,在这个例子中, Android 周刊是被观察者,它每周都会发布新闻通讯。读者是观察者,因为他们订阅了它,一旦订阅就会收到数据,如果不想读了,则可以停止订阅。

Property Observer在这个例子中就是 xml layout,它将会监听Repository实例的变化。因此,Repository被观察者,例如,一旦在Repository类的实例中更改了repository nane 属性后,xml不调用下面的代码也会更新:

binding.repository = repository
binding.executePendingBindings()

如何让它使用Data Binding 库呢?,Data Binding库提供了一个BaseObservable类,我们的Repostory类必须继承它。

class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int?
                 , var hasIssues: Boolean = false) : BaseObservable(){

    @get:Bindable
    var repositoryName : String = ""
    set(value) {
        field = value
        notifyPropertyChanged(BR.repositoryName)
    }

}

当我们使用了 Bindable 注解时,就会自动生成 BR 类。你会看到,一旦设置新值,就会通知它更新。现在运行 app 你将看到仓库的名字在 2 秒后改变而不必再次调用 executePendingBindings()

以上就是这一节的所有内容,下一节将会讲MVVM+Repository 模式的使用。敬请期待!感谢阅读。

作者 | Mladen Rakoajc
译者 | 依然范特稀西
编辑 | 依然范特稀西

原文地址:https://proandroiddev.com/mod...

本系列已更新完毕:

【译】使用Kotlin从零开始写一个现代Android 项目-Part1

【译】使用Kotlin从零开始写一个现代Android 项目-Part2

【译】使用Kotlin从零开始写一个现代Android 项目-Part3

【译】使用Kotlin从零开始写一个现代Android 项目-Part4

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你

查看原文

赞 11 收藏 7 评论 1

认证与成就

  • 获得 113 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-23
个人主页被 2.7k 人浏览