React/Antd Warning移除大法——整个世界清净了

好久没更了,真的是好久... 续个命233 HP+100

最近在搬砖搞一套后台系统,用的react 17.0.1 + antd 4.12.3.

闲下来瞅了瞅控制台,艾玛,几十个warning...

90%都是eslint对react hook依赖的警告。虽然说可以直接 eslint-disable-next-line 一劳永逸,然而总感觉不太专业...

于是乎,为了去除warning,按照react官方推荐写法,简简单单的一段代码就变得emmmmmm....

消除hook warning后,最终剩下两个不明所以的warning:

这两个警告只能通过升级库解决,并非自身代码问题。

一、

SharedArrayBuffer will require cross-origin isolation as of M91, around May 2021. See

原因是react 17.0.1 使用了 SharedArrayBuffer 这个API,这个API在将来随着chrome的更新,可能会导致旧功能不可用,所以会报警告。

解决方法: 将react 17.0.1 升级至 17.0.2 。
react在新版本中解决了这一隐患

二、

You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.

这是antd报的警告,提示antd未按需加载。这个警告困扰了我许久,webpack已经设置babel-plugin-import插件来做tree-shaking了,而且分析打包出的文件,确实实现了按需加载。蛋柿,这个警告阴魂不散... 最终发现时antd的bug,已经在16.13.0修复在本地开发时会报 tree-shaking 警告信息的问题。

解决方法: 将antd 4.12.3 升级至 4.13.0 以上即可。

IntersectionObserver

IntersectionObserver

最近发现一个有趣的API,实际上这个api已经面世好几年了,但最初浏览器兼容性有限,加上此功能使用场景不频繁,且有成熟的替代方案,所以知道的人并不多。

这个接口的功能,简单来说就是:监测页面中的某个元素的可视状态。可以是该元素相对于整个视窗的可见性,也可以是相对于某个父元素的可见性。我们平时使用较多的场景大概有:

举个栗子:

  1. 无限加载:滑动到底部时开始加载下页
  2. 吸顶效果:在页面滑动时触发某元素的悬浮效果(如下滑到一定位置,顶栏悬浮固定)

目前我们常用的成熟方案是,监听页面/元素的scroll事件,在scroll事件中通过判断元素的位置,来进行相应的逻辑处理。或者是分别通过touchstart touchmove touchend 等触摸事件,来判断滑动方向,滑动距离,再做出相应处理。主流的一些相关功能库基本也是这样的实现原理,如iscroll.js等。

从微信小程序的一个bug说起说起

这些方案使用上并没有太明显的问题,然而最近在开发小程序时,有一个滑动吸顶的效果,需要根据滑动距离变换顶部的背景,采用的便是上述的方案,监听scroll-view的滚动事件,根据元素距离顶部的距离实时判断修改。

为了提高性能,多滚动事件采取了节流,在此过程中发现小程序的一个问题,那就是采用节流后,如果滑动过快,小程序bindscroll事件返回的组件距离顶部位置会有错误,比如我从距顶50像素迅速滑到0,此时回调函数返回的任然是50。试验了下跟节流时间关系不大,即便设置几毫秒的节流间隔,已然存在这个问题。而如果不设置节流,在低端机,尤其安卓机上频繁触发,导致页面相当卡顿。

最后搜索半天,在微信开发者社区发现了原因所在。根据官方给出的解决方案,给scroll-view组件增加一个属性throttle="{{false}}"(官方文档中并无此属性),原因是官方的组件在配发滑动事件时,默认开启了节流,在滑动过快时,有部分事件可能被节流掉,就出现了上述情况,去掉官方节流,保证事件都能触发,然后自己再实现节流即可。

1
2
3
<scroll-view
throttle="{{false}}"
bindscroll="handleScroll">

说道半途记起的一个bug,回归正题。
使用监听scrool事件的方法,不是用节流肯定是不行的,节流事件如果太短,那么节流效果不明显,时间太长的话,事件响应又不够及时。经过权衡设置了大概50~100ms的延时,让事件的配发频率和响应延时都能接受,但效果任然不够理想,达不到极致的体验。理想情况下,我们追求:响应及时、性能负担小。

无意间看到微信的一个新的api wx.createIntersectionObserver,简单了解后不明觉厉,然后发现浏览器端也有对应的IntersectionObserver API(“交叉观察器”)。由此引出一种新的更完美的解决方案,即抛弃scroll事件,采用新的“交叉观察器”

scroll事件的一些缺点:
1、响应密集,会造成性能问题
2、为解决1,采用节流后响应不够及时
3、实现某些需求较为复杂:如,当视频滑到可视区域时自动播放等,需要判断列表中某元素的位置,较为复杂
4、统计列表中个元素的曝光量(这个用scroll时间监听的话感觉头都裂了)

IntersectionObserver

MDN doc介绍:

1
IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

属性:

  • IntersectionObserver.root

    要监听元素的父级窗口,及我要监听的元素,是相对于那个视窗的可见性,是整个全屏的视窗,还是页面中某个容器。不传默认为全局的视窗

  • IntersectionObserver.rootMargin

    相对于视窗的偏移量

  • IntersectionObserver.thresholds

    阈值的列表, 按升序排列。阈值指所监听元素的可视比例,全部不可见为0,全部可见为1,配置此属性后,会在到达对应阈值后触发回调事件,如[0.1, 0.4, 1] 表示在可视比例达到10%、40%、100%时,会分别相应回调事件;如果不设置,默认是在元素全显和全隐时分别触发。

方法:

  • IntersectionObserver.disconnect()

    使IntersectionObserver对象停止监听工作。

  • IntersectionObserver.observe()

    使IntersectionObserver开始监听一个目标元素。

  • IntersectionObserver.takeRecords()

    返回所有观察目标的IntersectionObserverEntry对象数组。

  • IntersectionObserver.unobserve()

    使IntersectionObserver停止监听特定目标元素。

示例

1
2
3
4
5
6
7
8
9
10
var intersectionObserver = new IntersectionObserver(function(entries) {
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
if (entries[0].intersectionRatio <= 0) return;

loadItems(10);
console.log('Loaded new items');
});
// start observing
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

阮一峰老师有一篇文章写的很简洁易懂,推荐去看看。

微信小程序 wx.createIntersectionObserver

小程序的此API与web端的API如出一辙,使用方法大致相似。

使用步骤:

1、wx.createIntersectionObserver 创建并返回一个IntersectionObserver 对象实例。

2、指定可视窗口参照物

  • IntersectionObserver.relativeTo 传入一个父级元素,作为可视窗口
  • IntersectionObserver.relativeToViewport 指定页面的整个窗口作为可视参照

3、IntersectionObserver.observe 开始监听

使用步骤大概就是这三步,具体的配置可参考文档。

总结

研究哇这个api后,果断将判断吸顶时用的scroll时间替换为IntersectionObserver,体验就是写法上更简洁,性能更好,能满足更多需求。

所以基本上IntersectionObserver可以完爆scroll了,一些使用广泛的库也开始使用此方案了,so 赶快用起来吧。

微屁恩使用指南之 —— V2Ray

2020.02.06
廊坊,大雪;陕南,雨 ;平凉,大晴。
新冠,正嚣张;强食自爱,正当时。

DC72E462-DEC2-4724-91DE-C19FE3FEE72E_Origina

弱小和无知不是生存的障碍,傲慢才是。

—— 刘慈溪,《三体》

正值立春,雨雪飘零,又时下革命形式严峻,当即抄写《卜算子·咏梅》。吾辈当继承毛泽东同志坚贞不屈的革命意志,面对疫情,严阵以待,做好防护。

IMG_2981

下文,非刁民勿入!

这些墙很有意思,刚开始你恨它,慢慢的你习惯于其中,时间久了,你发现离不开它了,这就是“体制化”。

—— 《肖生克的救赎》(The Shawshank Redemption)

一、前言

本文旨在介绍新的科学上网方式——V2Ray的使用。

从最开始GFW(Great Firewall,中文也称中国国家防火墙)之父,国之栋梁方滨兴院士,主导、研发、建立国家防火墙工程之后,大陆通往世界的互联网道路从此被阻隔。从建立伊始至今,此墙为广大中国人民屏蔽了无数的不良言论,得以使国内长期处于一片祥和的氛围当中。由此也诞生了中国最具良心、以搜索引擎起家的顶级互联网企业百度,此墙可谓国之重器。

然穷山恶水出刁民,总有刁民想翻墙。刁民谓:

面对威权压迫,你是甘为一辈子懦夫,还是坚定勇气,试着改变这个污浊不堪的世界?于此,虽千万人吾往矣。全新的shadowsocksR管理助手——SSR.Go,现已问世。长夜漫漫,父说,擦亮双眸的你,需要一道光明,使者便从了他的意,把光明给带来了。

—— Github SSR.Go 作者

时过境迁,在广大院士们的忘我科研中,墙越来越不易翻越,刁民们逐渐疲于应对,只能不断更换翻墙方式。从最初的ss到ssr,再到此文介绍的V2Ray,只为了看一眼墙外的景色。

V2Ray相比于ss系列,有何不同。首先直白的说下防火墙及翻墙与被封的原理。

防火墙的原理,就是通过各电信运营商,在大陆与外网通信的网络出入口,对传输的内容进行筛选、过滤。如果发现是国军,立马原地劝返。如果是共军则放行。至于如何识别是共军还是国军呢,大致的方法有 IP黑名单内容审查DNS劫持[1]

  • IP黑名单,简单说将外网的一些网站ip地址加入黑名单,如FB/Google等,当你的请求指向的地址在黑名单中,立即截断。
  • 内容审查,会有一份敏感词的列表,对于未加密传播的HTTP内容,拦截解析之,如果内容中包含敏感词,不好意思,白白了您嘞。
  • DNS劫持,可以理解为域名黑名单,对黑名单中的域名,将其解析到虚拟的ip地址。

