依然饭特稀西

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

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

个人动态

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

Jetpack compose 正式发布beta版本,原生Android 开发将更轻松!

Jetpack Compose大家都不陌生了,它是Google推出的一个新的UI工具包,旨在帮助开发者更快、更轻松地在Android 平台上构建Native应用。Jetpack compose提供了现代化的声明式Kotlin API(取代Android 传统的xml布局),可帮助开发者用更少的代码构建美观、响应迅速的应用程序。自2019年Google IO 宣布发布Compose 以来,备受Android 开发者的关注,我也在一直关注Compose的发展,并且一直看好它在原生Android开发上的应用。19年底,我写了一篇文章:

Android Jetpack Compose 最全上手指南

去年8月,Jetpack Compose 发布Alpha版本,我又写了一篇文章:

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

今天,Jetpack Compose的Beta版发布了,来到了一个新的里程碑。

本篇文章带大家一起看看Jetpack Compsoe Beta 版本有哪些更新!

1、稳定的API

随着Beta版的发布,意味着Compose API已完成,并具有构建可用于生产环境的应用程序所需的所有功能,同时也意味着Compose的API已趋于稳定,不会再随便的删除或者更改API。如果在之前了解过Jetpack Compose的同学应该清楚,前面几个预览版和Alpha版的API改动之大,很多都是实验性的API。现在API稳定了,也是我们开始学习Jetpack Compose的最佳时机,等不久后的release版本发布,我们就可以在生产环境进行尝试和使用了。

2、新特性

自2019年Compose开放源代码开发以来,Compose已经发布了30个公开版本,解决了700多个外部错误,并接受了200多个外部贡献。从去年8月Alpha版发布,到现在的Beta版本,又增加/改进了很多新的特性,如:

  • 对协程的支持
  • 对讲功能支持
  • 新的动画API 在alpha版本中,就支持动画,在本次Beta版本中,新的动画API更加简洁,还可以通过Android Studio 预览动画。
  • 与原生视图互操作
  • Material UI 组件支持
  • Lazy Lists 也就是Jetpack Compose 版的RecyclerView
  • Modifiers 修改器
  • Testing 测试
  • 主题与图形支持, 可以非常轻松的支持暗黑模式
  • 输入与手势支持
  • 文本与编辑框
  • 窗口管理

以上这些都是一些比较重要的新特性或者改进的API。这里我没有添加代码演示,后续我会出Jetpack Compose的系列文章,本次Beta 版本的关注点在于API的完整性和基础功能API的构建,这些都将在正式版继续完善和性能相关的优化。

3、Android Studio 对 Jetpack Compose 的支持

新版的Android Studio Arctic Fox(现在还是Canary版本) 中添加了许多新工具来支持Jetpack Compose新特性,比如:实时文字、动画预览,布局检查等等。

3.1 实时文字

新的Android Studio 增加了对文字更改实时预览的效果,可以是Preview、模拟器、或者真机。

3.2 动画预览

可以查看、检查或播放动画,还可以逐针播放

3.3 布局检查器

Android Studio Arctic Fox 增加了布局监测器对Compose的支持,可以分析Compose组件的层级。如下所示:

3.4 交互式预览

在此模式下,你可以与界面组件互动、点击组件,以及查看状态如何变化。通过这种方式,您可以快速获得有关界面如何反应的反馈,并可快速预览动画。如要启用此模式,只需点击“互动”图标 ,系统即会切换预览模式。

如需停止此模式,请点击顶部工具栏中的 Stop Interactive Preview

3.5 部署预览到设备

使用此功能可将界面的代码段部署到设备。这有助于在设备中测试一小部分代码,而无需启动整个应用。

点击 @Preview 注释旁边或预览顶部的“部署到设备”图标 ,Android Studio 会将该 @Preview 部署到连接的设备或模拟器。

以上就是新版Android Studio 对Compose 的支持

4、如何学习Jetpack Compose