ss/ssr的原理相同,都是通过socks5代理。举个栗子,你有一台香港的服务器,搭建了微屁恩,此时你要访问谷歌,会先与香港的服务器建立连接,并告诉香港服务器你要访问的内容,由香港服务器替代你,间接访问谷歌得到内容后,再转述给你。这里可以看到一个关键点就是,需要一个身在大陆防火墙之外的“代理人”,你们之间秘密通信,他替你完成内容的获取后转述给你。

ss作为初代梯子,简单快捷,造福了很多刁民,随后李大钊同志被请去喝茶,迫于淫威删除了开源代码。在李大钊同志倒下后又站起来了陈独秀同学,开源了ssr,并宣布ss已于识别检测,ssr加入了众多的加密混淆方法,更利于开展地下工作。然陈独秀同志最终也难逃被人肉的命运,最终也删除ssr代码。

ss和ssr在与GFW多年的肉搏下,其特性已经逐渐被摸清,GFW已经为这二位定制了一套战术,能够较高效的找到这二者的破绽,识别并封杀。在太平盛世下,这二位还能苟活些时日,然而一到有热点事件发生,GFW便会降低特征匹配率,这二位分分钟别秒杀。这就像人脸识别,系统会将预先录入的人像和当前人像对比,当达到一个设定的相似度,例如90%时,认为匹配成功。当特殊时期时,会将此阈值降低,例如降低到50%,此时只要你稍有国军的一点特征,便把你抓进去。ss和ssr这两位在特殊时期就显得无能为力。而这种时期恰是刁民们翻墙的高峰期。

V2Ray 在这种情况下应用而生,通过HTTPS、WS等一系列手段[2],将传输数据进行更好的加密伪装,从而能活的更长久。V2Ray由于其良好的防封表现,其功能也相当繁多、复杂。

SS => SSR => V2Ray,随着GFW的不断升级,梯子的复杂度也在指数增加,出现的结果就是,梯子越来越隐蔽稳定,但能搞定梯子的人变的越来越少😳

二、V2Ray 服务端配置

至此处,本文的核心内容已经完结。接下来的都是划水 是一些优秀文章的合集。

服务端推荐使用233boy开发的集成脚本,简单快捷 👉 服务端配置教程

三、客户端下载

暂将客户端分为:

  • macOS

    Mac下本人使用的是 👉 V2rayU

  • Windows

    未测试,无推荐,软件列表见本节末尾。

  • iOS

    iOS下暂无免费的客户端,付费客户端推荐Shadowrocket
    找到一个雷锋分享 AppStore 账号,已经购买了此软件,赶紧下起来。

    1
    2
    账号: rh72uw13@icloud.com
    密码: Dd112211

    账号过期的话,可以去此处找 👉 雷锋 获取

  • Android

    安卓客户端推荐 👉 V2RayNG

V2Ray客户端众多,除了上述推荐的之外,其余各平台的客户端见 👉 客户端汇总

四、软件配置

软件配置就不多说了,添加配置,手动填写/导入url/扫码导入都可,导入服务器配置后,开启服务器配置即可。

全局模式即为浏览器所有流量走代理。pac模式即为自动识别是否需要走代理,更加智能,当然也有识别出错的情况。

推荐

1、浏览器:这里多说一个 V2Ray 配合谷歌浏览器的神器插件 SwitchyOmega 的用法。SwitchyOmega 跟ss/ssr搭配已经很久了。这里不同的是,V2Ray 使用TCP模式时,SwitchyOmega 配置的代理记得从socks5换成HTTP。

2、全局代理:当我们想让电脑除浏览器外的其他软件也能翻墙时,需要使用全局模式,Mac下推荐 Proxifier ,具体设置可以自行Google。

附录

[1] 防火长城
[2] V2Ray

H5在全屏Webview中双端适配刘海屏

背景:
最近遇到一个看似常规的H5需求,是App内嵌的一个功能模块,看样子跟往常一样重复造轮子就OK了,客户端开个Webview加载页面即可。

正常我们遇到最多的是下面这种类型:

这种的话一般是封装一个Webview包含返回+标题+分享功能,然后加载H5即可,返回即关闭Webview,标题是读取网页的Title属性,分享是调起客户端的分享弹窗。

然是这次的H5有点不寻常的东西:

  1. 导航栏除了返回键、title、右侧的操作菜单(进入另一个H5页),在title还有一个操作项❔,用于点击弹出说明框。
  2. 有一个穿透状态栏和导航栏的背景
    大概长下面这样:

向上面的这种复杂页面一般是客户端做的,但是!!!

IMG_1345_Origina

因为种种原因,最后商量用H5做。那看到这样的设计图,机智的攻城狮们一般会跟产品争论一番讨论说你这背景图做不了,只能到导航栏以下,并且你这个问号得移到其他地方bulabulabula。。。

然而作为一名专业的攻城狮,我们当然是奔着最佳的视觉感官+用户体验去的,遇到问题要克服之!

那么确定100%还原设计图后,首先想到的一个方法是和双端定义一系列协议,包括设置全屏背景图、在title后面加操作按钮(及隐藏方法,因为到其他页面就没了),在右侧加自定义的菜单,点击后可跳转其他页面。这个看着就很麻烦,涉及到一系列的交互想想就头疼,还不如直接原生写的痛快一点。和客户端你同学讨论后他们果然面露难色,在我说完第二秒就否决的这种做法,原因很简单:

1、交互太复杂
2、扩展性太差,下次H5的设计换个样子又得加新交互

最终决定采用全屏Webview的形式,整个页面交给H5控制,这样不管页面设计成什么样都能实现,什么全屏背景,设么导航啦加各种东西通通不在话下。于是愉快的开发开始了。

然鹅在打码到一半的时候我意识到一个问题:双端的状态栏高度不一致,并且现在还有刘海屏存在🌚 这可肿么办?

一、IOS适配

首先对于IOS来说,乔帮主整的还是比较规范的,毕竟IOS闭源系统只能跑在Apple硬件上,设备型号有限,已知各机型尺寸如下:

注: 这里获取到的px值跟web中的px虽然单位一样,但并不是我们需要的值!!!Web所需的px实际为IOS中的pt值…,px转pt需要根据设备的ppi(Pixels Per Inch: 像素密度)进行转换:

px: pixel 像素,是屏幕上的显示的基本点,他并不是长度单位,这个点可以很大,也可以很小。点小的话就很清晰,我们称之为“分辨率高”,反之就是“分辨率低”。所以像素是一个相对单位。

pt: point 准确的说法是一个专用印刷单位“镑”,大小为1/72英寸,是一个长度单位。也是绝对长度。

可以看到ios中的px转pt根据设备的ppi大概是3:1/2:1/1:1转换。
转换完可以看到:

  • 4.7寸6、6s、7、8,状态栏高度为20pt,导航栏高度为44pt.
  • 5.5寸的6p、6sp、7p、8p,状态栏高度为18pt,导航栏高度为44pt.
  • 拥有刘海屏的X、XR、XS、XS MAX、11等一系列刘海屏,状态栏高度为44pt,导航栏高度为44pt.

不难发现:

  • 导航栏 高度所有机型都为44pt;
  • 状态栏 高度大致可以根据是否为刘海屏分为两类。没有刘海屏的大小机型分别为18和20pt,可以近似的看成都是20pt来处理,问题不大,有刘海屏的则统一为44pt高,跟导航栏高度相同。

适配方案:

iOS端的适配方案有两种:Apple官方适配方案、机型区分适配、jsBridge方案

Apple官方适配方案:

1、在粪叉之后引入了一个新概念:“safe area(安全区域)”,安全区域指屏幕内不受圆角、齐刘海、底部小黑条等元素影响的可视窗口。如下图:

2、同时,从iOS11开始,为了适配刘海屏,Apple公司对HTML的viewport meta标签做了扩展

1
<meta name="viewport" content="viewport-fit=cover">

viewport-fit=cover可设置为auto, contain, cover三种状态,这里我们重点使用cover值,指页面完全充满屏幕。

3、iOS11同时新增了一个特性,constant(safe-area-inset-*),这是Webkit的一个CSS函数,用于获取安全区域与边界的距离,有四个预定义的变量(单位px):

  • safe-area-inset-left:安全区域距离左边界距离,横屏时适配
  • safe-area-inset-right:安全区域距离右边界距离,横屏时适配
  • safe-area-inset-top:安全区域距离顶部边界距离,竖屏下刘海屏为44px,iphone6系列20px,竖屏刘海适配关键
  • safe-area-inset-bottom:安全区域距离底部边界距离,竖屏下为34px,竖屏小黑条适配关键

这样适配方案就比较明确了:

  1. 首先通过设置<meta name="viewport" content="viewport-fit=cover">让页面充满全屏
  2. 通过Webkit内置的CSS函数,获取安全区域与各边之间的间距,然后通过padding/margin/绝对定位等方式,让页面元素展示在安全区域内。

注: Webkit在iOS11中新增CSS Functions: env( )替代constant( ),文档中推荐使用env( ),而 constant( ) 从Safari Techology Preview 41 和iOS11.2 Beta开始会被弃用。在不支持env( )的浏览器中,会自动忽略这一样式规则,不影响网页正常的渲染。为了达到最大兼容目的,我们可以 constant( ) 和 env( ) 同时使用。

1
2
padding-top: constant(safe-area-inset-top); /* iOS 11.0 */
padding-top: env(safe-area-inset-top); /* iOS 11.2 */

最终适配代码如下:

使用@supports查询机型是否支持constant() / env()实现兼容代码隔离,个别安卓也会成功进入这个判断,因此加上-webkit-overflow-scrolling: touch的判断可以有效规避安卓机。

1
2
3
4
5
6
7
8
9
10
11
12
13
@supports ((height: constant(safe-area-inset-top)) or (height: env(safe-area-inset-top))) and (-webkit-overflow-scrolling: touch) {
.fullscreen {
/* 适配齐刘海 */
padding-top: 20;
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);

/* 适配底部小黑条 */
padding-bottom: 0;
padding-bottom: costant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
机型区分适配

这个就比较简单粗暴无脑了。因为目前市面上已有的Apple手机尺寸我们都是已知的,那剩下的就是css中的media适配了:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* iphone x / xs / 11 pro*/
@media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) {
...
}
/* iphone xr / 11 */
@media only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) {
...
}
/* iphone xs max / 11 pro max */
@media only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) {
...
}
.....

emmmmmm… 工作量大了点,另外每年9月份发布会后要及时更新代码🙈

jsBridge方案

如果你跟客户端小哥哥的关系比较好的话,就用这种方案吧😳,让客户端写个方法获取状态栏高度,然后在页面加载的时候通过jsbridge调用获取到状态栏高度,然后设置页面样式即可。

好了,鄙人想到的iOS适配方案到此为止。

二、Android适配方案

整完相对规范的iOS,开源的Android就相当眼花缭乱了,机器厂商百花齐放,各厂商的机型也是眼花缭乱。Android机型成百上千,适配方案反而变的简单了!!!why? 因为只有一种方案:JSBridge

像上述iOS的适配方案中,官方适配方案Android肯定是么得了,毕竟机型太多,搞不了官方规范。其次就是CSS media 查询精准适配,如果你的应用只针对于少数机型,那这种方案还是可以用用的,倘若不是那就拜拜了您嘞。

jsBridge方案

同iOS,客户端获取状态栏高度后,H5通过JSBridge交互拿到状态栏高度,设置页面样式避开齐刘海区域。

参考:

vue 项目在低配机器上线及优化

背景

功能:最近帮朋友的忙,抽空用vue-cli3脚手架,开发一个网站,适配pc和mobile,两端分别有约10个页面,也就是十个路由,网站中的图片比较多,且使用了几个自定义字体,ttf文件都在2m左右。

服务器环境:本着能抠一点就抠一点的原则,购买的是阿里云ECS的入门配置,1核1G,40G硬盘,1M带宽,基本上是最低配了。(本来想用搬瓦工来着,但最近搬瓦工被封的厉害,风险比较大,可能买来用一两周就GG了。)

由于是空闲时间搞得,时间也比较急,前期没进过任何优化,直接编译完上线,发现首次全部加载完成耗时32s !分析发现原因如下:

  • 每页加载的图片都在1025张左右,所有的图片全部没有压缩,尺寸较大,尤其顶部banner达12M左右,其余大图在700800kb之间,每页的图片加起来在5M10M之间。
  • 引入的自定义平方字体,使用了regular,medium,thin等几种,每种ttf字体都在2M左右,加起来字体有7、8M。
  • 首屏加载所有路由,加载耗时。
  • 由于服务器是1M带宽,那么每秒下行的最高速度约在120k左右,加载以上超过10M的资源,最快理论速度也在10s以上,且由于不是固态硬盘,速度上也不快。

综合以上的问题,对存在的问题进行逐个优化:

1、路由懒加载:

对于多路由的SPA,首次加载如果引入所有路由,那么首屏的时间势必会比较耗时。采用路由懒加载,只有当访问到相应的路由时,才会加载对应路由,缩短首屏加载时间。 参考

1
2
import Home from './pages/pc/home.vue'
{ path: '/home', component: Hone, label: '首页'},

=>

1
{ path: '/home', component: () => import('./pages/pc/home.vue'), label: '首页'},

2、图片压缩优化:

所有图片使用 https://tinypng.com 进行压缩,可以压缩约80%左右的大小,同时图片显示效果基本不变。

3、字体压缩:

当引用自定义的字体后,往往字体文件达1~2M,严重拖累加载进度。
使用 font-spider 按需加载字体,可由2M缩减为3k,效果显著。

字体压缩步骤是:

  1. 先将vue编译
  2. 执行 font-spider index.html

如果将资源文件放置在非本地(如cdn),那么无法直接使用font-spider压缩远程资源。
此时可以使用 fsp 压缩远程资源。示例配置如fspconfig.js

1
2
3
4
5
6
7
{
"localPath" : "./static/fonts/",
"onlinePath" : "http://cdn.lantianren.net/static/fonts/",
"url" : [
"http://lantianren.net/index.html"
]
}

执行 fsp run 即可。

4、将本地的静态文件批量上传至七牛cdn

使用 qshell 工具即可。执行:qshell qupload /Users/ludis/Work/src/qshell/qupload.json

配置强制刷新,每次修改完文件后,可以强制刷新上传至七牛。同时网站存在cdn缓存,不能及时更新,可以手动处理

配置文件如下:

1
2
3
4
5
6
7
{
"src_dir": "/Users/ludis/Desktop/work/lt-educate/dist",
"bucket": "lt",
"overwrite": true,
"check_exists": true,
"rescan_local": true
}

编译配置

vue.config.json: 根据运行模式设置资源根路径。可以将静态资源上传至cdn(七牛)加速。

1
2
3
4
5
6
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? 'http://cdn.lantianren.net/'
: './',
assetsDir: 'static'
}

经过这些优化后,首屏加载时间在2s~2.5s之间浮动,由于是SPA应用,所以其他页面基本是秒开,且图片进压缩,cdn处理后,加载也很快。整体上从开始的32S降至2s开屏,基本上达到可接受范围。

👉 体验地址

其余优化方式:

1、如果资源放置在本地,可以配置nginx gzip压缩
2、如果项目更加庞大复杂,可以继续使用ssr服务端渲染。

结语:

平时在工作中开发小型项目,在公司较高的硬件配置下比较少有优化加载速度的需求和想法。当使用低配环境时,就会发现还是有很多可优化的点。

Flutter 实战进阶

Flutter 在实际开发中遇到的一些问题及解决方案,作为笔记记录。

1、container width、height 100%

1
2
3
4
5
FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: ,
)
1
2
double width = MediaQuery.of(context).size.width
double height = MediaQuery.of(context).size.height

2、沉浸式背景图片

背景图片铺满Appbar及状态栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
Container(
child: Image.network('https://www.bing.com/az/hprichbg/rb/Punakaiki_DE-DE0884339574_1920x1080.jpg'),
color: Colors.lightGreen,
),
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: Text('Coin'),
),
body: MyCoinPage(),
)
],
);
}

3、APPBar no shadow

AppBar默认底部有阴影。去掉只需要配置参数即可:

1
2
3
AppBar(
elevation: 0.0,
)

4、自定义字体

1
2
3
4
5
6
7
8
9
10
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

fonts:
- family: Kphi
fonts:
- asset: assets/fonts/Komet-Pro-Heavy-Italic.otf

5、Tab切换动画

Tab切换需加入SingleTickerProviderStateMixin

通过 with SingleTickerProviderStateMixin 实现Tab页的切换动画效果。因为初始化animationController的时候需要一个TickerProvider类型的参数Vsync参数,所以我们混入了TickerProvider的子类SingleTickerProviderStateMixin

6、 逐帧动画 FrameAnimationImage

Flutter 逐帧动画 FrameAnimationImage。 逐帧动画: 依次顺序循环播放多张图片组成一个动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/// Flutter FrameAnimation 逐帧动画
import 'package:flutter/material.dart';

class FrameAnimationImage extends StatefulWidget {
final List<String> assetList;
final double width;
final double height;
final int interval;

FrameAnimationImage(
{this.assetList, this.width, this.height, this.interval = 200});

@override
State<StatefulWidget> createState() {
return _FrameAnimationImageState();
}
}

class _FrameAnimationImageState extends State<FrameAnimationImage>
with SingleTickerProviderStateMixin {
// 动画控制
Animation<double> _animation;
AnimationController _controller;
int interval = 200;

@override
void initState() {
super.initState();

if (widget.interval != null) {
interval = widget.interval;
}
final int imageCount = widget.assetList.length;
final int maxTime = interval * imageCount;

// 启动动画controller
_controller = new AnimationController(
duration: Duration(milliseconds: maxTime), vsync: this);
_controller.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_controller.forward(from: 0.0); // 完成后重新开始
}
});

_animation = new Tween<double>(begin: 0, end: imageCount.toDouble())
.animate(_controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});

_controller.forward();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
int ix = _animation.value.floor() % widget.assetList.length;

List<Widget> images = [];
// 把所有图片都加载进内容,否则每一帧加载时会卡顿
for (int i = 0; i < widget.assetList.length; ++i) {
if (i != ix) {
images.add(Image.asset(
widget.assetList[i],
width: 0,
height: 0,
));
}
}

images.add(Image.asset(
widget.assetList[ix],
width: widget.width,
height: widget.height,
));

return Stack(alignment: AlignmentDirectional.center, children: images);
}
}

7、 时间格式化库 intl

8、 时间格式化库intltextDirection冲突

报错如下:

1
The getter 'rtl' isn't defined for the class 'TextDirection'

参考: Stack Overflow

解决方案:

1
import 'package:intl/intl.dart' as intl;

9、 convert string to date

dart将时间字符串转为date库:

参考: Stack Overflow

1
2
var parsedDate = DateTime.parse('1974-03-20 00:00:00.000');
print('dateTime: $parsedDate');

10、 tabcontroller.addListener

Tab切换时,可以通过tabcontroller.addListener监听切换事件。在实际开发中发现如下问题,可以根据需求选择不同的判断方式:

1
2
3
4
5
6
7
_tabController = new TabController(length: 2, vsync: this);
_tabController.addListener(() {
// _tabController.addListener...
if (_tabController.indexIsChanging) {
// _tabController.indexIsChanging...
}
});

当点击切换时:

  • _tabController.addListener 触发两次
  • _tabController.indexIsChanging 触发一次

当滑动切换时:

  • _tabController.addListener 触发一次
  • _tabController.indexIsChanging 不触发

11、 FutureBuilder => Future widget

参考: FutureBuilder 延迟加载组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@override
Widget build(BuildContext context) {
return new FutureBuilder(
future: getTextFromFile(),
initialData: "Loading text..",
builder: (BuildContext context, AsyncSnapshot<String> text) {
return new SingleChildScrollView(
padding: new EdgeInsets.all(8.0),
child: new Text(
text.data,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
));
});
}