学习一项新技术、新框架,官方文档是最好的资料,Google官方提供了非常多的资料,足够我们去学习Jetpack Compose。Beta发布后,新的官网也更新了,上面又文档、入门教程、还有视频等(视频是在Youtube上的,需要翻墙)。

此外,Google也给我们准备了非常多的 Compose 示例,如果你想直接进入并查看“实际操作”,可以看看官方提供8个示例应用程序。有简单到复杂的示例,每个示例都展示了不同的API和用例。

地址:https://github.com/android/co...

5、总结

随着Jetpack Compose Beta 版的发布,它具有稳定的API和1.0的完整功能,如果你想开始使用和学习Jetpack Compose,现在是个不错的时机,官方更新了完善的开发文档和入门教程。地址:

https://developer.android.com...

根据官方前面给出的时间表,可能在今年的Google IO 大会就能与我们见面,差不多5、6月份。

最后,你对Jetpack Compose 有什么期待和想法呢?欢迎留言交流。

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」关注,干货好文,第一时间获取。

image.png

查看原文

赞 1 收藏 1 评论 0

依然饭特稀西 关注了用户 · 1月24日

xiangzhihong @xiangzhihong

著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》和《Android应用开发实战》

关注 8832

依然饭特稀西 发布了文章 · 1月20日

🏆2020年总结!翻过这座山,他们就会听到你的故事!

时间总是过得这样的快!转眼已来到2021。

2020年是魔幻的一年,我们经历疫情的肆掠,见证了什么是全国上下一心,共同抗疫。见证了什么是中国速度,雷神山、火神山、方舱医院在短短的几天之间拔地而起,为感染患者争取了生的机会。

同时,我们也目睹了很多人的离开,NBA篮球明星科比、足球明星马拉多纳,电影明星《黑豹》主演-查德威克鮑斯曼等相继离开了。可能上一秒你脑海里还有他们在球场上、电影里的身影,但是事实上他们却已不存在于这个世界。是啊!2020年就是这么的真实与不真实。有时候想想,能活着挺过2020,可真特么不容易。

回想我的2020年,虽然困难,但也有不少收获,因此,这篇年终总结就来与大家分享,2020我的收获与心得。

关于写作

掘金平台

我从16年底开始写技术博客,掘金是我的第一个平台,那时候,掘金的用户还不多,由于喜欢掘金简单、简洁的风格,注册了我的掘金账号 「依然范特稀西」,到现在已经5年多了,一路也见证了掘金的成长,也见证了自己的成长。到现在累计原创技术博客100余篇,阅读52w+,点赞1.8w+,掘力值2w+,并且今年等级达到了「LV6」(摸鱼6级大佬🐶):

CSDN平台

CSDN是我前两年忽略了的一个平台,其实我很早就知道这个网站,12年就注册了账号,还在学校的时候,有时候下载资源、查找资料会用到,最开始写博客为啥没去CSDN?是因为CSDN的广告确实太多了,掘金、简书这些比较简介阅读体验吸引我,因此没在CSDN发布文章。但今年来CSDN也作出了很多改变,体验好了不少,加之CSDN的流量确实大,说真的,如果写出的文章没人看,那你就没有写下去的动力,CSDN的用户量规模确实比后起的这几个平台大不少。因此,今年也把文章同步到CSDN,开始运营CSDN。累计粉丝277,阅读量12w+,收藏2000+。本来准备今年阅读量达20w+申请博客专家的,看来只能留到明年了。

2020年,发布原创文章十几篇,其实比起前3年的输出,今年的文章显得有点少,但是也在我自己的计划节奏内,平均保持在每月1篇文章。现在岁数大了,确实没有以前那一股冲劲了,以前写博客是没日没夜的肝,晚上下班后,写到凌晨才睡觉,基本每周1篇,有时候2篇。对于写技术博客的博主都知道,一周1-2篇技术博客其实是不容易的,因为你得写Demo、画图、然后组织语言将技术点讲清楚,很费时间。总之,保持自己的节奏吧,2021年,计划会比今年的产出要多。