Future<String> getFileData(String path) async {
return await new Future(() => "test text");
}

Future<String> getTextFromFile() async {
return getFileData("test.txt");
}
}

12、获取页面生命周期

使用 with WidgetsBindingObserver后,可以对页面的生命周期进行监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('current state: $state');
setState(() {
_lastLifecycleState = state;
});
}

}

13、页面重构

1、TabView + pull_to_refresh 如何优雅共存
当页面中有Tab切换,且每个Tab页都是可以下拉刷新、上拉加载的组件时。如果每个TabView使用的是同一个组件,只是根据不同的标识显示不同的内容。在实际开发中发现,这么做虽然可以减少部分代码量,但是出现的问题是,Tab切换时每次都等于是重新渲染公共的组件,导致比较卡顿,且切换时会发生多个TabView间的数据混乱等一些列问题。而且多个TabView公用一个scrollController的话也会出很多问题。

正确的做法是,每个TabView应该是独立组件,包括互相独立的scrollController。只不过其中的一些子组件是可以抽出来共用的,这是没问题的。

2、listView显示占位图。

空列表的一般需要显示占位图,开始的做法是,判断列表为空时,直接返回一个占位图的Container,代替ListView。这样做之后,发现数据为空时就不能下拉刷新了,因为下拉刷新和ListView绑定在一起。于是改为下述方案,当数据为空时任然可以下拉刷新,实时更新数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
child: new SmartRefresher(
headerConfig: const RefreshConfig(completeDuration: 300),
enablePullDown: true,
enablePullUp: true,
onRefresh: _onRefresh,
headerBuilder: LoadingIndicator.buildHeader,
footerBuilder: LoadingIndicator.buildFooter,
controller: _refreshController,
child: (!loading && _payList.length == 0)
? ListView.builder(
itemCount: 1,
itemBuilder: (context, i) {
return _emptyTip();
},
)
: _listView(),
),

14、网络图片加载组件

默认的图片加载组件 networkImage,当图片链接错误、为空时,会报exception导致Crash。FadeInImage同理。

参考: GitHub

试了好几款组件都没有做错误处理,最终找到一个可用的组件:Flutter_image ,同时配合FadeInImage设置默认占位图,完美。

1
2
3
4
5
6
adeInImage(
fit: fit,
placeholder: AssetImage(placeHolderImage),
image: NetworkImageWithRetry(imgUrl),
),

15、主题颜色 ThemeData

16、网络图片加载失败Exception

使用 flutter_imageNetworkImageWithRetry 代替 NetworkImage 后,当图片404或者错误时,确实没有报Exception奔溃,但是安卓端却显示crash。

换了很多其他的插件都不好使,最终查明,NetworkImageWithRetry虽然可以catch exception不让应用奔溃,但是会触发Flutter.onError,而安卓端在检测到onError之后,当做crash处理,直接将页面关闭….

方案: 修改组件源码,图片加载失败时,不要触发Flutter.onError。

17、ActionSheet 点击隐藏

点击 ActionSheet 选择某项后,隐藏之。

1
Navigator.of(context, rootNavigator: true).pop("Discard");

18、阻止软键盘顶起页面

当软键盘弹起时,会将页面高度缩减,导致页面中的内容随着父级高度变化而变化。即软键盘弹起时将内容顶起,导致诸多问题。

解决方案:

1
2
3
4
5
6
return Scaffold(
appBar: AppBar(
title: new Text("通讯录"),
),
resizeToAvoidBottomPadding: false, //输入框抵住键盘 内容不随键盘滚动
);

19、dart正则

1
2
3
4
5
6
7
8
9
10
RegExp regExp = new RegExp(
r'(ludis)',
caseSensitive: false, // 是否大小写敏感
multiLine: false, // 待匹配文字是否是多行的
);

// 正则
var d = regExp.allMatches(text);
d.forEach((f) => print(f.group(0)));
print('match result, ${d.toString()}, ${d.toList()}');

20、键盘确认按钮样式

键盘的确认按钮会显示为“搜索”,点击后触发onSubmitted事件。

1
2
3
4
5
TextFormField( 
...
textInputAction: TextInputAction.done,
...
)

21、Map类型错误

json转换时报错如下:

1
'_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'

原因: 类型为Map的其实际类型为Map<dynamic, dynamic>而非Map<String, dynamic>

解决方法: 进行转换

1
2
Map json; // Map<dynamic, dynamic>
Map<String, dynamic> = new Map<String, dynamic>.from(json); // Map<String, dynamic>

22、GestureDetector 点击区域小

使用GestureDetector包裹Container,发现在Container内容为空的区域点击时,捕捉不到onTap点击事件。

参考: GitHub

解决方法:

1
2
3
4
5
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: (){},
child: Container()
)

23、TextField优化:

TextField decoration 可使用以下两种:

  • 使用 InputDecoration.collapsed() 。其高度不能和父级同高,导致可点击区域变小,用户体验不好。优势在于InputDecoration.collapsed()为无边框的输入框。
  • 使用InputDecoration。可使输入框高度和父级同高,扩大可点击区域。去掉边框即可。

解决方法:

1
2
3
InputDecoration(
border: InputBorder.none, // 去除输入框边框
)

24、Flutter hide keyboard

调用方法隐藏键盘。

1
FocusScope.of(context).requestFocus(new FocusNode());

25、去除TextFiled水纹效果 ripple effect

参考: how-to-disable-default-widget-splash-effect-in-flutter

解决方案:

  1. 使用自定义主体,覆盖默认主题
  2. 给组件包裹Theme容器,设置 splashFactoryColors.transparent。✅
1
2
3
4
5
child: new Theme(
data: new ThemeData(splashFactory: Colors.transparent),
child: new TextField(...),
),

Dart 数组、字符串常用方法:

何人在此

发现现在有些年轻人,看书,看剧,看很多东西,听很多东西。日常表述观点惯用:某某说过,我特别赞同/喜欢某某在哪哪说的什么。遇到事自己没主意时,不先思考下,首先想到的就是去问周边同龄的朋友,或者去网上搜索,然后筛选些自己看着舒服的答案直接拿过来用。这些好像没什么不对。然而自始至终都没有太多的自我思考,大多是嗟来之食。

人生来就是一张白纸。就中青少而言白纸又分为多种。

一种,就是开头说的这种。在感觉到自己的空白后,在慌张下开始”学习“,这确实是好事,然而吃相太不讲究,遇到的各种观点,只要看着舒服的,拿来就吃,饥不择食,快速的填充自己的世界观。找到自己觉得不错的观点就狼吞虎咽,收藏起来。到头来,说起什么话题,遇到什么问题,确能迅速从藏品中拿出来一个观点回应,来作为行为的指引,实则是别人的观点。而这些观点一般都是供大众消费用的,类鸡汤文。看似很有见地,但却是真真儿的是受人操控,活在大流中的傀儡。这种人我称之为会吸星大法的,大家都知道,在所有武侠小说中,吸星大法都是极为厉害的武功,能让人快速“变强”,却是一门邪门功夫,正派君子不沾。而这种轻而易举就能变强的功夫,确实让大部分人难以抵挡,这也是当前的主流功法。

这就像是一个人只知道苹果是吃的,但不知道长什么样子,这时遇到另一个人给他吃了口草莓,并说这是苹果,尝着草莓又甜又软的挺好吃,那他就觉得这是苹果了。因为这确实是吃的,而且挺好吃,就不用再去费劲确认他是不是了。当有人给他真正的苹果时,咬了一口发现没有草莓好吃,他大概率会说这不是苹果,草莓才是苹果。人们更愿意接收自己觉的舒服的观点,而不是去探究自己真正的看法,这毕竟是件费脑的事,而且时长伴随着自我颠覆的痛苦。

一种,知其白而任其白,属于心大无脑的。这种就不细说。

一种,白而不自知。这个也好理解。

一种,称之“半白”。这种人,遇到一个问题,抛给他一个话题,很可能一时半会说不出所以然,需要思考的时间。比起第一种能快速读取丰富档案库的人会显得比较虚。但是会仔细思考自身想法,中途或引经论据,最终得出经过“捯饬”的观点。过程虽然有些费脑辛苦,废了些时间,少了些潇洒,确是自身主观意识为主,即便与主流观念相悖,却也真实可贵。

至于那些被大众尊为上等人,为大众提供主流观点的这些人。也是从半白走过来的,属于高等半白。但却被很多人尊为真主一样,一副你说啥就是啥,恨不得把这些人的脑子复印一份过来,这样自己就圆满了。

现在好多的综艺节目,为了收视率,多弄着些吸引年轻人眼球的话题。然后各位"大咖"各抒己见,这些观点大多是些个人的生活经验和鸡汤掺杂的产物,再加点大众心理学作为佐料,乍听起来都是洗涤心灵的好道理,对于嗷嗷待哺的年青一代,简直是天赐良品。然而既然是综艺,就是以逗乐为主,如果从其中汲取一点点有用的观点并转化为自己的观点,就已然是极好的。可是很多年轻人,把这些节目当做学习课堂,把这些娱乐性质的产物一股脑的直接咽下去吸收。同时表现出吃饱后的满足感。实在是…现在的社会确实让年轻人很焦虑,很躁动,但把脑子交出去却是不明智的。

想起《超人:钢铁之躯》中的场景,佐德江军一意孤行,想用宝典来批量造人,孩子的出生由宝典统一生产,就像基因编码一样造出更强的孩子,而超人作为氪星第一个自然生产的孩子,开启了全新剧情…在这个信息爆炸,各种观点满天飞的时代,这些狂吸不止的年轻人,何尝不是思想层面的被基因编码呢,通过左拼右凑构建出来一个世界观,然后在此指引下快乐不自知的生活着,这跟氪星造出来的没有特色的孩子有啥区别。

而这些东西还会传染的,物以类聚,人以群分。这些身具吸星大法的人在一起,我在别地儿吸一点,再传给你,双方都会很满足。一个是“知识”的传播者,获得成就感,一个是“知识”的获得者,获得者一般伴随着满足感,所谓知识就是力量。简直比二手烟还上瘾,你一口我一口,大家都是好朋友。

相比这种思想混搭的人,看上去头头是道的人,我更愿意接触半白,甚至空白的人。就像中午没吃饭很饿,但宁愿喝一口清面汤,也不会吃被汪星人啃过的肉包子。

跟身兼吸星大法的这种人简单相处并无大碍,一旦交集变深就会初现端倪,就会发现他们坚定拥护别人塞过来的观点,并坚信不疑。他们的世界观、爱情观、价值观等等,竟然大多来自一些素未谋面的大龄作者、大V,或娱乐节目,或是同龄的好友,更或者是微博的狗血话题…很少有自己的观念在其中。

年纪轻轻的他们,拿着要么是那些比自己年长几十岁的”知名人士“,要么是同龄旁人的观点,并在这些观点的指引下,来度过自己美好的青春。并为年纪轻轻就取得真经而庆幸。实在让人扼腕叹息。生命在于尝试,在于探索,这些尝试的过程或许伴随着痛苦,但却是人生之美。如果自己不去探索,只是按旁人的指引活着,乐趣何在?如果一出生就让你知晓各种精辟的为人处世道理,那走这一遭的乐趣何在?小孩子缺少思考能力,所以常被一股脑的灌输很多东西。但是人年之后,还被一股脑的灌,这就不应该了。这也是中国式教育,从小不引导孩子主动思考,而是被动接受的方式有一定关系。

网络上现在很多的”教育课“,喜马拉雅、知乎live等等这些平台上(我不是广告君…),大量的提高情商系列,教做人系列课程,有很多看似很有名气(原谅我读书少没听过)的教授大V的爆款课程。带着期待和好奇的心情,我买了一部分,硬着头皮听完了大部分课程,最终得到的结论就是,这些课程几乎都是非常主观的东西,很少有客观的分析在。其实就是这些人对自己的做人做事方式美颜后的分享,作为参考,这自然是没问题的。但却常冠名以”必听的“,”经典的“的精品课程,受到很多青年人的追捧,把这些东西牢记在心作为自己的处事原则。如果这些讲主得知,很多人在按照自己的指引活着,估计会很有成就感吧。其实这些平台上有很多历史、经济、文学等等方面正儿八经的好课程,但相比学会做一个高情商、会说话的人,这些课显然吸引力不足。

读书是好事,信息量充沛也是好事,但在读主观性太强的书,或者在看、听类似的节目时,一定要谨慎这些教做人的东西。所谓读书启迪智慧,启迪只是是启发,是引子,是开头的工作,后续的主要工作需要自己完成。而不是觉得舒服就拿来用了。不要总按照别人看似正确的指引活着,考虑下真实的自我。

论还在成长中的青少年们,如何在这股国产泥石流中保持些许清醒🌚

Flutter组件合集

Element

Form

1、Input

  • TextField 最常用的文本输入组件。
    • 用户修改文本时,可通过Onchange获取最新的文本信息。
    • onSubmitted可获取到软键盘的确认按钮。
    • 默认下方有一个横线,decoration属性可以设置图标、padding等更多属性

2、Checkbox

  • CheckboxListTile
    下拉复选框,带有复选框的ListTile,带有标签的ListTile
    整个列表的图块是交互的,点击图块中的任意位置可切换复选框。

  • Checkbox

3、Button

  • FlatButton
    默认无边框,无背景色的按钮
    FlatButton.icon 为带图标的

  • RaiseButton
    凸起的按钮——带有shadow阴影的质感按钮
    RaiseButton.icon 为带图标的

  • IconButton
    纯图标按钮,无边框无背景色

  • PopupMenuButton
    弹出菜单栏的图标。
    PopupMenuButton 和 popupMenuItem 配合使用。
    选择菜单项时,触发onSelected方法。

  • FloatingActionButton
    默认的圆形悬浮按钮,每个页面最多一个。Scaffold.floatingActionButton

  • RawMateriaButton
    不使用当前Theme或ButtonTheme,高度自定义的Materail button

  • DropdownButton
    从项目列表进行选择的按钮。

  • OutlineButton
    RaisedButton 和 FlatButton的交叉,有边框。高程最初为0,背景透明。按下按钮时,背景变不透明,高程增加

4、Text

  • Text
    一行/多行文本显示
  • RichText

5、Radio

  • Radio
  • RadioListTile
    点击按钮附加的文字时,同样可以触发点击效果

6、Slider

  • Slider 范围选择滑块
  • SliderTheme 自定义Slider的样式
  • SliderThemeData SliderTheme的data修饰属性

7、Switch

  • Switch
    Switch、Switch.adaptive(ios风格)
    可自定义背景图、背景颜色等

  • SwitchListTile
    衍生组件,可以通过点击关联的文字标签切换switch

  • AnimatedSwitcher
    一个在新旧组件之间做渐变切换的组件,有一定的动画效果

Frame

1、Align
Alignment.center/centerLeft/bottomLeft(yx)

2、Stack

  • Stack
    绝对布局,依次叠加。

  • IndexedStack
    显示一个子项列表中的单个子项(只显示指定的一个子选项)

3、Layout

  • Row
  • Column
  • Container
    最小空间原则
  • Center

4、Box

  • ContainerBox
    添加额外的限制条件到child上。比如最大/最小高度/宽度

  • OverflowBox
    对子组件添加的约束,不同于子组件从其父组件获得的约束,允许child溢出父控件的空间

  • DecoratedBox
    在child绘制前或绘制后,添加额外的限制条件到child上,可以用来绘制一个圆形、方形、padding等。

  • FittedBox
    根据需求对child进行缩放、定位。contain/fill/cover/fitHeight/fitWidth

  • LimitedBox
    对最大宽高进行限制(前提是LimitedBox不受约束),即将child限制在指定的最大宽高中。

  • RoraredBox
    可以将子组件旋转整数的四分之一

  • SizedOverflowBox
    特定大小的窗口组件,将其约束传递给其子组件,可能会溢出。

  • UnconstrainedBox
    不限制子组件的大小,让子组件尽可能的扩展

5、Expanded

  • Expanded
    撑开flex布局子组件空间

6、Spacing

  • Padding
    内边距

  • SliverPadding
    列表缩进,应用于每个子元素

  • AnimatedPadding
    缩进变化时的动画

7、Table

  • Table

Media

1、Image

  • AssetImage
    从AssetBundle中获取图像

  • DecorationImage
    修饰Box的图片,一般配合BoxDecoration的img属性使用

  • ExactAssetImage
    带有scale属性的AssetImage

  • FadeInImage
    提供placeholder image到目标图片的过度效果

  • FileImage
    展示本地的文件,将给定的File对象解码为图片。Image.file是ImageFile的简写形式

  • NetworkImage
    网络图片

  • RawImage
    显示dart:ui.Image的组件,很少使用,推荐使用Image

  • MemoryImage
    将给定的Unit8List缓冲区解码为图像的组件

  • Image
    Image.asset 加载项目资源目录的图片,相对路径
    Image.network网络资源图片
    Image.file加载本地图片
    Image.memory 加载Unit8List资源图片

2、Icon

  • Icon

  • ImageIcon
    来自ImageProvider的图标,如AssetImage

  • IconTheme
    用于应用栏图标的言责、不透明度和大小

  • IconData

  • IconThemeData

3、Canvas

  • Canvas
    用于操作图形的界面
  • PainterSketch
    操作图形的界面
  • PainterPath
    在canvas上绘制的团
  • CircleProgressBarPainter
    操作图形的界面

Components

1、BottomNavigationBarItem
2、BottomNavigationbar
底部导航

List

1、AnimatedList
新增、删除选项时可以设置动画
2、ListView
最常用
3、ListBody
不常用,按照主轴方向排列子节点

Card

1、Card
卡片用于表示一些相关信息,例如相册,地理位置,用餐,联系方式等。

Bar

1、AppBar
应用栏——顶部导航显示的工具栏
2、BottomAppBar
底部应用栏

3、BottomNavigationBarItem
底部导航应用栏的子项
4、SnackBar
屏幕底部弹出的提示信息
5、Sliverbar
SliderApperBar可随内容滚动,一般在scroll滑动视图中。与NestedScrollView配合可实现上提到顶的悬停。
6、ScrollbarPainter
7、FlexibleSpaceBar
类似 Sliverbar, ”扩展和折叠的应用栏“,APPBar的一部风,可以扩展和折叠;
8、ButtonBar
末端对齐的按钮容器,向左向右看齐的按钮。
9、SnackBarAction
SnackBar,底部消息右侧有无可操作的行为
10、TabBar
实现并行界面的横向滑动展示

Dialog

1、AlertDoalog
弹出对话框,可自定义操作选项
2、Dialog
无可操作选项的弹出层
3、AboutDialog
通常用于展示信息
4、SimpleDialog
可以为用户提供多个选项选择,有一个可选的标题,显示在选项上方

Scaffold

1、Scaffold
基本的布局结构。Scaffold支持顶栏、侧边栏、底部导航栏等常用布局
2、ScaffoldState
通常用来控制SnackBars和BottomSheets和Drawer的显示和隐藏。

Grid

1、GridTile
GridList中的一种瓷片组合,包含header、body、footer三部分
2、GridView
常见的滚动列表,会占满给出的空间区域。
3、GridPaper
会在GridView的item上层浮现一层网格
4、SliverGrid
可以将多个item以每行两个的形式进行排列
5、GridTileBar
通常用来做GridTile的header与footer组件。

Scroll

1、ScrollView
滚动视图,属于抽象类,不能直接实例化。
ListView: 常用的ScrollView
PageView: 每个子widget都是视口窗口大小
GridView: 一个现实二维子widget的ScrollView
CustomScrollView: 自定义滚动效果的ScrollView
SingleChildScrollView: 只有一个子Widget的ScrollView
ScrollNotification、NotificationListener: 可以用于在不使用ScrollController的情况下查看滚动位置的widget