关于公众号

年初,我的目标是今年公众号粉丝达2W+,现在基本达到了,从17年开始做公众号到现在,3年多的时间,2w粉丝成绩确实不怎么样,但是对于我自己来说,我已经足够努力了。现在公众号涨粉比较困难,很多人都开通了自己的公众号,也有很多读者向我咨询,现在做公众号怎样?我的建议是可以开通公众号,作为一个自己的平台,但是一般原始粉丝的积累通常比较慢,一般粉丝来源有2种渠道,一种是博客平台带来粉丝,一种是和其他公众号互推涨粉。互推其实是要互惠互利,也就是相互带粉,初期是没有人带你的。因此最开始的粉丝来源只有靠博客平台。

开始做公众号,多产出,能出一些“技术爆文”,也能带来可观的粉丝量,如果文章质量好,可以联系微信公众号大V来转载,据我的经验和了解,很多公众号对于质量高的文章都愿意转载的,这样快速带来很多的原始粉丝,一篇文章有时候能带来几百上千个粉丝。2021年,我也计划多出一些原创技术文,希望给大家带来一些不一样的东西。

关于买房

买房可能是我们经常讨论的一个话题,在中国这样一个家庭观念很强的国家,很难说一辈子租房住,就是你答应,你媳妇也不会答应,就算你媳妇答应,你丈母娘也不会答应... 因此,买房是我们早晚要面对的事。

前几天,终于拿到接了房,拿到了房产证,但是买房也躺了许多坑,或者从投资的角度来看,并不赚,我是2018年初上的车,期房。那个时候,正处在15年后这一波涨房价的顶峰,一套房子一天一个价,房子是出来一套秒一套。一些朋友也在劝我买房,刚需嘛,早上车早好。而那个时候成都限区+摇号,只能买高新区的盘资格,但是高新区基本没啥新盘,二手房又太贵。于是决定在老家省会-重庆买,我户口在重庆,没啥限制,匆忙上了一套。

其实现在来看,当时是有些心急了,当时没有太多考虑品牌、学区、配套等一些硬件,买了之后房价一直没涨也没降。反而今年疫情之后,还有很多买房优惠,比如,打折、首付2成、首付分期等。这两年对买房也学到了不少东西,如果现在买的话,我一定不会买原来的地方。
因此,买房一定不要心急,要多考虑品牌、学区、配套、地段、规划等相关东西,多楼盘对比。挑自己满意的。长远来说买房也是一种好的投资。

关于理财

相信2020年在买股票和买基金的朋友都赚了不少钱,以前,对于理财没有太多的关注(也主要是穷😭没有多余子弹),终于在下半年开始醒悟,觉得要学习一下投资理财,于是开始看一些理财书籍和基金相关的知识,终于在10月份开始买基金实操,还好赶上了年底这一波上涨,喝了一点汤。

2020年,很多板块像是消费、白酒饮料、新能源、医疗涨得都非常不错,恭喜在车上喝酒吃肉的朋友们,理财方面自己也还是小白一枚,目前还在学习中,重要的是思想的转变。喜欢这方面的朋友可以一起学习交流。希望2021年也能牛气冲天,大家多多发财吧!

结尾

2021年在这几个方面给自己立个flag吧:

1、投资自己

任何时候都要记得投资自己,2021年不管是技术、还是公众号运营、理财方面都能有所进步,下面这一摞书希望可以看完:

2、公众号粉丝希望可以达3-4w
3、原创技术文章输出,一个月能有2篇左右,累计30篇左右吧,重点一下几个方面
  • Jetpack相关
  • Kotlin相关
  • Java相关(基础、面试)

最后的最后,我想送一句话给大家也送给我自己:

翻过这座山,他们就会听到你的故事