2、Scrollable
一个可以使内容滚动的Widget
3、ScrollbarPainter
用户绘制滚定条的CustomPainter。
4、ScrollMetrics
包含当前ViewPort及滚动位置等信息,抽象类,不可被实例化。
5、ScrollPhysics
确定滚动组件的物理属性
6、BoxScrollView
使用单个子布局模型的ScrollView
ListView、GridView、CustomScrollView
7、CustomScrollView
自定义滚动效果的ScrollView
8、NestedScrollView (⭐️)
可以嵌在另一个滚动视图中的ScrollView

Tab

1、Tab
Tab切换,如果同时传给Tab icon和text,text会展示在icon下面

1、CheckedPopupMenuItem
带有选中标记的弹出菜单,配合PopupMenuButton使用
2、DropdownMenuItem
DropdownButton创建的一个菜单项,配合DropdownButton使用
3、PopupMenuButton
一个提供菜单栏弹出对话框的按钮
4、PopupMenuDivider
菜单栏弹出对话框中每一项的水平线
5、PopupMenuEntry
弹出菜单的一个基类
6、PopupMenuItem

Pick

1、DayPicker
显示给定月份的日期,并可以选择一天
2、MonthPicker
选择一个月的可滚动月份列表,同上
3、YearPicker
同上
4、ShowdatePicker
日期选择器的对话框
5、CityPicker
显示中国的省市县区,通过函数showCityPicker调用,在容器窗口上弹出遮罩层

Chip

1、Chip
chip是表示属性,文本,实体或动作的元素。

2、ChipTheme
描述chip的颜色,形状和文本样式
3、ChipThemeData
保存chip主题的颜色,形状和文本样式
4、ChoiceChip
允许从一组选项中进行单一的选择
5、FilterChip
通过使用标签或者描述性词语来过滤内容
6、InputChip
输入型chip
7、RawChip

Panel

1、ExpansionPanelList
扩展面板,包含一个标题和一个正文,可以展开或者折叠。面板展开,主体可见。

Progress

1、LinearProgressIndicator
一个线性进度条
2、CircularProhressIndicator
循环进度条,旋转表示进度

Themes

Materail

1、MaterialApp
代表Material设计风格的应用
2、MaterailColor
颜色值
3、MaterailButton
4、MaterailPageRoute
页面跳转携带参数替换整个屏幕的页面路由
5、MaterailAccentColor
用来定义单一的强调色,以及四种色调的色系
6、MergeableMaterialItem

Cupertino(IOS style)

1、CupertinoApp
Cupertino苹果设计风格的应用,用于创IOS风格应用的顶层组件(Cupertino苹果电脑的全球总公司所在地,位于美国旧金山)。
包含了iOS应用程序通常需要的许多widget
2、CupertinoButton
iOS风格的button
3、CupertinoColors
iOS平台常用的颜色
4、CupertinoIcons
Cupertino图标的标识符
5、CupertinoNavigationBar
iOS风格的导航栏
6、CupertinoPageRoute
iOS风格全屏切换路由的滑动动画
7、CupertinoPageScaffold
实现单个iOS应用程序页的页面布局
8、CupertinoPicker
iOS风格的选择器
9、CupertinoPopupSurface
像iOS弹出式表面,快速实现一个圆角弹框
10、CupertinoScrollbar
iOS风格的滚动条
11、CupertinoSlider
iOS风格的Slider,选择连续性或非连续性的数据
12、CupertinoSegmentedControl

展示一些用户可以选择的选项
13、CupertinoSliverNavigationBar
iOS-11风格下拥有较大标题块的导航栏目
14、CupertinoSwitch
iOS风格的switch开关
15、CupertinoTabBar
iOS风格的底部导航栏
16、CupertinotabScaffold
iOS应用程序的选项卡的根布局结构
17、CupertinoTabView
具有自己的Navigator状态与历史记录的选项卡试图组件。
该组件有自己的路由体系,有自己的导航体系,并且他自身内部的导航系统并不与任何父母元素共享
18、CupertinoTimerPicker
iOS风格下的时间选择组件

Mweb使用腾讯COS作为图床

新入一盆小老弟同款熊爪,取名金风。

F9A1CBCE2ACD37E2D2591AD8613F9556


回归主题。接上上上上篇,使用腾讯COS替代七牛作为图床之后。平时使用Mweb作为Markdown写作软件,用着蛮舒服的。但是Mweb集成的图床是七牛的图床,腾讯COS的没有集成。每次发布文章前要把文章中的图片手动上传,然后复制链接。😂 当文章中的图片比较多的时候,我发现太扯淡了…所以想着整个上传的API。结果找了一圈没发现现成的轮子,只能自己造一个了。

思路比较简单,就是利用Mweb可配置自定义的图片上传功能。写一个腾讯COS上传图片的接口,然后配置到Mweb中即可。同时可配置CDN加速、图片处理规则。

github: https://github.com/flute/Mweb_tencet_cos

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* MWeb使用腾讯云存储COS作为图床
* 利用COS API,用nodejs作为server上传图片,添加至MWeb的配置中即可
* 2019-01-25 23:00
* author: ludis
* github: https://github.com/flute/Mweb_tencet_cos
* COS Doc: https://cloud.tencent.com/document/product/436/8629
*/

const express = require('express');
// form表单需要的中间件。
const mutipart = require('connect-multiparty');
// Tecent COS
const COS = require('cos-nodejs-sdk-v5');

const config = {
// 本地文件临时路径
temporaryUploadDir: './temporary',
// node服务启动端口
defaultPort: 3000,
// 到 控制台密钥管理 获取您的项目 SecretId 和 SecretKey。
// https://console.cloud.tencent.com/capi
secretId: 'AKIDsipmK32L..XXXXX..OuOtbSDFpair',
secretKey: 'VzSwGaimpHj..XXXXX..IoQAn1FMMSVcUz',
// 到 COS 对象存储控制台 创建存储桶,得到 Bucket(存储桶名称) 和 Region(地域名称)。
// https://console.cloud.tencent.com/cos4
bucket: 'ludis-1252396698',
region: 'ap-beijing',
// 文件上传后的路径前缀,如上传 test.png 后存储的目录为 ghost/flutter/test.png
preUrl: 'ghost/images/flutter',
// 文件上传后的域名
// 上传成功默认域名: ludis-1252396698.cos.ap-beijing.myqcloud.com
// 实用万象优图,处理图片
// 默认图片处理域名: ludis-1252396698.picbj.myqcloud.com
// 使用cdn加速域名: ludis-1252396698.image.myqcloud.com
domain: 'ludis-1252396698.image.myqcloud.com',
// 图片处理规则,没有可为空
rule: '!ghost'
}

// 使用永久密钥创建实例
const cos = new COS({
SecretId: config.secretId,
SecretKey: config.secretKey
});

const mutipartMiddeware = mutipart();
const app = express();

app.use(mutipart({
uploadDir: config.temporaryUploadDir
}));
app.set('port', process.env.PORT || config.defaultPort);
app.listen(app.get('port'), function () {
console.log(`Express started on http://localhost: ${app.get('port')} ; press Ctrl-C to terminate.`);
});

app.post('/upload', mutipartMiddeware, (req, res) => {
// 获取文件名、路劲(临时路劲)
const {
name,
path
} = req.files.file
// 上传
upload(name, path, url => {
if (url) res.json({
url: url
})
else res.send('upload failed!')
})
});

const upload = (name, path, callback) => {
// 分片上传
cos.sliceUploadFile({
Bucket: config.bucket,
Region: config.region,
Key: `${preUrl}/${name}`,
FilePath: path
}, function (err, data) {
console.log(err, data);
if (err) {
callback(null)
} else {
callback(`https://${config.domain}/${data.Key}${config.rule}`)
}
});
}

配置config中所需的各类信息后,启动node服务即可。

然后在Mwe偏好设置中进行配置:

配置完成后。上传图片至图床即可。

哦了,这样文章中的所有图片已经自动上传至腾讯COS,并返回正确的链接。舒服了🐣🐣🦉

Flutter学习之旅——实用入坑指南

开篇: 一如前端深似海,从此节操是路人从此再无安宁日,从此红尘是路人。要说技术更迭速度,还有比前端更快的么😂根本停不下来。这不,Google刚发布Flutter不到一年时间,1.0正式版发布不到两个月。阿里系的闲鱼老大哥,已经率先用Flutter重构了闲鱼,虽然没完全重构,但高频的重度页面都是Flutter的了。这一幕似曾相识,当初RN出来的时候不也是闲鱼团队先吃的螃蟹吗,在这里向闲鱼团队的老哥们致敬🐣。

既然老大哥都出动了,也侧面验证了这项技术的可行性。当小弟的也不能落后嘛,每天抽时间断断续续的学了两周时间,仿部分知乎的客户端,撸了一套客户端出来。前一周主要是熟悉Dart语言和常规的客户端布局方式,后一周主要是掌握使用HTTP的请求、下拉上拉、左滑右滑、长按等常用手势、相机调用、video播放等进阶用法。 两周下来,基本上可以开发80%以上常见的客户端需求。

前期一直在用simulator开发,略有卡顿,心中难免有些疑惑。结果最后release打包到手机后,竟然如丝般顺滑!!!简直喜出望外,完全可以睥睨原生开发,在这一点上的确要优于目前的RN。最重要的是作为Materail Design极简又有质感风格的狗血粉丝,Flutter造出来的界面简直倍爽。至此正式入坑Flutter开发。Google万岁!

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。

Beta1版本于2018年2月27日在2018 世界移动大会公布。
Beta2版本2018年3月6日发布。
1.0版本于2018年12月5日(北京时间)发布

这里把学习过程中一些常用高频的东西总结出来,基本能满足大多数情况下的开发需求。

完整的代码: https://github.com/flute/zhihu_flutter

欢迎加入Flutter开拓交流,群聊号码:236379502

Scaffold 主要的属性说明

  • appBar:显示在界面顶部的一个 AppBar
  • body:当前界面所显示的主要内容
  • floatingActionButton: 在 Material 中定义的一个功能按钮。
  • persistentFooterButtons:固定在下方显示的按钮。https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons
  • drawer:侧边栏控件
  • bottomNavigationBar:显示在底部的导航栏按钮栏。可以查看文档:Flutter学习之制作底部菜单导航
  • backgroundColor:背景颜色
  • resizeToAvoidBottomPadding: 控制界面内容 body 是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。

底部菜单 bottomNavigationBar,Tab栏切换 TabBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
TabController controller;

@override
void initState() {
super.initState();

// initialize the tab controller
// vsync ??
controller = new TabController(length: 5, vsync: this);
}

@override
void dispose() {
// dispose of tab controller
controller.dispose();
super.dispose();
}

...

body: new TabBarView(
children: <Widget>[new HomeTab(), new IdeaTab(), new ColleagueTab(), new MessageTab(), new MeTab()],
controller: controller,
),
bottomNavigationBar: new Material(
// background color of bottom navigation bar
color: Colors.white,
textStyle: new TextStyle(
color: Colors.black45
),
child: new TabBar(
unselectedLabelColor: Colors.black45,
labelColor: Colors.blue,
controller: controller,
tabs: <Tab>[
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.home, size: 25,),
Text('首页', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.access_alarm, size: 25,),
Text('想法', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.access_time, size: 25,),
Text('大学', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.account_balance_wallet, size: 25,),
Text('消息', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.adb, size: 25,),
Text('我的', style: TextStyle(fontSize: 10),)
],
),
),
),
],
),
),

顶栏自定义 appbar:title属性

顶部搜索栏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: searchBar(),
backgroundColor: Colors.white,
bottom: new Text('bottom'),
),
body: new Container()
);
}

/**
* 顶部搜索栏
*/
Widget searchBar() {
return new Container(
child: new Row(
children: <Widget>[
new Expanded(
child: new FlatButton.icon(
color:Color.fromRGBO(229, 229, 229, 1.0),
onPressed: (){
Navigator.of(context).push(new MaterialPageRoute(builder: (context){
return new SearchPage();
}));
},
icon: new Icon(
Icons.search,
color: Colors.black38,
size: 16.0,
),
label: new Text(
"诺奖得主为上课推迟发布会",
style: new TextStyle(color: Colors.black38)
),
),
),
new Container(
child: new FlatButton.icon(
onPressed: (){
Navigator.of(context).push(new MaterialPageRoute(builder: (context){
return new AskPage();
}));
},
icon: new Icon(
Icons.border_color,
color: Colors.blue,
size: 14.0
),
label: new Text(
'提问',
style: new TextStyle(color: Colors.blue),
),
),
)
],
),
);
}

图片圆角

1
2
3
4
5
6
7
8
9
10
11
Container(
margin: EdgeInsets.only(right: 5),
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
image: new NetworkImage(avatarUrl),
)
),
width: 30,
height: 30,
),

数组里动态添加组件

https://github.com/flutter/flutter/issues/3783

1
2
3
4
5
6
7
8
9
10
bool notNull(Object o) => o != null;
Widget build() {
return new Column(
children: <Widget>[
new Title(),
new Body(),
shouldShowFooter ? new Footer() : null
].where(notNull).toList(),
);
}

Text显示指定行数,超出后显示省略号

1
2
3
4
5
6
Text(
content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontSize: 14, color: Colors.black54),
),

margin 负值

https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return Container(
width: 40,
height:40,
// flutter中的margin没有负值的说法
// https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter
transform: Matrix4.translationValues(-20.0, 0.0, 0.0),
decoration: new BoxDecoration(
border: Border.all(width: 3, color: Colors.white),
color: Colors.black,
shape: BoxShape.circle,
image: new DecorationImage(
image: new NetworkImage('https://pic3.zhimg.com/50/d2af1b6b1_s.jpg')
)
),
);

图片自适应填满container

https://stackoverflow.com/questions/45745448/how-do-i-stretch-an-image-to-fit-the-whole-background-100-height-x-100-width

1
2
3
4
5
6
7
8
9
new Container(
height: 200,
decoration: new BoxDecoration(
image: new DecorationImage(
image: NetworkImage('https://pic3.zhimg.com/50/v2-f9fd4b13a46f2800a7049a5724e5969f_400x224.jpg'),
fit: BoxFit.fill
)
),
),

布局方式

justify-content: mainAxisAlignment
align-items: crossAxisAlignment

column 设置crossAxisAlignment: stretch后子元素宽度为100%,如果想让子元素宽度不为100%, 将其包裹在Row元素中即可。

flutter row and column

https://medium.com/jlouage/flutter-row-column-cheat-sheet-78c38d242041

捕捉点击事件

使用GestureDetector包裹widget即可。

1
2
3
4
5
6
7
child: new GestureDetector(
onTap: click,
child: Text(
name,
style: TextStyle(color: Colors.black87),
),
),

Dart 数组方法

https://codeburst.io/top-10-array-utility-methods-you-should-know-dart-feb2648ee3a2

PopupMenuButton 下拉弹窗菜单

https://stackoverflow.com/questions/43349013/how-to-open-a-popupmenubutton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DetailPage extends StatefulWidget {
@override
DetailPageState createState() => DetailPageState();
}

class DetailPageState extends State<DetailPage> {
final GlobalKey _menuKey = new GlobalKey();
....
....
....

child: new Row(
children: <Widget>[
new Container(
child: new GestureDetector(
onTap: () {
dynamic state = _menuKey.currentState;
state.showButtonMenu();
},
child: new Container(
child: new Text('默认排序'),
),
),
),
new PopupMenuButton(
icon: Icon(Icons.keyboard_arrow_down),
offset: Offset(0, 50),
key: _menuKey,
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
child: const Text('默认排序'), value: 'default'),
new PopupMenuItem<String>(
child: const Text('按时间排序'), value: 'timeline'),
],
onSelected: (_) {}
)

],
),

分割线

水平分割线 Divider
垂直分割线 VerticalDivider (无效???)

swiper

https://pub.dartlang.org/packages/flutter_swiper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:flutter_swiper/flutter_swiper.dart';

...

var images = [
'https://pic3.zhimg.com/v2-5806d9e33e36fa772c8da56c931bb416_b.jpg',
'https://pic1.zhimg.com/50/v2-f355ca177e011626938b479f0e2e3e03_hd.jpg',
'https://pic2.zhimg.com/v2-d8e47ed961b93b875ad814104016bdfd_b.jpg'
];

child: new Swiper(
itemBuilder: (BuildContext context,int index){
return new Image.network(images[index], fit: BoxFit.cover,);
},
itemCount: 3,
pagination: new SwiperPagination(),
//control: new SwiperControl(),
),

floatingActionButton 浮动button

https://proandroiddev.com/a-deep-dive-into-floatingactionbutton-in-flutter-bf95bee11627

floatingActionButton 配合 Scaffold 使用最佳

1
2
3
4
5
6
7
8
9
Scaffold(
floatingActionButton: new FloatingActionButton(
onPressed: (){},
child: Icon(Icons.edit),
//mini: true,
),
// 默认右下角,可设置位置。
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)

滑动视图

SingleChildScrollView

水平方向滑动 scrollDirection: Axis.horizontal

高斯模糊

https://stackoverflow.com/questions/43550853/how-do-i-do-the-frosted-glass-effect-in-flutter

1
2
3
4
5
6
import 'dart:ui';

new BackdropFilter(
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Text(desc, style: TextStyle(color: Colors.white),),
),

对话框弹窗

https://docs.flutter.io/flutter/material/AlertDialog-class.html

AlertDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void _showDialog(BuildContext context) {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: Text('Rewind and remember'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('You will never be satisfied.'),
Text('You\’re like me. I’m never satisfied.'),
],
),
),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

// 调用
....
onPressed: (){
_showDialog(context);
},
....

HTTP 请求、JSON编码解码

https://flutterchina.club/networking/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 加载库
import 'dart:convert';
import 'dart:io';

// 请求
try {
var request = await httpClient.getUrl(Uri.parse(url));
var response = await request.close();
if (response.statusCode == HttpStatus.OK) {
var json = await response.transform(UTF8.decoder).join();
var data = JSON.decode(json);
result = data['origin'];
} else {
result =
'Error getting IP address:\nHttp status ${response.statusCode}';
}
} catch (exception) {
result = 'Failed getting IP address';
}

// 保存返回的数据
// error: setState() called after dispose()

// If the widget was removed from the tree while the message was in flight,
// we want to discard the reply rather than calling setState to update our
// non-existent appearance.
if (!mounted) return;

setState(() {
_ipAddress = result;
});

时间控制:延时

1
2
3
4
5
6
7
8
9
10
11
import 'dart:async';
Future<Null> _onRefresh() {
Completer<Null> completer = new Completer<Null>();

new Timer(new Duration(seconds: 3), () {
print("timer complete");
completer.complete();
});

return completer.future;
}

下拉刷新 RefreshIndicator

1
2
3
4
5
6
7
8
9
new RefreshIndicator(
onRefresh: _onRefresh,
child: new SingleChildScrollView(
child: new Container(
padding: EdgeInsets.all(10),
child: new Text(_jsonData),
),
),
)

上拉加载更多

https://juejin.im/post/5b3abfc4518825622c14a6f1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ScrollController _controller = new ScrollController();