2018年英雄联盟 s8 赛季 IG 夺冠之后,记得老师激动万分的说出了这句话「翻过这座山,他们就会听到你的故事,继续前进吧!IG」,这句话时常都会出现在我的脑海里,现在仍令我激动。那是经历前面7个赛季,LPL无数次的倒在世界赛上,然后又爬起来,最终夺冠的故事,翻过那座山,他们听到了你的故事,等来了那场金色的雨。

我们的人生中,又何尝不会遇到很多的山,跌倒了,再爬起来就是,翻过那些山,最终的胜利将属于我们!

本文参与了SegmentFault 思否征文「2020 总结」
查看原文

赞 0 收藏 0 评论 0

依然饭特稀西 发布了文章 · 2020-11-04

如何在Android 11 中正确请求位置权限?以及Android 8 - 11位置权限的变化及适配方法!

由于现在位置信息变为了敏感数据,因此Android限制了它的使用,尤其在APP后台。

在Android 9 之前,位置权限没有按照前后台分离,APP在前台和后台使用相同的资源。

但是,Google开始意识到一些APP滥用此类数据,于是决定按照前台后台分离资源的方式,增加一些层级来保护用户的位置信息数据。

对于Android开发者来说,这就为我们在APP中请求位置权限增加了一些额外的工作。

在详细介绍之前,让我们先看一看在最近几个Android版本中的更改日志:

  • 在Android 8 中: 处于后台的应用,每小时只能检索几次用户的位置。
  • Android 10 之前:位置权限是一个单一资源,应用只需一次授权就可以到处使用,随时使用(前台和后台)
  • 在Android 10 中: 后台位置变成了一个独立的资源, 应用程序除了前台请求外,还必须明确请求此权限。
  • Android 11 中:无法与其他人同时请求后台位置权限,应用必须单独请求。此外,请求此权限不会像其他权限一样立即弹窗提示用户,而是会将用户带到/ Settings页面/ Location权限会话,以便用户可以更新权限级别。

根据上面的这些变化,我们必须根据不同的系统版本处理位置权限的请求,这里有3种不同场景(在写本文时,我们目前使用的是Android 11):

Android 10 之前

位置权限只需请求一次,处于前台和后台的APP都可使用

@TargetApi(28)
fun Context.checkLocationPermissionAPI28(locationRequestCode : Int) {
    if (!checkSinglePermission(Manifest.permission.ACCESS_FINE_LOCATION) ||
        !checkSinglePermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
        val permList = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
        requestPermissions(permList, locationRequestCode)
    }
}

private fun Context.checkSinglePermission(permission: String) : Boolean {
    return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}

在这些版本中,用户只有2个选项:授权不授权

Android 10

在这个版本中,增加了ACCESS_BACKGROUND_LOCATION, 你可以请求此权限来同时获得前台和后台的权限,像下面这样呢:

@TargetApi(29)
private fun Context.checkLocationPermissionAPI29(locationRequestCode : Int) {
    if (checkSinglePermission(Manifest.permission.ACCESS_FINE_LOCATION) &&
        checkSinglePermission(Manifest.permission.ACCESS_COARSE_LOCATION) &&
        checkSinglePermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) return
    val permList = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, 
                           Manifest.permission.ACCESS_COARSE_LOCATION,
                           Manifest.permission.ACCESS_BACKGROUND_LOCATION)
    requestPermissions(permList, locationRequestCode)
    
}

private fun Context.checkSinglePermission(permission: String) : Boolean {
    return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}

同样,如果请求前台权限(ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION),则Android操作系统会自动将后台权限(ACCESS_BACKGROUND_LOCATION)添加到请求中。它类似于<uses-permission>声明,无论是ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION被声明,ACCESS_BACKGROUND_LOCATION将在安装过程中被添加。

用户现在有三个选项:后台(随时)前台(仅使用APP期间)拒绝

Android 11

除上述内容外,开发人员还需要增加其他一些步骤。

这里有2种场景,第一种情况是当仅请求前台权限,在这种情况下,我们通常使用ACCESS_FINE_LOCATION 或者ACCESS_COARSE_LOCATION, 但是,请求授权的弹窗和以前的略有不同,在Android 11 中,Google 增加了一个可选项 Only this time

请注意,即使将ACCESS_BACKGROUND_LOCATION添加到要请求的权限列表中,系统也将忽略它。

第二种情况是:应用也需要后台权限,为此,你必须准备自己的对话框,并使用明确的消息来说明后台位置的使用。

当用户同意后,将他引导到应用设置页面,那里可以选择他想授予的权限等级

示例代如下:

@TargetApi(30)
private fun Context.checkBackgroundLocationPermissionAPI30(backgroundLocationRequestCode: Int) {
    if (checkSinglePermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) return
    AlertDialog.Builder(this)
        .setTitle(R.string.background_location_permission_title)
        .setMessage(R.string.background_location_permission_message)
        .setPositiveButton(R.string.yes) { _,_ ->
            // this request will take user to Application's Setting page
            requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), backgroundLocationRequestCode)
        }  
        .setNegativeButton(R.string.no) { dialog,_ ->
            dialog.dismiss()
        } 
        .create()
        .show()
        
}

如图所示:

如您所见,Android 11 中,我们有4个权限等级用于位置信息。

当用户选择Allow all the time, APP 就有了后台使用位置信息的权限了。

瞧,这就是Android 11中获取位置权限的整个过程,和各个系统版本的不同处理情况,希望本篇博客对你有用!!!

原文链接:
https://medium.com/swlh/reque...

image.png

查看原文

赞 0 收藏 0 评论 0

依然饭特稀西 发布了文章 · 2020-10-26

再见!onActivityResult!你好,Activity Results API!

背景

在Android应用程序开发中,启动一个Activity不一定是单项操作,从启动的Activity获取数据是常见的场景,最传统的方式是通过Intent携带数据,然后使用startActivityForResult方法来启动下一个Activity,然后通过onActivityResult来接收返回的数据,代码如下:

  1. 调用startActivityForResult方法启动
 startActivityForResult(intent,1)
  1. 实现onActivityResult方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == 1 && resultCode == Activity.RESULT_OK){
            // 处理第二个页面带回的数据
        }
}

以上方式,onActivityResult就能获取从上一个界面返回的数据,这种方式非常有用,不仅能同一个应用中,也可以从其他应用中获取数据,比如我们常见的,调用系统相机、相册获取照片,获取系统通讯录等。

但也有一些问题...

随着应用的扩展,onActivityResult回调方法各种嵌套、耦合严重、难以维护。 最常见的场景就是调用系统相机相册获取照片了。代码可能像是如下这样:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_PERMISSION -> {
                    // 处理权限
                }
                REQUEST_CAMERA -> {
                    // 相机获取图片结果
                }
                REQUEST_ALBUM -> {
                    // 相册获取图片结果
                }
                REQUEST_CROP -> {
                    // 系统裁剪
                }
            }
        }

        super.onActivityResult(requestCode, resultCode, data)
    }

    companion object {
        const val REQUEST_PERMISSION = 1001
        const val REQUEST_CAMERA = 1002
        const val REQUEST_ALBUM = 1003
        const val REQUEST_CROP = 1004
    }
}

各种处理结果都耦合在onActivityResult回调里,并且还得定义一堆额外的常量REQUEST_CODE,用与判断是哪个请求的回调结果。

onActivityResult 现状?

Google 可能也意识到onActivityResult的这些问题,在androidx.activity:activity:1.2.0-alpha02

和`androidx.fragment:fragment:1.3.0-alpha02` 中,已经废弃了`startActivityForResult`和`onActivityResult`方法。
 /**
    * {@inheritDoc}
    *
    * @deprecated use
    * {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}
    * passing in a {@link StartActivityForResult} object for the {@link ActivityResultContract}.
    */
   @Override
   @Deprecated
   public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent,
           int requestCode) {
       super.startActivityForResult(intent, requestCode);
   }
 /**
    * {@inheritDoc}
    *
    * @deprecated use
    * {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}
    * with the appropriate {@link ActivityResultContract} and handling the result in the
    * {@link ActivityResultCallback#onActivityResult(Object) callback}.
    */
   @CallSuper
   @Override
   @Deprecated
   protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
       if (!mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
           super.onActivityResult(requestCode, resultCode, data);
       }
   }

可以看到,这两个方法都被标记为了Deprecated,那这两个方法不推荐使用了,Google推荐使用什么方式从Activity中更好的获取数据呢?答案就是 Activity Results API

Activity Results API

Activity Results API 是 Google官方推荐的Activity、Fragment获取数据的方式。

Activity Results API 到底怎么用?相比onActivityResult有哪些优势?接下来,将一一为你解答。

在介绍如何使用之前,先为大家介绍Activity Results API 中两个重要的组件:ActivityResultContractActivityResultLauncher

  • ActivityResultContract: 协议,它定义了如何传递数据和如何处理返回的数据。ActivityResultContract是一个抽象类,你需要继承它来创建自己的协议,每个 ActivityResultContract 都需要定义输入和输出类,如果您不需要任何输入,可使用 Void(在 Kotlin 中,使用 Void? 或 Unit)作为输入类型。
  • ActivityResultLauncher: 启动器,调用ActivityResultLauncherlaunch方法来启动页面跳转,作用相当于原来的startActivity()
使用 Activity Results API 在Activity之间传递数据
1. 首先,在app下的build.gradle中加入依赖:
implementation 'androidx.activity:activity:1.2.0-beta01'
implementation 'androidx.fragment:fragment:1.3.0-beta01'
2. 定义协议

新建一个Contract类,继承自ActivityResultContract<I,O>,其中,I是输入的类型,O是输出的类型。需要实现2个方法,createIntentparseResult,输入类型I作为createIntent的参数,输出类型O作为parseResult方法的返回值,在下面的例子中,输入输出类型都是String:

 class MyActivityResultContract: ActivityResultContract<String,String>(){
        override fun createIntent(context: Context, input: String?): Intent {
            return Intent(context,SecondActivity::class.java).apply {
                putExtra("name",input)
            }
        }

        override fun parseResult(resultCode: Int, intent: Intent?): String? {
            val data = intent?.getStringExtra("result")
            return if (resultCode == Activity.RESULT_OK && data != null) data
            else null
        }

    }

如上代码,我们在createIntent方法中创建了Intent,并且携带了参数name,在parseResult方法中,获取了返回的数据result

3. 注册协议,获取启动器-ActivityResultLauncher

注册协议,使用registerForActivityResult方法,该方法由ComponentActivity或者Fragment提供,接受2个参数,第一个参数就是我们定义的Contract协议,第二个参数是一个回调ActivityResultCallback<O>,其中O就是前面Contract的输出类型。代码如下:

private val myActivityLauncher = registerForActivityResult(MyActivityResultContract()){result ->
   Toast.makeText(applicationContext,result,Toast.LENGTH_SHORT).show()
   textView.text = "回传数据:$result"
}

如上代码,注册了MyActivityResultContract,registerForActivityResult方法的返回值是ActivityResultLauncher, 因此我们定义了一个myActivityLauncher,回调方法中,result就是从上一个界面传回的值。这里我们简单的用Toast显示。

4. 最后,调用启动器的launch方法开启界面跳转

MainActivity中添加一个Button,点击Button时,调用launch方法跳转:

 button.setOnClickListener {
      // 开启页面跳转
      myActivityLauncher.launch("Hello,技术最TOP")
 }

SecondActivity的代码很简单:

class SecondActivity : AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.second_layout)

        val name = intent.getStringExtra("name")
        textView3.text = "接收到的数据为:$name"

        button2.setOnClickListener {
            val intent = Intent().apply {
                putExtra("result","Hello,依然范特西稀,我是回传的数据!")
            }
            setResult(Activity.RESULT_OK,intent)
            finish()
        }
    }
}

以上3步,就实现了使用新的Activity Results API 来完成Activity之间的数据传递,并获取Activity返回的数据

看一下效果:

这就完了吗?

你可能会有疑问,虽然确实减少了代码耦合,但是使用并不简单啊。

确实,但这并没有完!!!

预定义的Contract

大伙都看出来,新的Activity Results API使用起来好像有点麻烦,每次都得定义Contract。Google肯定考虑到了这个问题的,于是,Google 预定义了很多Contract,把你们能想到的使用场景基本上都想到了,它们都定义在类ActivityResultContracts中,有以下这些Contract:

StartActivityForResult() 
RequestMultiplePermissions()
RequestPermission()
TakePicturePreview()
TakePicture()
TakeVideo()
PickContact()
CreateDocument()
OpenDocumentTree()
OpenMultipleDocuments()
OpenDocument()
GetMultipleContents()
GetContent()

下面分别介绍一下这些Contract:

  • StartActivityForResult: 通用的Contract,不做任何转换,Intent作为输入,ActivityResult作为输出,这也是最常用的一个协定。
  • RequestMultiplePermissions: 用于请求一组权限
  • RequestPermission: 用于请求单个权限
  • TakePicturePreview: 调用MediaStore.ACTION_IMAGE_CAPTURE拍照,返回值为Bitmap图片
  • TakePicture: 调用MediaStore.ACTION_IMAGE_CAPTURE拍照,并将图片保存到给定的Uri地址,返回true表示保存成功。
  • TakeVideo: 调用MediaStore.ACTION_VIDEO_CAPTURE 拍摄视频,保存到给定的Uri地址,返回一张缩略图。
  • PickContact: 从通讯录APP获取联系人
  • GetContent: 提示用选择一条内容,返回一个通过ContentResolver#openInputStream(Uri)访问原生数据的Uri地址(content://形式) 。默认情况下,它增加了 Intent#CATEGORY_OPENABLE, 返回可以表示流的内容。
  • CreateDocument: 提示用户选择一个文档,返回一个(file:/http:/content:)开头的Uri。
  • OpenMultipleDocuments: 提示用户选择文档(可以选择多个),分别返回它们的Uri,以List的形式。
  • OpenDocumentTree: 提示用户选择一个目录,并返回用户选择的作为一个Uri返回,应用程序可以完全管理返回目录中的文档。

上面这些预定义的Contract中,除了StartActivityForResultRequestMultiplePermissions之外,基本都是处理的与其他APP交互,返回数据的场景,比如,拍照,选择图片,选择联系人,打开文档等等。使用最多的就是StartActivityForResultRequestMultiplePermissions了。

有了这些预定义的Contract, Activity之间传递数据就简单多了,比如,前面的例子,可以简化成这样:

1. 注册协议,获取ActivityResultLauncher:

 private val myActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ activityResult ->  
        if(activityResult.resultCode == Activity.RESULT_OK){
            val result = activityResult.data?.getStringExtra("result")
            Toast.makeText(applicationContext,result,Toast.LENGTH_SHORT).show()
            textView.text = "回传数据:$result"
        }
    }

2. 构造需要传递的数据,启动页面跳转

 button.setOnClickListener {
        val  intent = Intent(this,SecondActivity::class.java).apply {
             putExtra("name","Hello,技术最TOP")
        }
        myActivityLauncher.launch(intent)
}

OK,就是这么简单!!!

在比如,我们的权限,申请,请看代码:

request_permission.setOnClickListener {
    requestPermission.launch(permission.BLUETOOTH)
}

request_multiple_permission.setOnClickListener {
    requestMultiplePermissions.launch(
        arrayOf(
            permission.BLUETOOTH,
            permission.NFC,
            permission.ACCESS_FINE_LOCATION
        )
    )
}

// 请求单个权限
private val requestPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        // Do something if permission granted
        if (isGranted) toast("Permission is granted")
        else toast("Permission is denied")
    }

// 请求一组权限
private val requestMultiplePermissions =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions : Map<String, Boolean> ->
        // Do something if some permissions granted or denied
        permissions.entries.forEach {
            // Do checking here
        }                                                                             
}

有了这个,我们就可以抛弃所有的第三方权限请求框架,只需要将这两个Contract放到BaseActivity中,或者抽取到一个单独的类中,就能随时随地申请权限。是不是很方便!!!

在非Activity/Fragment的类中接收Activity的结果

在Activity和Fragment中,我们能直接使用registerForActivityResultAPI ,那是因为ConponentActivityFragment基类实现了ActivityResultCaller 接口,在非Activity/Fragment中,如果我们想要接收Activity回传的数据,可以直接使用 ActivityResultRegistry 来实现。

比如,用一个单独的类来实现协议的注册和启动器的启动:

    class MyLifecycleObserver(private val registry : ActivityResultRegistry)
            : DefaultLifecycleObserver {
        lateinit var getContent : ActivityResultLauncher<String>

        fun onCreate(owner: LifecycleOwner) {
            getContent = registry.register("key", owner, GetContent()) { uri ->
                // Handle the returned Uri
            }
        }

        fun selectImage() {
            getContent("image/*")
        }
    }

    class MyFragment : Fragment() {
        lateinit var observer : MyLifecycleObserver

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

            observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
            lifecycle.addObserver(observer)
        }

        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            val selectButton = view.findViewById<Button>(R.id.select_button)

            selectButton.setOnClickListener {
                // Open the activity to select an image
                observer.selectImage()
            }
        }
    }

示例中,我们在MyLifecycleObserver中实现协议注册和启动器启动,为什么要实现LifecycleObserver 呢?因为,使用生命周期组件,LifecycleOwner 会在 Lifecycle 被销毁时自动移除已注册的启动器。不过,如果 LifecycleOwner 不存在,则每个 ActivityResultLauncher 类都允许您手动调用 unregister() 作为替代。但在使用ActivityResultRegistry时,Google官方强烈建议我们使用可接受 LifecycleOwner 作为参数的 API。

Activity和Fragment中为什么不需要手动调用unregister()呢?,因为ComponentActivity和Fragment已经实现了LifecycleObserver

ComponentActivity源码在这里:

Fragment中源码在这里:

总结

新的Activity Result API提供了一种执行许多常见任务的简便方法,比如我们调用第三方APP 获取数据,请求权限、拍照、选图片、获取联系人等等。除此之外,降低了代码的耦合,减少了样板代码(比如,定义requestCode常量)。另外,startActivityForResultonActivityResult已经被废弃,官方也是强烈建议使用这种方式来进行数据传递并获取Activity返回的数据。

还没用起来的,赶快使用起来,贼香!!!祝大家编码愉快!!!

查看原文

赞 4 收藏 3 评论 0

依然饭特稀西 关注了用户 · 2020-10-09

郭霖 @guolin_5f69be4c1b80e

Android软件开发工程师。从事Android开发工作多年,有着丰富的项目实战经验,负责及参与开发过多款移动应用与游戏, 对Android系统架构及应用层开发有着深入的理解。

2014年, 创作了《第一行代码——Android》一书,目前《第一行代码 第3版》已出版。

关注 841

依然饭特稀西 赞了文章 · 2020-09-09

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 上看到大家的应用!

查看原文

赞 8 收藏 0 评论 0

依然饭特稀西 赞了文章 · 2020-09-06

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

查看原文

赞 5 收藏 4 评论 0

依然饭特稀西 发布了文章 · 2020-09-03

热门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。那你呢?喜欢哪些?欢迎在评论区留言。

查看原文

赞 12 收藏 9 评论 0

依然饭特稀西 收藏了文章 · 2020-09-02

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

查看原文

认证与成就

  • 获得 133 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-23
个人主页被 4.5k 人浏览