@override
void initState() {
super.initState();
_controller.addListener((){
if(_controller.position.pixels == _controller.position.maxScrollExtent) {
print('下拉加载');
_getMoreData();
}
});
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

...
scroll controller: _controller
...

flutter运行模式

https://www.jianshu.com/p/4db65478aaa3

flutter学习资源

http://flutter.link/

1
2
3
4
➜  zh git:(master) ✗ flutter clean
Deleting 'build/'.
➜ zh git:(master) ✗ rm -rf ios/Flutter/App.framework ios/Flutter/Flutter.framework
➜ zh git:(master) ✗ rm -rf /Users/ludis/Library/Developer/Xcode/DerivedData/Runner-

报错解决

1、在安卓真机release后 ios simulator无法编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Launching lib/main.dart on iPhone X in debug mode...
Xcode build done. 1.0s
Failed to build iOS app
Error output from Xcode build:

** BUILD FAILED **
Xcode's output:

=== BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug ===
diff: /Users/ludis/Desktop/opt/flutter/zh/ios/Pods/Manifest.lock: No such file or directory
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
Could not build the application for the simulator.
Error launching application on iPhone X.
Exited (sigterm)

解决

1
2
cd ios
pod install

常见问题: https://www.jianshu.com/p/bf3002de6a5e

Flutter scroll animation

https://medium.com/flutter-community/scrolling-animation-in-flutter-6a6718b8e34f

布局指南

在scrollView的滚动布局中,如果使用column组件,并为其添加Expanded扩展子组件的话,这两者会存在冲突。
如果坚持要使用此布局,在column设置mainAxisSize: MainAxisSize.min,同时子组件由Expanded改为Flexible即可。

表单、校验

https://www.cnblogs.com/pengshaomin/p/8945720.html

1、单行文本输入框 TextFormField

1
2
3
4
5
6
7
8
new TextFormField(
maxLength: 32,
onSaved: (val)=> this._config = val,
validator: (v)=>(v == null || v.isEmpty)?"请选择配置": null,
decoration: new InputDecoration(
labelText: '配置',
),
),

2、多行输入框 keyboardType: TextInputType.multiline,

1
2
3
4
5
new TextField(
keyboardType: TextInputType.multiline,
maxLines: 3,
maxLength: 100,
),

2、单选Radio

1
2
3
4
5
6
7
8
9
10
11
new Radio(
groupValue: this.radio,
activeColor: Colors.blue,
value: 'aaa',
onChanged: (String val) {
// val 与 value 的类型对应
this.setState(() {
this.radio = val; // aaa
});
},
),

3、复选 CheckBox

1
2
3
4
5
6
7
8
9
new Checkbox(
value: flutter,
activeColor: Colors.blue,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),

4、switch

1
2
3
4
5
6
7
8
9
new Switch(
activeColor: Colors.green,
value: flutter,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),

5、slider

1
2
3
4
5
6
7
8
9
10
new Slider(
value: _slider,
min: 0.0,
max: 100.0,
onChanged: (val) {
setState(() {
_slider = val;
});
},
),

6、DateTimePicker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 设置存储日期的变量
DateTime _dateTime = new DateTime.now();

// 显示文字Text,设置点击事件,点击后打开日期选择器
new GestureDetector(
onTap: (){
_showDatePicker();
},
child: new Container(
child: new Text(_dateTime.toLocal().toString()),
),
),

// 打开日期选择器
void _showDatePicker() {
_selectDate(context);
}

Future<Null> _selectDate(BuildContext context) async {
final DateTime _picked = await showDatePicker(
context: context,
initialDate: _dateTime,
firstDate: new DateTime(2016),
lastDate: new DateTime(2050)
);

if(_picked != null) {
print(_picked);
setState(() {
_dateTime = _picked;
});
}
}

7、TimePIcker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  TimeOfDay _time = new TimeOfDay.now();

// text显示当前时间
new GestureDetector(
onTap: _showTimePicker,
child: new Text(_time.format(context)),
),

// 显示timpicker
void _showTimePicker(){
_selectTime(context);
}

Future<Null> _selectTime(BuildContext context) async {
final TimeOfDay _picker = await showTimePicker(
context: context,
initialTime: _time,
);
if(_picker != null) {
print(_picker);
setState(() {
_time = _picker;
});
}
}

Toast/showSnackBar

showSnackBar:

https://material.io/design/components/snackbars.html#usage

1
2
3
4
5
6
7
8
9
10
11
12
void _showToast(BuildContext context) {
final scaffold = Scaffold.of(context);
scaffold.showSnackBar(
SnackBar(
content: const Text('Added to favorite'),
action: SnackBarAction(
label: 'UNDO',
onPressed: scaffold.hideCurrentSnackBar
),
),
);
}

Toast:

https://github.com/PonnamKarthik/FlutterToast

1
2
3
4
5
6
7
8
9
10
11
void _showToast(String title) {
Fluttertoast.showToast(
msg: title,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
textColor: Colors.white
);
}

Popover/popup

popup: CupertinoActionSheet

http://flatteredwithflutter.com/actionsheet-in-flutter/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
new MaterialButton(
onPressed: () {
_showActionSheet();
},
child: new Text('show ActionSheet', style: TextStyle(color: Colors.white),),
color: Colors.greenAccent,
),

void _showActionSheet() {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => actionSheet(),
).then((value) {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('You clicked $value'),
));
});
}

Widget actionSheet(){
return new CupertinoActionSheet(
title: new Text('title'),
message: const Text('your options are'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('yes'),
onPressed: (){
Navigator.pop(context, 'yes');
},
),
CupertinoActionSheetAction(
child: const Text('no'),
onPressed: (){
Navigator.pop(context, 'no');
},
)
],
cancelButton: CupertinoActionSheetAction(
child: new Text('cancel'),
onPressed: () {
Navigator.pop(context, 'Cancel');
},
),
);
}

IOS风格组件

https://flutter-es.io/widgets/cupertino/

Dismissible 滑动删除

https://flutter.io/docs/cookbook/gestures/dismissible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Dismissible(
// Each Dismissible must contain a Key. Keys allow Flutter to
// uniquely identify Widgets.
key: Key(item),
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});

// Then show a snackbar!
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$item dismissed")));
},
// Show a red background as the item is swiped away
background: Container(color: Colors.red),
child: ListTile(title: Text('$item')),
);

Swipe 左滑右滑删除

https://github.com/letsar/flutter_slidable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Widget _swipe(int i, String title, String desc) {
return new Slidable(
delegate: new SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
child: new Container(
color: Colors.white,
child: new GestureDetector(
onTap: (){},
onDoubleTap: (){},
onLongPress: (){},
child: new ListTile(
leading: new CircleAvatar(
backgroundColor: Colors.grey[200],
child: new Text(
'$i',
style: TextStyle(color: Colors.orange),
),
foregroundColor: Colors.white,
),
title: new Text(
'$title',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.black87, fontSize: 16),
),
subtitle: new Text(
'$desc',
style: TextStyle(color: Colors.blue[300]),
),
),
)
),
actions: <Widget>[
new IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
new IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
new IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
new IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
}

常用手势 GestureDetector

1
2
3
4
5
new GestureDetector(
onTap: (){_showToast('点击: $i');},
onDoubleTap: (){_showToast('连点: $i');},
onLongPress: (){_showToast('长按: $i');},
)

flutter 常用组件

https://github.com/flutter/plugins

camera / image_picker

https://medium.com/flutter-community/implementing-camera-feature-in-flutter-f7f6a7a5e6dd

image_picker: (最常用场景,从相册选择或拍照得到照片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
dynamic _picture;
dynamic _gallery;

new FlatButton.icon(
icon: Icon(Icons.camera),
label: Text('选择头像'),
onPressed: (){
_optionsDialogBox();
},
),

Future<void> _optionsDialogBox() {
return showDialog(context: context,
builder: (BuildContext context) {
return AlertDialog(
content: new SingleChildScrollView(
child: new ListBody(
children: <Widget>[
GestureDetector(
child: new Text('Take a picture'),
onTap: openCamera,
),
Padding(
padding: EdgeInsets.all(8.0),
),
GestureDetector(
child: new Text('Select from gallery'),
onTap: openGallery,
),
],
),
),
);
});
}

void openCamera() async {
Navigator.of(context).pop();
var picture = await ImagePicker.pickImage(
source: ImageSource.camera,
);

setState(() {
_picture = picture;
});
}
void openGallery() async {
Navigator.of(context).pop();
var gallery = await ImagePicker.pickImage(
source: ImageSource.gallery,
);
setState(() {
_gallery = gallery;
});
}




camera: (高阶用法,打开相机,实时获取相机流,可以定制拍照、录像等按钮。可用于相机扫码、实时识别、直播等场景)

https://pub.dartlang.org/packages/camera

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
camera: ^0.2.9

import 'package:camera/camera.dart';

class _CameraState extends State<CameraWidget> {
List<CameraDescription> cameras;
CameraController controller;
bool _isReady = false;

@override
void initState() {
super.initState();
_setupCameras();
}

Future<void> _setupCameras() async {
try {
// initialize cameras.
cameras = await availableCameras();
// initialize camera controllers.
controller = new CameraController(cameras[0], ResolutionPreset.medium);
await controller.initialize();
} on CameraException catch (_) {
// do something on error.
}
if (!isMounted) return;
setState(() {
_isReady = true;
});
}

Widget build(BuildContext context) {
if (!_isReady) return new Container();
return new Container(
height: 200,
child: AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(controller),
),
)
}
}

video player

https://github.com/flutter/plugins/tree/master/packages/video_player

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
video_player: ^0.8.0

import 'package:video_player/video_player.dart';

VideoPlayerController _controller;
bool _isPlaying = false;

@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://www.quirksmode.org/html5/videos/big_buck_bunny.mp4',
)
..addListener(() {
final bool isPlaying = _controller.value.isPlaying;
if (isPlaying != _isPlaying) {
setState(() {
_isPlaying = isPlaying;
});
}
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}

@override
void dispose() {
_controller?.dispose();
super.dispose();
}

// 显示、控制
_controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: new Container(
padding: EdgeInsets.all(10),
color: Colors.black,
child: VideoPlayer(_controller),

),
)
: Container(
child: new Text('视频加载中~'),
),
new FlatButton.icon(
label: Text('播放/暂停'),
icon: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
onPressed: _controller.value.isPlaying
? _controller.pause
: _controller.play,
)

AudioPlayer

https://github.com/rxlabz/audioplayer

Flutter的学习还在路上,列了个To Do List。继续加油: