Skip to content

18334581089/Contact_Flutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Jul 17, 2021
7723a6d · Jul 17, 2021
Apr 3, 2021
Jul 7, 2021
Apr 26, 2021
Jun 18, 2021
Mar 11, 2021
Jul 17, 2021
Jul 16, 2021
Jul 17, 2021
Jul 17, 2021
May 15, 2021
Mar 11, 2021
May 12, 2021
Mar 11, 2021
Jul 16, 2021
Jul 8, 2021
Jul 17, 2021
Jul 17, 2021
Jul 9, 2021
Jul 9, 2021
Jul 17, 2021

Repository files navigation

这是我的第一个flutter项目,我想通过项目实战来学习这一门技术,资料的地址(https://book.flutterchina.club/);

flutter_app_vscode

A new Flutter project.

Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

For help getting started with Flutter, view our online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.

3/11

(https://flutterchina.club/setup-windows/)

  • 下载安装flutter, android studio,
  • 配置插件
  • 在 android studio 和 vscode 分别使用flutter创建一个空项目

使用镜像:(添加用户环境变量,flutter官方为中国开启的临时镜像,写在电脑的:系统属性>高级>用户环境变量里面)

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

3/12

  • 简单学习dart语言

声明变量 函数 异步

  • 以上全部只是看了一遍

3/13

  • dart 官网 教程一个简单的dart程序

3/14

  • 小目标,先把dart的语法接着看一部份(生成一次commit)

3/15

  • (昨天没看,今天多看些!!!)
  • 异步

3/16

  • 看书
  • dart 和java 和 js

JavaScript无疑是动态化支持最好的脚本语言 Dart既能进行服务端脚本、APP开发、web开发

  • 开始flutter计数器(创建flutter项目时默认的一个组件)

看动代码

3/17

  • 计数器(看懂代码)

简单理解

  • 路由

3/18

  • 实现路由传参

目前还没完全看懂

3/19

  • yaml (的简单了解)

3/20

  • route完善
  • 路由传值

mainResolve 引入routerTestRoute widget, push 到 tipRoute (携带test参数) tip返回routerTestRoute 携带 参数 (如果点击tip的左上角返回按钮返回,则会返回null) 上面是非命名路由,

  • 命名路由

路由表:注册路由(起名字) Map<String, WidgetBuilder> routes map数据 key是string类型, 表示路由名称 widgetBuilder是路由的回调函数 注册路由表 MyApp类中添加routes属性

routes: {
  "new_page": (content) => NewRoute()
}

表示首页home 的路由 "/": (context) => MyHomePage(title: 'flutter Demo Home Page') 通过命名路由打开新页面 方法 Navigator.pushName Future pushNamed(BuildContext context, String routeName,{Object arguments}) 命名路由传参 通过settings对象注册参数

(context) {
  return TipRoute(text: ModalRoute.of(context).settings.arguments);
}

也可以通过settings对象获取参数(原本是) var args=ModalRoute.of(context).settings.arguments; 路由钩子 onGenerateRoute : MaterailApp的属性 ,和routes同级 在routes中没有注册,但被navigator.pushNamed调用时会触发

3/22

  • 接着包管理资料

pub仓库实例 添加 english_words 包 把english_words 添加到依赖包管理列表

  1. 修改yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  # 新添加的依赖
  english_words: ^3.1.3
  1. 控制台: flutter packages get

如果在 android studio 编辑器,可以单继yaml右上角的package get,获取最新依赖 如果在 vs,yaml修改后自动更新

  1. 页面引入

import 'package:english_words/english_words.dart';

  1. 页面中创建组件并使用

new WordPair.random().toString() 返回一个随机字符串

3/23

  • 依赖本地包
dependencies:
	pkg1:
        path: ../../code/pkg1
  • 依赖git(包位于Git存储库的根目录中)
dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git

3/24

  • assets

flutte 分为 code 和 assets 两部分

  • 通过yaml配置flutter assets
flutter: 
  assets:
    - assets/my_icon.png
    - assets/background.png
  • variant

asset 变体 flutter 的 assets 在构建过程中 会在相邻子目录中查找具有相同名称的任何文件 例入: 配置assets/background.png,构建时也会包含assets/dark/background.png

3/27

  • 目前看variant 貌似没什么用(将来有用)
  • 加载 assets

主流两种方法加载assets(文字和图片)

1 rootBundle 对象, 每一个flutter 都有一个rootBundle对象,可以获取主资源包. package:flutter/services.dart 当前方法会暴露一个rootBundle对象 2 DefaultAssetsBundle 对象, 通过它获取AssetsBundle 推荐第二种(好像是有响应关系的) 常见用法 在组件运行上下文中使用 DefaultAssetBundle.of()间接加载assets 在组件上下文之外使用rootBundle 加载图片 AssetImage 可以根据设备像素比例引入对应的图片尺寸 (满足上述条件: 目录配置倍数分辨率图片) 1

 new DecorationImage(
    image: new AssetImage('xxx.png'),
  ),

返回一个ImageProvider不是widget,所以用DecorationImage

2 Image.asset('xxx.png'),返回的是一个图片widget 其他  依赖包的资源 (给AssetImage配置package参数)

注意:包在使用本身的资源时也应该加上package参数来获取。 加载非flutter应用资源(上面的都是flutter启动后才能用)  举例: app图标,app启动图 设置APP图标 Android开发人员指南

  • 调试(总算该看点能看懂的了)

dart 分析器

flutter analyze, 一个静态代码检查工具, 测试你的代码, (intellij 会自动启用) 不知道vs 现在这个插件有没有analyze检测 dart Observatory 单步调试和分析器 需要配置 debugger() 声明 使用上面的调试工具,代码里面写这个就可以插入断点, 需要先引入import 'dart: developer'; 设置条件断点 debugger(when: offset > 30.0); 还有: print, debugPrint, flutter logs, 调试动画,调试性能, debugPrint: 把当前组件状态转存dump(debugDumpApp: 转存widget树的状态)

异常捕捉

捕捉之前先看dart的运行机制 ![dart运行]](https://book.flutterchina.club/assets/img/2-12.eb7484c9.png)

app执行 先执行microtask(微任务),在执行event (事件队列) (flutter 可以通过Future.microtask(…)向微任务中添加事件) 在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。 flutter 框架捕捉错误方法 try catch finally flutter 默认

catch (e, stack) {
  // 有异常时则弹出错误提示  
  built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}

自定义捕捉错误

FlutterError.onError = (FlutterErrorDetails details) {
  reportError(details);
};

flutter 框架不捕捉的错误处理(空对象异常,flutter执行异常)

同步 通过try 异步 runZoned 理解: 类似沙箱, 影响降低 下面是组件

3/28

周日了,加了一个学flutter的新手,已经关注 名字叫 toknowmore

3/29

  • 组件
  • widget

不是页面展示的组件, 是一个配置数据

1 屏幕显示的是 类 element. widget树生成element树(广义:widget树 是 ui树) 2 一个widget可以对应 个element widget 抽象类 继承DiagnosticableTree 用来 提供调试信息 key,性能优化(是否复用组件,canUpdate方法用来判断) createElement() statelessWidget 继承widget widget的构造函数参数应使用命名参数,命名参数中的必要参数要添加@required标注,这样有利于静态代码分析器进行检查 context : build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文 StatefulWidget 继承 widget 重写了父类的createElement()方法 添加新的方法,createState() statefullWidget对应一个state类 widget构建时可以同步获取 在widget生命周期中可以改变,并调用setState() 通知框架重新执行build 属性1widget:表示与该state绑定的widget,属性2context:同statelessWidget的context 写一个组件,研究state声明周期 CounterWidget

3/31

  • 复习state声明周期
  • widget获取state对象

通过context获取

  // 查找父级最近的Scaffold对应的ScaffoldState对象
  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
  //调用ScaffoldState的showSnackBar来弹出SnackBar
  _state.showSnackBar(
    SnackBar(
      content: Text("我是SnackBar"),
    ),
  );

通过of直接获取

// 直接通过of静态方法来获取ScaffoldState 
ScaffoldState _state=Scaffold.of(context); 

通过GlobalKey

//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();

3/32

  • 操作: 在widget中获取state

看不懂Builder 点击时,showSnackBar会弹出一个窗口显示文字 通过of

提供of方法表示表示可以通过of获取(scaffold组件默认提供了of) 没有提供of方法表示没有不可以返回父级state

  • flutter 内置组件库

Text, 带样式的文本 Row, Coulmn, 类似web中的flexbox布局 Stack, 取代线性布局: 类似与web中的绝对定位盒子(配合Positioned) Container, 矩形视觉元素: 可以装饰一个BoxDecoration(如背景,边框,阴影),也可以设置margin,padding

  • widget组件库(Material和Cupertino)

1 内部都引入flutter/widgets.dart,所以使用这俩组件时,不需在引入 2 不同域web开发,不需要担心引入两个组件库,导致安装包变大,dart只会编译你使用了哪些代码

4/2

  • 同react和vue 的状态管理

1 如果是用户数据,由父组件管理 (选中状态,滑块位置) 2 如果是外观数据,由当前组件管理 (颜色,动画) 3 如果出现不同组件使用同一数据, 由他俩的父元素管理

  • 简单组件 TapboxAState

自己管理自己状态的组件

4/3

  • 父管子组件 ParentWidget

组件名字都得是驼峰,首字母都得大写

  • 父子分别管组件 ParentWidgetC

flutter DouBan : 学习他的lib文件结构 创建文件夹划分模块(状态管理:statusManagement)

import 语法中的 package: + 项目名称 表示lib文件夹下 (项目名称在pubspec文件) 发现了: dart语言中的(),可以写属性 Container( child: new Text('...tip...'),) decoration,用来写样式的 , 赋值 用 new BoxDecoration

4/4

  • 先把状态组件放在一块

4/5

需要安装Android 4.1(API level 16)或更高版本的Android设备 在您的设备上启用 开发人员选项 和 USB调试 。详细说明可在Android文档中找到。 使用USB将手机插入电脑。如果您的设备出现提示,请授权您的计算机访问您的设备。 在终端中,运行 flutter devices 命令以验证Flutter识别您连接的Android设备。 运行启动您的应用程序 flutter run。

4/6

  • 看了一篇文,
  • 原生的还是原生的,但是学起来是有难度的,
  • flutter 相当于js的vue, 扩展,

  • 我本来对它很有期待,
  • 现在有一点怀疑了。

  • 会用对我来说应该更加重要

4/7

  • 我为了准备面试,学了目前能学到的所有东西

4/9 看文档

通过全局状态管理器, 处理相距较远的组件通信 1 全局事件总线, app组件的initState方法中订阅语言改变的事件 2 使用状态管理包, Provider,Redux

4/14

由于基础的git操作失误,错失了这几天的commit日志,虽然,没改啥. 基础还是有点差劲

4/16

ssi: 服务器端渲染, 可以选择reactssi框架,搭建项目

4/18

一个作业帮的小哥,93年,4年前端,20k以上,年轻,帅气,又是一个比我厉害的人 理论:每天早上一小时,写flutter或者react

4/19

  • 基础组件Text

4/20

  • 基础组件Text (2)

TextSpan

  • 文字算是看完了,最后的引入文字文件不是很理解,其他的应该问题不大

4/24

  • 基础组件 按钮
  • 昨天的字体引入有问题: 导致无法运行程序

字体先不管,提交了一个issue继续写按钮的

4/25

  • 图片和icon

ImageProvider 是一个抽象类, 就是定义了图片获取接口(load) image

 包括 AssetsImage: 定义从assets加载图片 NetworkImage: 定义从network加载图片 使用image的属性 SingleChildScrollView,这玩意可以实现滚动 EdgeInsets,SizedBox,BoxFit, 这些widget相当于见过了 对 child 使用 .map 和 toList 也是见过了(就是把数组里面的每一个部件进行过滤和加工处理) 类型判断A is B 注意: flutter 缓存图片数量最大1000,图片内存最大100m icon  查看所有的icons 使用map的时候的问题(baseWidgetImg的49行注释)

4/26

icon

使用meaterialIcons MainAxisAlignment 使用引入字体(ttf格式) 无法引入字体文件并使用(原因是因为,修改了pubspec文件需要重新启动项目,而且控制台已经报错了)

4/27

表单中switch和checkbox

Switch 和 Checkbox 都继承StatefulWidget,但是他们本身不会保存状态,父级来管理 No Material widget found Switch widgets require a Material widget ancestor使用switch需要再material的scaffold组件下使用 下一个,表单的控制焦点

4/29

表单: 空值焦点

FocusNode 和 FocusScopeNode来控制焦点 autofocus: true, 一个页面中同时两个默认获取焦点,第一个获取到 FocusNode.unfocus: 失去焦点 FocusScope.of(context) 来获取Widget树中默认的FocusScopeNode 主题色: 按钮和输入框的默认颜色就是主题色中的hintColor

4/30

继续按钮

昨天 的 Theme 是个组件...理解上还有点问题 theme 将主题应用于字类  1 描述排版, 颜色  2 全局theme,MaterialApp 创建(可以局部使用) 问题: 如何实现表单域的滚动效果 没有解决这个问题 往下 form

5/1 (劳动节)

  • form

Expanded, 类似于column,row,flex 用来展示多个组件集合的组件

封装Spacer组件(根据指定比例占位)

  Spacer(
    flex: 1,
  ),
class Spacer extends StatelessWidget {
  const Spacer({Key key, this.flex = 1})
    : assert(flex != null),
      assert(flex > 0),
      super(key: key);
  
  final int flex;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),
    );
  }
}

问题: 不知道为什么按钮沾满了个剩余高度. 理解使用 Builder: 使用builder的目的是改变当前context.(初步理解)

  • 进度指示器

5/2

  • 布局

flutter布局简介  根据是否包含字节点把widget分成了三类(布局类组件都是有子组件的)  LeafRenderObjectWidget,SingleChildRenderObjectWidget,MultiChildRenderObjectWidget 1 布局类组件就是直接或间接包含MultiChildRenderObjectWidget的widget 2 一般都有children用域接受widget 3 继承关系: widget > RenderObjectWidget > (上面三类) RenderObjectWidget 定义了创建更新 RenderObject的方法

  • 线性布局: row 和 column

5/3

  • 接着说线性布局

row , column 问题: 使用column嵌套子级 时会默认沾满高度,但是我的没有, 如果column嵌套column ,并且想让子级column高度占满column使用Expanded组件

  • 下面时弹性布局

flex , Expanded 因为Row和Column都继承自Flex Spacer创建一个可调整的空间隔,可用于调整Flex容器(如行或列)中窗口小部件之间的间距。(包装好的Expanded) 和示例有点差别

5/4

  • 流式布局(超出)

Wrap 除了超出显示范围 Wrap会折行以外,其他行为基本相同 runAlignment不知道效果 Flow 自定义布局和性能要求高的情况(性能好,灵活) 比wrap复杂,必须指定父组件大小.

5/5

  • 层叠布局

stack positioned 相当于position: relative和position: absolute

  • 对齐,align

FlutterLogo Alingnment 1 布局时: Alingnment 会以矩形的中心点作为坐标原点, 理解: 数值 和 静态常量得写法

Alignment(-1.0, -1.0) = Alignment.topLeft 表示左顶点, Alignment(1.0, -1.0) = Alignment.topRight 表示右顶点 2 坐标转换公式 布局 (Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)  上面得 childWidth childHeight 表示子元素宽高 上面两个式子算出来的时子元素实际偏移量(相对于左上角) FractionalOffset 这个和alignment一样都是用来定位的,区别在于: 他是用左上角来定位(和web一样) 坐标转换公式: (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

  • 对比(align和stack)
  1. 定位参考体系不同
  2. Stack可以有多个子元素
  3. (相当于web里面的 text-align和position 的区别)
  • Center组件

继承了Align 对齐方式确定(Alignment.center) DecoratedBox: 可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等

5/6

  • 容器类组件(竟然还挺快的)

padding 尺寸限制类组件

ConstrainedBox、SizedBox、UnconstrainedBox、AspectRatio

5/7

  • 容器组件

SizeBox 多个父元素尺寸限制 UnconstrainedBox

5/8

  • 装饰组件

DecoratedBox

decoration: BoxDecoration(
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
)

LinearGradient 用域定义线性渐变的类(还有其他定义渐变的类)

  • 变换组件

transForm

5/9

  • RotatendBox
  • Container

本身不具备RendeObject, 可以装饰变化限制 因为: 他集合了很多其他组件功能: decoratedBox,constrainedBox,trnasform,padding,align 设置宽高: constraints 或者 width/height, 后者优先 设置背景: color 和 decoration,(只能设置一个)

  • Flutter Gallery是Flutter官方提供的Flutter Demo,源码位于flutter源码中的examples目录下,笔者强烈建议用户将Flutter Gallery示例跑起来,它是一个很全面的Flutter示例应用,是非常好的参考Demo,也是笔者学习Flutter的第一手资料。

`Could not find a command named "channer". flutter多写一个t

Setting "enable-windows-desktop" value to "true".

You may need to restart any open editors for them to read new settings. 控制台报错: Unexpected child "deferred-components" found under "flutter".

5/10

  • github user 1: Jun Shi Yan
  • github user 1: 李泽鹏
@DhavalRKansara Your Flutter is on the stable channel. As mentioned previously, the gallery runs off the master channel, to which you can switch with

flutter channel master
flutter upgrade

5/11

  • gallery

无法执行 命令: flutter channel 列表或开关Flutter通道。 命令: flutter create . 创建一个新的Flutter项目。 命令: flutter upgrade 升级你的Flutter副本。 重新执行master 和 upgrade,执行flutter upgrade 自动执行一次flutter doctor,出现错误: Please install the "Desktop development with C++" workload, including all of its default components 下面是根据博客上的文章执行的: 安装visual studio (它和vscode的区分查看: https://www.zhihu.com/question/384334551)

  1. 打开网站下载
  2. 打开安装包,点确认
  3. 点击添加负荷
  4. 选中: "Desktop development with C++"
  5. 选中: Windows 10 SDK (10.0.17763.0)(Windows 10 SDK (10.0.17763.0) ,需要下载的是10.0.17763.0这个版本的)
  6. 选择下载路径,点击安装
  7. 执行flutter doctor

这个问题和昨天的问题有关联,具体内容查看(https://zhuanlan.zhihu.com/p/91686888)

5/12

  • gallery
  • 首先重新执行 flutter doctor

其他: 如何查看flutter 版本并切换1 查看flutter --version 2 flutter versioin 3 flutter versioin vxxx(版本号)

  • 然后执行 flutter channel stable
  • 然后执行 flutter upgrade

报错 unable to access 'https://github.com/flutter/flutter.git/' 应该是网络问题导致的,重新来一次(如果不行就配置代理,这个回头在学)

  • 然后to run the app on Windows:,执行: flutter config --enable-windows-desktop
  • 然后执行flutter create .

报错: 还是Unexpected child "deferred-components" found under "flutter"(也就是说,我通过下载安装上面c++那个软件并没有用 或者 我使用错误) 没有解决, 忽略直接run(还是报错) 晚上看 答案就放在那,我解决不了问题(问) 问群里

如果你的电脑没有在开发者模式,使用插件会出错。 你可以在设置-->更新和安全-->开发者选项里设置

Building with plugins requires symlink support. Please enable Developer Mode in your system settings
  • 重新 clone
  • 失败
  • 决定随缘,不写了

5/13

  • marterial

组件名称: 解释 AppBar: 一个导航栏骨架 MyDrawer: 抽屉菜单 BottomNavigationBar: 底部导航栏 FloatingActionButton: 漂浮按钮

(看react native 官网) (

  1. 文档的交互示例,组件类型(函数和class),提示
  2. 核心组件和原生组件(该看这个了)

)

5/14

  • Tab
Tab({
  Key key,
  this.text, // 菜单文本
  this.icon, // 菜单图标
  this.child, // 自定义组件样式
})

使用任何图片都需要在yaml文件先引入

5/15

  • 解决了浮动按钮,嵌到底部的问题
  • 裁剪
剪裁Widget	作用
  ClipOval  子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆
  ClipRRect  将子组件剪裁为圆角矩形
  ClipRect  剪裁子组件到实际占用的矩形大小(溢出部分剪裁)

'CustomClipper' 'clip'

5/15

  1. react 能干什么: 用js访问移动平台的api 实现外观和行为,通过react ui 组件
  2. 基本概念 a. 视图: react ui的最小组成部分, 相当于flutter的widget,或者html的标签 b. 原生组件:react native 编写的应用和原生的一样,实质是对系统原生组件的封装(react native会把组件自动转换为系统的原生组件)(系统的原生组件:android的Kotlin或java编写视图,ios的swift或objective-c编写视图)。 c. 核心组件:基础/常用的原生组件。
  3. 和react一样的api react components > react native components > core/native/community(第三方组件) 所以,接下来就是react的基础(组件,jsx,自定义组件,prop,state) 另外:jsx中传递一个 JS 对象值的时候,就必须用到两层括号:{{width: 200, height: 200}}。 另外:React.Fragment是抽象类,相当于小程序的block,写法<> </>``<React.Fragment key={item.id}></React.Fragment> 暂停,使用检视阅读先读一次 -检视阅读开始 1:跳过不过的 2:重点看主要的 a. 环境 :译注:请注意!!!国内用户必须必须必须有稳定的代理软件,否则在下载、安装、配置过程中会不断遭遇链接超时或断开,无法进行开发工作。某些代理软件可能只提供浏览器的代理功能,或只针对特定网站代理等等,请自行研究配置或更换其他软件。总之如果报错中出现有网址,那么 99% 就是无法正常连接网络。 全部看标题看了一遍,感觉,把这个看完,了解了react native 开发之前的所有理论知识,除了配置环境的时候可以实际操作一下,大部分都是需要自己理解的。不过,看完知道了这是一个大的领域,蛋蛋android和ios就是两个完全不同的世界。目前看来,会比flutter更好开发一些,但是我看的官网,所以理解起来可能不像实战书那么熟。决定还是先看文档。

5/18

  • 可滚动组件

默认超出会报错 可滚动组件直接或间接包含一个scrollable组件 scrollable

axisDirection属性:滚动方向 physics属性:决定响应用户操作方式,接受scrollPhysics类型对象(包括ClampingScrollPhysics,和,bouncingScrollPhysics) controller: 控制位置和监听事件,接受ScrollController类型对象 scrollbar 给可滚动组件加滚动条 会在ios平台自动切换成CupertinoScrollbar(ios滚动条风格) sliver 只构建出现在当前视口的子组件一种性能优化(视口: ViewProt,当前widget的实际显示区域) SingleChildScrollView不支持sliver

  • singleChildScrollView

  • react native: 入门组件

TextInput

注意 react 中的 onChange 对应的是 rn 中的 onChangeText 具有“动态状态”的最简单的组件 ScrollView 所有组件都会被渲染,不进行sliver,不适合长列表 再android和ios都有个子的区别 flatList 长列表: 立即渲染所有元素,而是优先渲染屏幕上可见的元素 secionList 长列表: 需要分组的数据

  • react native: 针对不同平台的处理

方法一: Platform模块 适用于平常代码中解决不同平台的少量代码冲突

Platform.OS返回ios或这android

import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
  height: Platform.OS === 'ios' ? 200 : 100
});

Platform.Select()直接返回设定的value值

import { Platform, StyleSheet } from 'react-native'
const styles = StyleSheet.create({
  container: {
    flex: 1,
    ...Platform.select({
      ios: {
        backgroundColor: 'red'
      },
      android: {
        backgroundColor: 'blue
      }
    })
  }
})

接受任何合法类型的参数,包括组件

Platform.Version, 返回android和ios的当前版本

在 Android 上,Version属性是一个数字 在 iOS 上,返回当前系统版本的字符串,比如可能是"10.3"。

方法二: 特定平台扩展名 适用于:不同平台代码放在不同的文件里时

目录下有俩文件

BigButton.ios.js
BigButton.andorid.js

引入时会根据不同平台引入对应的后缀

import BigButton from './BigButton';

明天就是环境搭建

5/19

23:00 在家没法运行flutter, 先把native看一遍(环境配置)

  • 明确环境
  1. 区分开发平台
  2. 区分目标平台(在哪个平台使用) 想从一个平台转移另一个平台,查看官方进行对应的环境搭建就好(部分区别)
  • 明确依赖
  1. node,JDK,android studio (android studio 是一个编译器,开发android应用的。开发时需要使用他提供的工具和环境) (node版本大于12) (JDK 是对java基础环境和相应开发平台标准和工具包的封装,安装版本必须是1.8,也称8版本)

不能使用淘宝镜像(cnpm)

(yarn是脸书替代npm的工具,可以加速node的下载,官方推荐使用yarn代替npm)

5/20

  • (工作上使用taro 又熟练了一波)
  • 开始搭建rn环境
  • 本机已经安装android studio,(然后跟着官网走)
  • 以下是Android 开发环境
  1. 安装 Android Studio(注意 安装 Android sdk, Android sdk platform, Android virtual device)
  2. 安装 Android SDK (找官网,再配置中找到并且安装指定版本)
  3. 配置 ANDROID_HOME 环境变量
  4. 把一些工具目录添加到环境变量 Path
  • 以下开始创建项目
  1. npx react-native init AwesomeTSProject --template react-native-template-typescript使用ts模板创建rn项目 (如果没有安装 react-native 会先安装这个)
  2. 接下来就是在 react native 的项目里面操作

5/24

  • 继续flutter组件学习
  • ListView
  • ListView 的 默认构造函数

指的是直接使用ListView({children: list}) 通过这样的方式 list 中的所有widget都会提前全部渲染,(也就是不支持sliver)

  • ListView 的 builder

滚动组件普遍规律: 构造函数构建的可滚动组件通常就是支持基于Sliver的懒加载模型的,反之则不支持

  • ListView 的 separated

separated和builder的区别就是,多了个separatorBuilder,根据条件控制列表的每一项 问题: 无法使用word_pair构造出组件,baseScroll4无法渲染

5/27

  • 上次报错原因: listTile必须包在material组件下

学习上拉组件

  1. initState

使用主题颜色借鉴: Theme.of(context).primaryColor 为什么叫做状态: 针对于组件,组件的当前的状态就是组件的state 不使用箭头函数的stateFulWidget的createState方法

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

找到了

initState() 方法是在创建 State 对象后要调用的第一个方法,常常用做于初始化一些数据 有点像钩子函数,类似的方法还有:didChangeDependencies() (initState() 方法执行完毕后执行的第二个方法) Widget: 部件, context: 当前Widget创建的element对象, state: element的状态

  1. 下次接着看吧(先看懂再说)

5/29

  • 只想更新应用依赖的包(pubspec.yaml 中的依赖)

flutter packages get 或者 flutter pub get

  • 如果想看命令行命令, 输入 xxx -h 自己就可以看到

实验 flutter run

执行后会有选择执行环境,然后会自动拉起设备 好处: 1 看到了命令

应该和vscode启动的顶部菜单是一致的功能 坏处: 好像vs code 并没有打开 app运行的状态(底部的黄条,以及 顶部的功能菜单) 坏处: 无法自动更新

  • f1

看到常用的flutter命令 输入launch emulator比较设备

  • 继续学习无限滚动组件

Divider

分割线组件 Future.delayed(Duration(seconds: 2)) 相当于web里面的setTimeout generateWordPairs  english_words中的一个方法  用来模拟数据 CircularProgressIndicator 之前写过,是一个圆形进度条.可以用来做加载中的 icon

  • 滚动组件 添加固定表头

使用文档中的内容,控制台会报错,原因不详(并且会打开一个文件)

可能原因: 外层嵌套了Column所以会报错,直接放在scaffold的body没有报错 应该是因为我复制错了代码,文档中也说会出错,需要设置 复制文档中的实例实现了顶部固定,底部滚动的效果 实现原理是,ExpandedColumn(解决了适配不同屏幕的效果) 也可以公国material中的sizeBox来对高度进行计算,保证高度适配(没有第一个方法好)

5/30

  • GridView

和listview大部分参数一样 唯一需要关注的参数gridDelegate(设置子组件如何排列)

值为: SliverGridDelegate类,flutter提供了两个子类, SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent GridView.count可以用来替换GridView+SliverGridDelegateWithFixedCrossAxisCount的情况 GridView.extent可以用来替换GridView+SliverGridDelegateWithMaxCrossAxisExtent的情况 GridView.builder用来显示异步的子项情况,或子项较多的时候

6/2

  • 证实,无法再Column中使用gridview
  • GridView.Builder的示例
  1. 接受两个参数

gridDelegateitemBuilder

  • CustomScrollView自定义滚动组件
  1. 滚动模型

Sliver版的可滚动组件,不包含滚动模型 非Sliver版的可滚动组件,包含滚动模型

  1. Material也是一个widget,可以作为根组件返回

属性 child

  1. CustomScrollView

slivers属性,设置一个数组 CustomScrollView的子组件必须都是Sliver。

SliverAppBar相当于AppBar,前者可以集成到CustomScrollView(实现头部伸缩的效果) SliverPaddingsliver 增加padding SliverGrid组件网格 SliverFixedExtentList 组件列表

6/3

  • 学习CustomScrollView组件
  1. Image.asset的 fit属性,指定图片在容器的分配方式,值为BoxFit

BoxFit 常见的有: scaleDown,contain,cover

  1. SliverGrid 构建一个网格组列表

属性gridDelegateGridDelegate一样 属性delegate 设置每一个子项的widget,参数(值为SliverChildBuilderDelegate)

  1. SliverFixedExtentList 构建一个列表(值为SliverChildBuilderDelegate)
  • scroll 监听
  1. ScrollController

offset可滚动组件当前位置 jumpTo``animateTo两个方法跳转指定位置

  1. ScrollController间接继承自Listenable

也就是说可以使用assListener方法 示例

为了避免内存泄露,需要调用_controller.dispose super.dispose 放在最后调用

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

6/5

  • 滚动页面位置恢复
  1. PageStorage

用来保存页面相关数据的组件 里面的widget 可以通过指定不同的pageStorageKey来存储各自的数据和状态

每次滚动结束,都会把offset存储在PageStorage中 重新创建时恢复offset 判断keepScrollOffset的值,有救使用,没有就用initialScrollOffset

  1. ScrollPosition

用来保存可滚动组件的滚动位置 animateTo() 和 jumpTo() 用来控制跳转位置的方法 执行过程

先会调用ScrollController (“注册位置”)可滚动组件会调用attach()方法 组件销毁时 会调用ScrollController的detach()方法 将其ScrollPosition对象从ScrollController的positions属性中移除

  1. NotificationListener

可以用来监听(类似冒泡)

scrollController的区别 controller 只能监听关联的组件,notification 可以让所有父级醉驾案监听  controller 只能获取当前滚动位置信息, notification 可以额外获取viewPort的一些信息 示例 学习示例内容 太困了,学了一半

6/6

  • ScrollNotification
  1. metrics 属性, 值为 SrollMetrics(包含当前ViewPort的滚动位置等信息)

 理解 ViewPort的含义,当前设备可视窗口信息 包括

pixels: 当前滚动位置, maxScrollExtent: 最大可滚动长度, extentBefore: 划出ViewPort的顶部的长度(划出屏幕上方的列表长度), extentInside: ViewPort内部长度(屏幕中显示的列表长度), extentAfter: 没有滑入ViewPort部分的长度(列表底部没显示区域的长度), atEdge: 是否滚动到了边界(顶部或者底部) 目前问题很多,一个滚动没有想到会有那么多不懂得地方,感觉有点复杂,现在调整一下心态.现在得主要目的是了解,识别.所以示例明白怎么写的就好,不看和官方文档以外得部分

  • 功能性组件
  • 导航返回拦截
  1. WillPopScope

使用场景: 防误触判断,eg:用户在某一个时间段内点击两次时,才会认为用户是要退出 属性

onWillPop: 回调函数,返回一个Futrue对象,如果futrue最终值是false,则不出栈(不返回) 示例 返回时有打印,但是没有返回上个页面,原因不详

6/7

开始早上来了,先学习,在游戏得习惯

  • 数据共享(类似stare,redux)
  1. 能实现组件跨级传递数据
  2. InheritedWidget 父组件传递子组件, Notification 子组件传递父组件
  3. didChangeDependencies 子组件的回调, 当父组件的属性发生变化,会调用这个函数
  4. 示例:

报错未处理(代码没有复制完)

6/8

接着把示例搞定

搞定

6/10

  • 看懂,跨级传递数据(类似props)
  1. 单独拿出来会报错?

自己没有写import

  1. RaisedButton按钮被废弃,使用ElevatedButton来代替
  2. ShareDataWidget extends InheritedWidget

父组件继承InheritedWidget 父组件必须定义updateShouldNotify决定是否更新状态 子组件要定义didChangeDependencies 子组件要定义 必须使用 父组件 中的共享数据 访问:ShareDataWidget.of(context)

  • 场景2: 父组件数据改变,触发子组件build, 不触发 didChangeDependencies
  1. 父组件 getElementForInheritedWidgetOfExactType

dependOnInheritedWidgetOfExactType注册了依赖关系,getElementForInheritedWidgetOfExactType不会

  • 重点预告

现在只要调用_InheritedWidgetTestRouteState的setState()方法,所有子节点都会被重新build,这很没必要 下一节我们将通过实现一个Provider Widget 来演示如何缓存,以及如何利用InheritedWidget 来实现Flutter全局状态共享。

6/11

  • 跨组件共享(Provider)
  1. 一般的原则是:如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理
  2. 使用全局事件总线EventBus(观察者模式)(类似 vue Bus)
enum Event{
  login,
  ... //省略其它事件
}
bus.emit(Event.login);
void onLoginChanged(e){
  //登录状态变化处理逻辑
}

@override
void initState() {
  //订阅登录状态改变事件
  bus.on(Event.login,onLogin);
  super.initState();
}

@override
void dispose() {
  //取消订阅
  bus.off(Event.login,onLogin);
  super.dispose();
}

(观察者模式)缺点: 必须显式定义各种事件,不好管理 订阅者必须需显式注册状态改变回调,也必须在组件销毁时手动去解绑回调以避免内存泄露。

  1. 利用InheritedWidget自动更新子组件特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可,Flutter社区著名的Provider包正是基于这个思想实现的一套跨组件状态共享解决方案
  • 示例
  1. 定义一个通用的InheritedProvider类,它继承自InheritedWidget

问题一:通知数据变化,通过 eventBus方式,来进行事件通知 通过flutter 的 ChangeNotifier,继承Listenable,也可以实现(发布订阅)

class ChangeNotifier implements Listenable {
  List listeners=[];
  @override
  void addListener(VoidCallback listener) {
     //添加监听器
     listeners.add(listener);
  }
  @override
  void removeListener(VoidCallback listener) {
    //移除监听器
    listeners.remove(listener);
  }
  
  void notifyListeners() {
    //通知所有监听器,触发监听器回调 
    listeners.forEach((item)=>item());
  }
}

通过调用addListener()removeListener()来添加、移除监听器(订阅者); 通过调用notifyListeners() 可以触发所有监听器回调。

  1. ChangeNotifierProvider 编译报错

6/12

  1. _typeOf 报错,原因不明,

重新看一次文档 除了 _typeOf其他的报错已经解决,还是因为没有理解他们之间的关系,直接自己写的有问题

  1. 使用上面的方法,实现 跨组件传递

示例 目的: 显示购物车中所有商品总价

1 组件 UnmodifiableListView 一种禁止修改的ListView,比如电商app购物车里面的物品是禁止修改的。 不能变更List的话,尽量使用unmodifiableListView有助提高编程习惯。 2 CartModel extends ChangeNotifier 3 Builder (第一次记录在 454行,这次通过老孟flutter进行理解)

Builder(
  builder: (BuildContext context){
    return Container();
  },
)

builder 可以更加解决scaffold的body获取不到 context的问题(扩展了context)

  1. 如果我们将ChangeNotifierProvider放在整个应用的Widget树的根上,

那么整个APP就可以共享购物车的数据了,这时ChangeNotifierProvider的优势将会非常明显

  1. 优化1

封装Consumer组件 解决1: 依赖CartModel很多时,这样的代码将很冗余 解决2: 语义将会很明确

使用Consumer会报错 报错原因没有找到 优化2 listen 可以实现 数据便哈按钮本身没有变化,不重新build的

6/14

  • 颜色和主题
  1. Color

色值转换和亮度 将rgba值#xxxxxx类型转换为 flutter 中的Color类

  1. 示例,导航变色

知识一,

创建一个类,构造函数中的对象可以添加组件调用时的参数(这个类似 react)

6/15

  • 回顾昨天
  1. Color 构造函数,传入8位十六进制直接使用
  2. Color 如果传入6位十六进制,透明度位00
  3. int.parse(_color2, radix: 16) 可以将颜色(字符串类型)转换为数字类型
  4. 0x 开头的数字表示16进制
  5. 修改透明度方法

0x00FFFFFF | 0xFF000000 把前两位替换成FF(透明度) Color.withAlpha 方法,传入十进制alpha数值(相当于修改透明度)

  1. color.computeLuminance() 方法返回当前颜色一个亮度值(0-1之间)
  • MaterialColor
  1. 实现颜色的类: 包含一种颜色的10个级别的渐变色
  2. 通过"[]"运算符的索引值来代表颜色的深度
  3. MaterialColor的默认值为索引等于500的颜色(Colors.blue 相当于 设置Colors.blue[500])
  • Theme
  1. 定义主题数据
  2. ThemeData 用于保存是Material 组件库的主题数据

子组件通过Theme.of方法来获取当前的ThemeData 有些是不能自定义的,如导航栏高度,ThemeData只包含了可自定义部分。

ThemeData({
  Brightness brightness, //深色还是浅色
  MaterialColor primarySwatch, //主题颜色样本,见下面介绍
  Color primaryColor, //主色,决定导航栏颜色
  Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
  Color cardColor, //卡片颜色
  Color dividerColor, //分割线颜色
  ButtonThemeData buttonTheme, //按钮主题
  Color cursorColor, //输入框光标颜色
  Color dialogBackgroundColor,//对话框背景颜色
  String fontFamily, //文字字体
  TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  IconThemeData iconTheme, // Icon的默认样式
  TargetPlatform platform, //指定平台,应用特定平台控件风格
  ...
})
  1. 路由换肤示例

示例完成 悬浮按钮没有实现变色

  1. 实例中实现了 不使用父组件的样式(第二行的黑色)
  • async
  1. FutureBuilder
FutureBuilder({
  this.future,
  this.initialData,
  @required this.builder,
})

future 以来的Future,通产是一个异步操作 initialData 初始数据,用户设置默认数据 builder Widget构建器 实例, 进入页面2秒后返回一个字符串显示 ConnectionState

一个枚举类,四个状态

none,// 当前没有异步任务,比如[FutureBuilder]的[future]为null时 waiting,// 异步任务处于等待状态。 active,// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态(active只在StreamBuilder中才会出现。) done,// 异步任务已经终止.

  1. StreamBuilder

接受多个异步操作结果 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等

6/15

  • 对话框
  1. AlertDialog
const AlertDialog({
  this.title, //对话框标题组件
  this.content, // 对话框内容组件
  this.actions, // 对话框操作按钮组
})
  1. showDialog()

返回一个Future值,(点击弹窗的按钮可以设置返回值)

Future<T> showDialog<T>({
  @required BuildContext context,
  bool barrierDismissible = true, //点击遮罩时是否关闭它
  WidgetBuilder builder, // 对话框UI的builder(AlertDialog)
})
  1. SimpleDialog

展示一个列表,用户选择

  1. Dialog

showDialogSimpleDialog 的父级 showDialogSimpleDialog 都使用了 IntrinsicWidth 来通过子组件的实际高度调整自身尺寸,所以无法延迟加载模型 Dialog可以实现延迟加载模型

不是特别理解,但是可以明白他俩的区别,dialog 更偏向于动态的多条弹窗数据 MySimple2

  1. showDialog 方法中的builder用来返回一个弹窗,上面已经说了三种

可以不返回上面三个,可以返回别的. 实例dialog2>UnconstrainedBox

6/16

  • 对话框打开动画和遮罩
  1. 前面说的都是material 的方法, flutter自己也有dialog方法

showGeneralDialog showDialog正是基于showGeneralDialog的封装 showDialog默认打开对话框是一个Fade的动画,我们可以自己定义showGeneralDialogshowCustomDialog方法,来设置动画

  1. 实例showCustomDialog

transitionBuilder 设置打开的动画

  • 弹窗管理状态
  1. 弹窗中需要通过状态动态显示数据,并且传给页面组件
setState(() {
  //更新复选框状态
  withTree = !withTree;
});

问题: context 不一样, 产生原因:

1 setState方法只会针对当前context的子树重新build,对话框不是再父组件中构建的是通过showDialog单独构建的 2 showDialog是通过路由创建的,修改父级的状态不会影响下一个路由 解决方法 1 单独抽离出StatefulWidget

单独封装一个checkbox 并且继承 StatefulWidget 组件,自己修改状态 2 使用StatefulBuilder方法 3 精妙的解法

  • 弹窗中管理状态
  1. 实例1, 错误写法

打开弹窗,点击选中,没有效果

  1. 实例2, 单独抽离checkbox

重新封装一层checkbox,context作为参数传进去

  1. 实例3, 使用StateBuilder方法

使用builder 方法简化了一下2方法

  1. 实例4, 最好的方法

setData调用时,调用_element.markNeedsBuild(),页面才会重构

markNeedsBuild 方法标记当前element为 dirty,由此实现重构 如果我们能标记为dirty,自然就能实现重构 context实际上就是Element对象的引用, 所以接下来

6/17

  • 其他对话框
  1. showModalBottomSheet

底部对话框 实例 5

  1. showBottomSheet

底部弹出整个页面的弹窗 实例 5 PersistentBottomSheetController 报错: 不知道怎么使用这个弹窗无法弹出内容,调用方法时控制台会报错

  1. Loading

通过showDialog+AlertDialog来自定义 实例 6 showDialog中已经给对话框设置了宽度限制,所以不能直接修改宽度

先 UnconstrainedBox 抵消宽度限制 再使用 SizedBox

  1. 日历弹窗

实例6

  • Pointer Event: 原始指针事件处理(触摸事件)
  1. 一次完整的事件分为三个阶段

手指按下、手指移动、和手指抬起(高级别的手势(如点击、双击、拖动等)都是基于这些原始事件的)

  1. 过程: 事件会在组件树中向上冒泡
  2. Flutter中没有机制取消或停止“冒泡”过程
  • Listener
  1. 监听原始触摸事件

使用示例 PointerDownEvent PointerMoveEvent PointerUpEvent 都是 PointerEvent 的子类 常用的属性有:

position:它是鼠标相对于当对于全局坐标的偏移。 delta:两次指针移动事件(PointerMoveEvent)的距离。 pressure:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。 orientation:指针移动方向,是一个角度值。

  1. Listener.behavior = HitTestBehavior.deferToChild,在命中测试期间如何表现

behavior , 值为HitTestBehavior,枚举 分别是

deferToChild 子组件会一个接一个的进行命中测试 opaque 将当前组件当成不透明处理 translucent 当点击组件透明区域时,可以对自身边界内及底部可视区域都进行命中测试

  1. 不想让某个子树响应PointerEvent

IgnorePointer和AbsorbPointer

Listener(
  child: AbsorbPointer(
    child: Listener(
      child: Container(
        color: Colors.red,
        width: 200.0,
        height: 100.0,
      ),
      onPointerDown: (event)=>print("in"),
    ),
  ),
  onPointerDown: (event)=>print("up"),
)

如果将AbsorbPointer换成IgnorePointer,那么两个都不会输出。

6/18

  • behavior

这个属性不是特别理解

  • 手势
  1. GestureDetector

实例1 点击,双击,长按

onTop 延迟200毫秒,如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。 实例2 拖动,滑动

6/19

  1. GestureRecognizer

是一个抽象类,一个手势的识别器对应一个GestureRecognizer的字类 gestureDetector 内部是使用一个或多个GestureRecognizer来识别各种手势的 实例

  1. GestureDetector 的 child 属性是一个widget组件,所以,如果不是widget组件,就无法使用GestureDetector来绑定手势事件,

GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,所以,可以用GestureRecognizer来尝试不是widget的情况  实例中使用了TextSpan的 recognizer属性,可以是一个 TapGestureRecognizer()

使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。

  • 手势竞争与冲突
  1. 场景: 同时监听水平和垂直方向,斜着拖动

取决于第一次移动时两个轴上的位移分量 实例 BothDirectionTestRoute

组件 CircleAvatar, Positioned 属性 onVerticalDragUpdate, onHorizontalDragUpdate

  1. 解决冲突

当 有一个widget 可以左右拖动,现在我们也想检测在它上面手指按下和抬起的事件 不能写在GestureDetector里面(会发现 并没有答应 down和up) 通过 Listener来见同(打印了down1和up1)

手势冲突只是手势级别的,而手势是对原始指针的语义化的识别, 所以在遇到复杂的冲突场景时,都可以通过Listener直接识别原始指针事件来解决冲突。

  • 番外读物
  1. 原生开发

程序稳定后的必走之路

优点 缺点
速度快、性能高、稳定性强、用户体验好 前期开发费用高
可以访问手机所有功能 开发效率偏低
支持大量图形和动画 后期维护繁琐
可离线使用 上线时间无法固定

跨平台的特点

优点 缺点
节省人力、开发成本 性能低于原生
节省开发时间 用户体验低于原生
多端的一致性 包体积变大
易上手 跨平台框架自身bug、缺陷
  1. Web App 有以下缺点,使得它始终是 “主角的心,配角的命”

a: 性能低,操作体验不好 b: 无法调用原生 API,很多功能无法实现, c: 依赖于网络,网速慢时体验很差,并且没有离线功能,优化不好的话会消耗流量 d: 只能做为一个临时的入口,用户留存率低

  1. Hybrid App 采用原生和 Web 开发 App(还可以采用 HTML5 + 原生)

理解: 主要是用js和原生技术相互调用,可以初步实现跨平台使用的效果 实现: Hybrid App 的原生 UI 组件用来展示交互复杂和渲染要求高的界面,其他的可以给 HTML5 来展示。 适用: 对于需要快速试错、快速占领市场的团队来说,Hybrid App 是一个不错的选择 相关技术: Hybrid 相关的技术有很多,比如 PhoneGap、Cordova、Ionic、VasSonic 等等

  1. 原生渲染

react native 理解: 写react的代码,可以再ios和android执行 实现: ios端: 在Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法, 当Objective-C 接收js传来的参数,调用对应的参数 特点:

优点 缺点
复用了 React 的思想,有利于前端开发者涉足移动端。 做不到 Write once, Run everywhere
能够利用 JavaScript 动态更新的特性,快速迭代。 不能做到完全屏蔽 iOS 端或 Android 的细节
相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好。 由于 Objective-C 与 JavaScript 之间切换存在固定的时间开销,所以性能必定不及原生

weex(阿里开发的轻量级的原生渲染解决方案,大公司的 KPI 产物) uniapp(小程序最早的开创者) 在2015年9月,DCloud推进微信团队开展小程序业务.微信团队经过分析,于2016年初决定上线小程序业务 理解: 在 App端内置了一个webview和一个基于 weex 改进的原生渲染引擎,提供了原生渲染能力。 用途: 在App端: a: 如果使用vue页面,则使用webview渲染 b: 如果使用nvue页面(native vue的缩写),则使用原生渲染

  1. 自渲染

flutter Google 开源的 UI 工具包.支持移动、Web、桌面和嵌入式平台 特点1: Dart 语言来避免 JsBridge引起的性能问题 特点2: Flutter不使用OEM Widgets(或DOM WebViews),它提供了自己的 Widgets。 特点3: 高效率,开发快(模拟器运行热重载) 特点3: 高度一致,ios和android样式基本一样

  1. 重点提醒:

不管选择何种框架,前提还得对原生的开发环境以及开发模式有一定的了解,不然也是事倍功半。 并不是所有公司都能长期承担起原生App开发与维护的成本

  • 事件总线
  1. 跨页面事件通知(例如: 登陆时登录和注销来进行一些状态更新)
  2. 实现一个简单的事件总线

示例: EventBus

  1. Dart中实现单例模式的标准做法就是使用static变量+工厂构造函数的方式,

就可以保证new EventBus()始终返回都是同一个实例,读者应该理解并掌握这种方法。 关于组件之间状态共享也有一些专门的包如redux、以及前面介绍过的Provider。

  • Notification
  1. 通知冒泡(Notification Bubbling)

每一个节点都可以分发通知,通知会沿着当前节点向上传递, 所有父节点都可以通过NotificationListener来监听通知 通知冒泡和触摸事件冒泡相似,但终止通知冒泡后,通知将不会再向上传递。 示例1: 滚动组件使用滚动通知

6/21

NotificationListener的泛型引入可以限制监听的事件范围 onNotification函数返回布尔值,返回true阻断冒泡,返回false继续向上冒泡

  1. 自定义通知

dispatch原理刨析: Notification.dispatch 可以发起冒泡通知 dispatch(context)中调用了当前context的visitAncestorElements方法,该方法会从当前Element开始向上遍历父级元素 visitAncestorElements方法有一个遍历回调参数visitAncestor,会判断每一个遍历到的父级Widget是否是NotificationListener,如果是则调用NotificationListener的_dispatch方法 执行onNotification方法

  • 动画
  1. 动画实现的原理

在一段时间内,快速地多次改变UI外观 超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑 超过32FPS,人眼基本上就感受不到差别了 理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的 为了方便开发者创建动画,不同的UI系统对动画都进行了一些抽象

  1. Animation

addListener(), 监听每一帧变化 addStatusListener(), 状态改变的监听器(开始、结束、正向或反向)

  1. CurvedAnimation可以通过包装AnimationController和Curve生成一个新的动画对象

常见的Curves

Curves 描述
linear 匀速的
decelerate 匀减速
ease 开始加速,后面减速
easeIn 开始慢,后面快
easeOut 开始快,后面慢
easeInOut 开始慢,然后加速,最后再减速

可以自定义 Curves

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

6/22

  • AnimationController
  1. 控制动画

forward(), stop(), reverse() // 表示启动,停止和反向 派生自Animation,因此可以在需要Animation对象的任何地方使用 创建一个controller对象

final AnimationController controller = new AnimationController( 
 duration: const Duration(milliseconds: 2000), 
 lowerBound: 10.0,
 upperBound: 20.0,
 vsync: this
);
  1. Ticker

其中,vsync是一个TickerProvider对象,用来创建Ticker Ticker就是通过SchedulerBinding来添加屏幕刷新回调 锁屏时,SchedulerBinding(每次屏幕刷新的回调)不会执行,所以不会执行ticker

  1. Tween

定义从输入范围到输出范围的映射

  • 动画实现
  1. 简单实现

实例1 (线性放大) 实例1 (增加 Curve) 实例2 (AnimatedWidget简化)(AnimatedWidget类封装了调用setState()的细节,并允许我们将widget分离出来) 实例2 AnimatedBuilder 正是将渲染逻辑分离出来, 实例2 实现循环: 动画正向执行结束时反转动画

6/23

  • 自定义 路由切换 时的动画
  1. MaterialPageRoute

它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换 CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件,它实现的就是左右滑动切换。 PageRouteBuilder来实现切换动画

  1. 以渐隐渐入动画来实现路由过渡

实例 PageRouteBuilder pageBuilder 有一个属性animation,所以可以实现自定义动画过渡 PageRouteBuilder其实只是PageRoute的一个包装,我们可以直接继承PageRoute类来实现自定义路由 实例 PageRouteBuilder > FadeRoute 实现: 在打开新路由时应用动画,而在返回时不使用动画 实例 PageRouteBuilder > FadeRoute > (修改部分)

  • Hero动画(英雄 动画)
  1. 指的是可以在两个页面(路由)之前都存在的动画

实现: Hero组件将要共享的widget包装起来,并提供一个相同的tag

6/24

InkWell 组件 组件在用户点击时出现“水波纹”效果

  • 交织动画(Stagger Animation)
  1. 由一个动画序列或重叠的动画组成
  2. 注意以下几点:

要创建交织动画,需要使用多个动画对象(Animation)。 一个AnimationController控制所有的动画对象。 给每一个动画对象指定时间间隔(Interval)

  1. 实例: 柱状图

dart中extends、 implements、with的用法与区别 继承(关键字 extends) 混入 mixins (关键字 with) 接口实现(关键字 implements) 有前后顺序: extens在前,mixins在中间,implements最后; extends 规则 子类会继承父类里面可见的属性和方法 但是不会继承构造函数 子类能复写父类的方法 getter和setter 子类重写超类的方法,要用@override 子类调用超类的方法,要用super 子类可以继承父类的非私有变量 mixins 规则 作为mixins的类只能继承自Object,不能继承其他类 作为mixins的类不能有构造函数 一个类可以mixins多个mixins类 mixins绝不是继承,也不是接口,而是一种全新的特性 implements 接口实现 (没有interface的,但是Flutter中的每个类都是一个隐式的接口,Flutter中:class 就是 interface)

  • AnimatedSwitcher
  1. 通常在切换(Tab切换、路由切换)时都会指定一个动画,以使切换过程显得平滑

Flutter SDK组件库中已经提供了一些常用的切换组件,如PageView、TabView等, 但是,这些组件并不能覆盖全部的需求场景

  1. AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画

实例 计数器

  1. 实现原理
Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    // child没变化,...
  } else {
    //child发生了变化,构建一个Stack来分别给新旧child执行动画
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //旧child应用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child应用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 给旧child执行反向退场动画
    _controllerOldAnimation.reverse();
    //给新child执行正向入场动画
    _controllerNewAnimation.forward();
  }
}

//build方法
Widget build(BuildContext context){
  return _widget;
}

类似海有: AnimatedCrossFade

  1. 高级用法

实例: MySlideTransition2 无法实现实例

6/25

实例完成,因为没有写child元素

记录错误:

The relevant error-causing widget was
AnimatedSwitcher
lib\animation\MySlideTransition.dart:49

应该是因为没有用child

  • 自己封装一个切换动画(任意方向)

实例: SlideTransitionX

  • 动画过渡组件
  1. Widget属性发生变化时会执行过渡动画的组件
  2. 特征: 就是它会在内部自管理AnimationController

所以: 自己封装一个AnimationController可以大大提升 过渡动画的易用性 实例: AnimatedDecoratedBox1: decoration属性发生变化时执行一个过渡动画 ImplicitlyAnimatedWidget类, 用来封装动画

  1. Flutter预置的动画过渡组件

实例: AnimatedWidgetsTest

6/27

  • 自定义组件
  1. 场景

flutter提供组件无法满足需求 为了共享代码,封装一些公用组件

  1. 创建方式

通过组合其他组件 自绘 实现 RenderObject

  1. 组合其他组件

适用: 自定义组件最简单的方法,优先考虑 例如: Container就是一个组合组件(由: DecoratedBox,ConstrainedBox,Transform,Padding,Align组成) 思想: 开发就是组合提供的组件实现不同布局

  1. 自绘

适用: 无法通过现有组件实现需要的ui 例如: 实现一个圆形渐变的进度条 局限: CircularProgressIndicator的valueColor只支持执行旋转动画时变化Indicator的颜色 实现: 通过Flutter中提供的CustomPaint和Canvas来实现UI自绘。

  1. RenderObject

RenderObject是用来渲染文本和图片的,RenderObject.paint抽象方法 paint方法第一个参数表示上下文(PaintingContext), PaintingContext.canvas 就是主要的绘制逻辑 区别于自绘(CustomPaint和Canvas),自绘只是为了方便开发者封装的一个代理类

组合 自绘/RenderObject(通过Canvas)
简单 强大灵活,理论上可以实现任何外观的UI
容易 必须了解Canvas API细节,并且得自己去实现绘制逻辑
  • 自定义组件组合
  1. 自定义渐变按钮

通过组合DecoratedBox和InkWell来实现 实例: GradientButton 理解代码

  1. 抽离出单独的组件注意: 代码规范

如: 必要参数要用@required 标注 如: 可选参数在特定场景需要判空或设置默认值 如: 错误地使用组件时能够兼容或报错提示 如: 使用assert断言函数 如: 组件更新时是否需要同步状态

  1. 增强TurnBox组件

功能: 1,任意角度来旋转其子节点;2,过渡动画;3,手动指定动画速度 实例: TurnBox

  1. 富文本展示组件

实例: MyRichText 注意: 组件更新时是否需要同步状态 RichText TextSpan: 需要设置属性,不设置无法显示文字

6/28

  • 自定义组件-自绘
  1. 几乎所有的UI系统都会提供一个自绘UI的接口, Canvas,开发者可以根据api绘制各种图形

flutter 提供了CustomPaint 结合画笔 CustomPainer 绘制图片

  1. CustomPaint

如果CustomPaint有子节点 将子节点包裹在RepaintBoundary组件 RepaintBoundary 子组件的绘制将独立于父组件的绘制

CustomPaint(
  size: Size(300, 300), //指定画布大小
  painter: MyPainter(),
  child: RepaintBoundary(child:...)), 
)
  1. CustomPainter void paint(Canvas canvas, Size size);

Canvas:一个画布,包括各种绘制方法

1 1
API名称 功能
drawLine 画线
drawPoint 画点
drawPath 画路径
drawImage 画图像
drawRect 画矩形
drawCircle 画圆
drawOval 画椭圆
drawArc 画圆弧

Size 绘制区域大小

  1. 画笔Paint
var paint = Paint() //创建一个画笔并配置其属性
  ..isAntiAlias = true //是否抗锯齿
  ..style = PaintingStyle.fill //画笔样式:填充
  ..color=Color(0x77cdb175);//画笔颜色
  1. 性能

绘制是比较昂贵的操作 a: 利用好shouldRepaint返回值

如果绘制依赖外部状态,改变则应返回true来重绘,反之相反 实例: CustomPaintRoute 实例: 圆形背景渐变进度条

6/29

  • 文件操作
  1. 都是通过Dart IO库来操作文件的

IO库包含了文件读写的相关类,它属于Dart语法标准的一部分 Dart VM下的脚本还是Flutter,都是通过io库来进行操作的

  1. 访问app目录

PathProvider 这个插件提供一种平台透明(不分平台)访问设备常用位置 支持访问的位置有:

缓存(临时目录)
getTemporaryDirectory()获取临时目录
iOS上,这对应于NSTemporaryDirectory() 返回的值;
Android上,这是getCacheDir() 返回的值
文档目录
etApplicationDocumentsDirectory()来获取应用程序的文档目录
只有自己可以访问的文件
只有当应用程序被卸载时,系统才会清除该目录
在iOS上,这对应于NSDocumentDirectory
Android上,这是AppData目录。
外部存储目录
getExternalStorageDirectory()来获取外部存储目录
eg: sd卡,(ios不支持)

6/30

  • 昨天文件操作的补充

dart io库的操作非常丰富,这里只是讲一些前端最基本的部分,具体自己了解

  • HttpClient
  1. HttpClient发起请求分为五步

第一步: HttpClient httpClient = new HttpClient(); 第二步: HttpClientRequest request = await httpClient.getUrl(uri); 包含Query参数:

Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
  "xx":"xx",
  "yy":"dd"
});

设置请求header: request.headers.add("user-agent", "test"); 携带请求体方法:

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); //可以直接添加输入流

第三步: HttpClientResponse response = await request.close(); 返回对象: 返回一个HttpClientResponse对象,它包含响应头(header)和响应流(响应体的Stream) 第四步: 读取内容 String responseBody = await response.transform(utf8.decoder).join(); 第五步: httpClient.close(); 关闭client后,通过该client发起的所有请求都会中止。

  1. 实例: HttpTestRoute

没有看效果

  1. 常见配置参数
属性 含义
idleTimeout 对应请求头中的keep-alive字段值,为了避免频繁建立连接,httpClient在请求结束后会保持连接一段时间,超过这个阈值后才会关闭连接。
connectionTimeout 和服务器建立连接的超时,如果超过这个值则会抛出SocketException异常。
maxConnectionsPerHost 同一个host,同时允许建立连接的最大数量。
autoUncompress 对应请求头中的Content-Encoding,如果设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip"
userAgent 对应请求头中的User-Agent字段。

通过HttpClient设置的对整个httpClient都生效,而通过HttpClientRequest设置的只对当前请求生效

  1. 其他

证书校验其实就是提供一个badCertificateCallback回调

String PEM="XXXXX";//可以从文件读取
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
  if(cert.pem==PEM){
    return true; //证书一致,则允许发送数据
  }
  return false;
};

findProxy 代理

client.findProxy = (uri) {
  // 如果需要过滤uri,可以手动判断
  return "PROXY 192.168.1.2:8888";
};

如果不需要代理,返回"DIRECT"即可。 · APP开发中,很多时候我们需要抓包来调试 · 抓包软件(如charles)就是一个代理 · 可以将请求发送到我们的抓包软件,我们就可以在抓包软件中看到请求的数据了

HTTP请求认证 · 用于保护非公开资源 · 如果Http服务器开启了认证,那么用户在发起请求时就需要携带用户凭据 · 如果你在浏览器中访问了启用Basic认证的资源时,浏览就会弹出一个登录框 Basic认证的基本过程 · 客户端发送http请求给服务器,服务器验证该用户是否已经登录验证过了 · 客户端得到响应码后,将用户名和密码进行base64编码(格式为用户名:密码),设置请求头Authorization,继续访问服务器验证用户凭据,如果通过就返回资源内容 · Flutter的HttpClient只支持Basic和Digest两种认证方式(前者只是简单的通过Base64编码(可逆),而后者会进行哈希运算相对来说安全一点点) · 之外还有:Digest认证、Client认证、Form Based认证等 · 安全起见都应该在Https协议下 Http认证的方法和属性 · addCredentials: 添加用户凭据

httpClient.addCredentials(_uri, "admin", new HttpClientBasicCredentials("username","password"), ); · authenticate: 当服务器需要用户凭据且该用户凭据未被添加时,httpClient会调用此回调,一般这个回调会调用addCredential()来动态添加用户凭证

httpClient.authenticate=(Uri url, String scheme, String realm) async{
  if(url.host=="xx.com" && realm=="admin"){
    httpClient.addCredentials(url,
      "admin",
      new HttpClientBasicCredentials("username","pwd"), 
    );
    return true;
  }
  return false;
};

· addCredentials()来添加全局凭证

7/1

  • Dio http库
  1. Dart社区第三方http请求库

直接使用HttpClient发起网络请求是比较麻烦 涉及到文件上传/下载、Cookie管理等就会非常繁琐

  1. 功能

支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等

  1. 配置
dependencies:
  dio: ^x.x.x #请使用pub上的最新版本

引入dio 的代码要放在dependencies二级

7/2

  • Http分块下载
  1. 实例 downloadWithChunks
  2. 概念

HTTP分块下载,也就是断点续传下载, 根据HTTP1.1协议(RFC2616)中定义的HTTP头Range和Content-Range字段来控制的: 客户端在HTTP请求头里面指明Range,即开始下载位置 服务端在HTTP响应头里面返回Content-Range,告知下载其实点和范围

  1. 好处

将文件分为若干个块,然后维护一个下载状态文件用以记录每一个块的状态, 这样即使在网络中断后,也可以恢复中断前的状态

  1. 实际注意

分块大小多少合适? 下载到一半的块如何处理? 要不要维护一个任务队列?

7/4

  • WebSockets
  1. 客户端与服务端实时通信而产生的技术

websocket.org提供的测试服务器 1: 连接到WebSocket服务器 web_socket_channel 提供了 WebSocketChannel 可以监听来自服务器的消息,又可以将消息发送到服务器的方法 2: 监听来自服务器的消息

new StreamBuilder(
  stream: widget.channel.stream,
  builder: (context, snapshot) {
    return new Text(snapshot.hasData ? '${snapshot.data}' : '');
  },
);

3:将数据发送到服务器 channel.sink.add('Hello!'); 4:关闭WebSocket连接 channel.sink.close(); 报错1 Target of URI doesn't exist 表示有包没有配置的pubspec.yaml中

报错2 Insecure HTTP is not allowed by platform android端 AndroidManifest.xml 文件中修改为

<uses-permission android:name="android.permission.INTERNET" />
<application
  android:label="flutter_app_vscode"
  android:usesCleartextTraffic="true"
  android:icon="@mipmap/ic_launcher">
  1. 扩展

1: 之前介绍的Http协议和WebSocket协议都属于应用层协议 就是说: 上面说到的http和websocket都是直接使用框架封装好的 2: 应用层协议的实现都是通过Socket API来实现的 3: 类似的应用层协议还有很多如:SMTP、FTP等 4: 高级编程语言中的Socket库其实都是对操作系统的socket API的一个封装 5: 如果我们需要 情况一: 自定义协议或者想直接来控制管理网络链接 情况二: 我们觉得自带的HttpClient不好用想重新实现一个 就需要使用Socket

  1. 实例: 简单实现()
_request() async{
  //建立连接
  var socket=await Socket.connect("baidu.com", 80);
  //根据http协议,发送请求头
  socket.writeln("GET / HTTP/1.1");
  socket.writeln("Host:baidu.com");
  socket.writeln("Connection:close");
  socket.writeln();
  await socket.flush(); //发送
  //读取返回内容
  _response =await socket.transform(utf8.decoder).join();
  await socket.close();
}
  • Json转Dart Model类
  1. dart:convert中内置的JSON解码器json.decode() 来实现
//一个JSON格式的用户列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
//将JSON字符串转为Dart对象(此处是List)
List items=json.decode(jsonStr);
//输出第一个用户的姓名
print(items[0]["name"]);
  1. 问题

由于json.decode()仅返回一个Map<String, dynamic>, 这意味着直到运行时我们才知道值的类型(类型安全、自动补全和最重要的编译时异常) 实例 dartModel

报错1 Unexpected character 原因可能是字符串格式有问题(这次是因为我多加了一个"") 3. 解决

“Json Model化” 通过预定义一些与Json结构对应的Model类 在请求到数据后再动态根据数据创建出Model类的实例 帖子上看,说就是一个虚拟类

  1. 实践

通过引入一个简单的模型类(Model class)- User 包括: User.fromJson构造函数,用来从一个map构造出一个 User实例 map structure 包括: toJson 方法,将 User 实例转化为一个map.

  1. 官方的 son_serializable package

自动化的源代码生成器, 为我们自动处理JSON序列化, 生成JSON序列化模板 实例: dartModel2 报错 *** Target of URL hasn't been generated: 'user.g.dart'*** 这些错误是完全正常的,这是因为Model类的生成代码还不存在。 为了解决这个问题,我们必须运行代码生成器来为我们生成序列化模板。 有两种运行代码生成器的方法: 1: 一次性生成 flutter packages pub run build_runner build 一个好的建议是将所有Model类放在一个单独的目录下,然后在该目录下执行命令。 2: 持续生成 'watcher' flutter packages pub run build_runner watch 在项目根目录下运行来启动_watcher_ 只需启动一次观察器,然后它就会在后台运行,这是安全的。

  1. 根据json生成模板
  1. template.dart 模板的模板
  2. mo.dart (脚本)它可以根据指定的JSON目录,遍历生成模板 如果JSON文件名以下划线“_”开始,则忽略此JSON文件。 复杂的JSON对象往往会出现嵌套,我们可以通过特殊标志来手动指定嵌套的对象
  3. mo.sh (shell)将生成模板和生成model串起来
  4. 至此,脚本写好了
  5. 使用: 在根目录下新建一个json目录,然后把user.json移进去, 然后在lib目录下创建一个models目录,用于保存最终生成的Model类。 现在我们只需要一句命令即可生成Model类了:./mo.sh

7/5

  • 开发Package
  1. 通过package可以创建共享的模块化代码
  2. Package包括

1: pubspec.yaml 声明了Package的名称、版本、作者等的元数据文件 2: lib 文件夹 公开的(public)代码,最少应有一个dart文件

  1. Package分类

1: Dart包, 包含Flutter的特定功能,对Flutter框架具有依赖性,这种包仅用于Flutter 2: 插件包, 插件包括原生代码 包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现 Flutter的Dart和Dart VM(组件集合)是不同

  • 开发
  1. 创建dart 包

1: Android Studio:File>New>New Flutter Project 创建一个Package工程 2: --template=package 来执行 flutter create 命令来创建 两个方法都会生成 lib/hello.dart:Package的Dart代码 test/hello_test.dart:Package的单元测试代码。 (hello是包的的名称,会根据创建时的名称自动生成)

  1. 实现package

1: 纯Dart包,只需在主lib/.dart文件内或lib目录中的文件中添加功能即可 。 2: 要测试软件包,请在test目录中添加unit tests

  1. 导入包 import 'package:utilities/utilities.dart';
  2. 生成文档

使用https://github.com/dart-lang/dartdoc#dartdoc,为包生成文档. 开发者需要做的就是遵守文档注释语法在代码中添加文档注释,最后使用dartdoc可以直接生成API文档(一个静态网站) 文档注释是使用三斜线"

  1. 处理包的相互依赖

需要将该依赖包添加到pubspec.yaml文件的dependencies部分

dependencies:
  url_launcher: ^0.4.2

使用 import 'package:url_launcher/url_launcher.dart' android android/build.gradle

android {
    // lines skipped
    dependencies {
        provided rootProject.findProject(":url_launcher")
    }
}

在android/src中使用 import io.flutter.plugins.urllauncher.UrlLauncherPlugin iOS ios/hello.podspec

Pod::Spec.new do |s|
  # lines skipped
  s.dependency 'url_launcher'

在ios/Classes中使用 #import "UrlLauncherPlugin.h"

  1. 冲突

假设我们想在我们的hello包中使用some_package和other_package 并且这两个包都依赖url_launcher,但是依赖的是url_launcher的不同的版本 避免这种情况的最好方法是在指定依赖关系时,程序包作者使用版本范围 (opens new window)而不是特定版本

  1. 发布Package

发布之前,检查pubspec.yaml、README.md以及CHANGELOG.md文件,以确保其内容的完整性和正确性 查看是否准备ok flutter packages pub publish --dry-run 发布 flutter packages pub publish 代理 export all_proxy=socks5://127.0.0.1:1080

#### 7/5

  • 插件开发: 平台通道简介
  1. “平台特定”或“特定平台”

平台指的就是Flutter应用程序运行的平台 完整的Flutter应用程序实际上包括原生代码和Flutter代码两部分

  1. platform channel

Flutter中提供了的一个平台通道 1: Flutter APP和原生平台进行通信 2: 调用平台能力,如: 蓝牙、相机、GPS等 3: lutter本身只是一个UI系统,它本身是无法提供一些系统能力 4: 是Flutter插件的底层基础设施 5: 灵活的系统(无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用)

  1. 消息传递方式

1: 通过platform channel将消息发送到其宿主应用(原生应用) 2: 宿主监听平台通道,并接收该消息. 3: 然后调用该平台的API,并将响应(如果有数据是异步的)发送回客户端(应用程序的Flutter部分)

  1. MethodChannel

MethodChannel API 可以发送与方法调用相对应的消息 在宿主平台上(android 和 ios) 可以接收方法调用并返回结果 属于自定义编解码器,类似的还有BasicMessageChannel

  1. 获取平台信息

defaultTargetPlatform 用来获取平台信息 是一个枚举 使用 if(defaultTargetPlatform==TargetPlatform.android){ // 是安卓系统,do something } 其他用法 假如: 想让我们的APP在所有平台都表现一致 比如: 比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格 可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台

debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform); 

上述代码会输出TargetPlatform.iOS defaultTargetPlatform的值也会变为TargetPlatform.iOS

  • 开发Flutter插件
  1. 介绍

获取电池电量的插件 我们在Dart中通过getBatteryLevel 调用Android BatteryManager API和iOS device.batteryLevel API

1: 创建一个新的应用程序项目(之前讲的步骤) 2: 首先,我们构建通道 单个应用中使用的所有通道名称必须是唯一的; 我们建议在通道名称前加一个唯一的“域名前缀” 例如samples.flutter.io/battery main的state类中加入 static const platform = const MethodChannel('samples.flutter.io/battery'); 接下来,我们调用通道上的方法,指定通过字符串标识符调用方法getBatteryLevel。 该调用可能失败(平台不支持平台API,例如在模拟器中运行时), 我们将invokeMethod调用包装在try-catch语句中。

7/6

  • 看懂开发flutter插件
  1. 创建flutter应用
  2. 创建Flutter平台客户端

MethodChannel _getBatteryLevel RaisedButton

  1. Android端API实现 复制官方代码无法启动项目
  • 原生和Flutter之间如何共享图像的方法TexturePlatformView
  1. 以及如何在Flutter中嵌套原生组件
  2. flutter局限

他的平台通道,消息传送不能覆盖所有的应用场景 摄像头拍照录视频(如果把图像每一帧都传递到flutter应用,代价非常大:内存和CPU的巨大消耗) Flutter提供了一种基于Texture的图片数据共享机制。

  1. Texture 是一个gpu内存将要绘制的图像数据对象

Flutter engine会将Texture的数据在内存中直接进行映射(而无需在原生和Flutter之间再进行数据传递) Flutter会给每一个Texture分配一个id,同时Flutter中提供了一个Texture组件 Texture 组件正是通过textureId与Texture数据关联起来 整个流程 1: 图像数据先在原生部分缓存 2: Flutter部分再通过textureId和缓存关联 3: 绘制由Flutter完成

  1. 如果我们开发插件

textureId完全可以通过MethodChannel来传递。

  1. 注意

原生摄像头捕获的图像发生变化时,Texture 组件会自动重绘,这不需要我们写任何Dart 代码去控制。

  • Texture用法
  1. Flutter官方提供的相机camera插件和视频播放video_player插件都是使用Texture来实现的,它们本身就是Texture非常好的示例
  2. camera包自带的一个示例

1: 可以拍照,也可以拍视频,拍摄完成后可以保存;排号的视频可以播放预览。 2: 可以切换摄像头(前置摄像头、后置摄像头、其它) 3: 可以显示已经拍摄内容的预览图。

  1. 看一下camera具体代码

1: 依赖camera插件的最新版

pubspec.yaml 
  camera: ^0.5.2+2

2: 在main方法中获取可用摄像头列表。

void main() async {
  // 获取可用摄像头列表,cameras为全局变量
  cameras = await availableCameras();
  runApp(MyApp());
}

3: 构建UI 4: 完整代码(camera.dart)

  • PlatformView (平台组件)

1: 开发过程中需要使用一个原生组件 例如: webview 2: 将需要使用原生组件的页面全部用原生实现,在flutter中需要打开该页面时通过消息通道打开这个原生的页面 3: Flutter SDK中新增了AndroidView和UIKitView 两个组件, 这两个组件的主要功能就是将原生的Android组件和iOS组件嵌入到Flutter的组件树中,能让Flutter共享原生组件 4: 由于AndroidView和UIKitView 是和具体平台相关的,所以称它们为PlatformView 5: 使用Platform View 以Flutter官方提供的webview_flutter插件为例 1: 原生代码中注册要被Flutter嵌入的组件工厂

public static void registerWith(Registrar registrar) {
   registrar.platformViewRegistry().registerViewFactory("webview", 
   WebViewFactory(registrar.messenger()));
}

2: 在Flutter中使用 打开flutter中文首页

class PlatformViewRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: "https://flutterchina.club",
      javascriptMode: JavascriptMode.unrestricted,
    );
  }
}

注意 用PlatformView的开销是非常大的,因此,如果一个原生组件用Flutter实现的难度不大时,我们应该首选Flutter实现。

  • 多语言
  1. Flutter SDK已经提供了一些组件和类来帮助我们实现国际化,下面我们来介绍一下Flutter中实现国际化的步骤

1: 下面举例MaterialApp类为入口的应用来说明如何支持国际化 2: 大多数应用程序都是通过MaterialApp为入口,MaterialApp实际上也是WidgetsApp的一个包装 3: 本地化的值和资源指我们针对不同语言准备的不同资源,资源一般是指文案(字符串) 4: 默认情况,仅提供美国英语本地化资源 要添加对其他语言的支持,应用程序须添加一个名为flutter_localizations的包依赖 还需要在MaterialApp中进行一些配置

  1. 使用flutter_localizations

1: 添加依赖

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

2: 下载flutter_localizations 3: 然后指定MaterialApp的localizationsDelegatessupportedLocales,

import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // 本地化的代理类
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // 美国英语
    const Locale('zh', 'CN'), // 中文简体
    //其它Locales
  ],
  // ...
)

1: localizationsDelegates 列表中的元素是生成本地化值集合的工厂。 2: GlobalMaterialLocalizations.delegate 为Material 组件库提供的本地化的字符串和其他值,它可以使Material 组件支持多语言。 3: GlobalWidgetsLocalizations.delegate 定义组件默认的文本方向,从左到右或从右到左,这是因为有些语言的阅读习惯并不是从左到右,比如如阿拉伯语就是从右向左的。 4: supportedLocales 也接收一个Locale数组,表示我们的应用支持的语言列表,在本例中我们的应用只支持美国英语和中文简体两种语言

  1. 获取当前区域Locale

Locale用来标识用户的语言环境的,它包括语言和国家两个标志如

const Locale('zh', 'CN')

获取应用的当前区域Locale

Locale myLocale = Localizations.localeOf(context); 4. 监听系统语言切换

切换语言这个过程是隐式完成的 可以通过localeResolutionCallbacklocaleListResolutionCallback回调来监听locale改变的事件 localeResolutionCallback的回调函数签名:

Locale Function(Locale locale, Iterable<Locale> supportedLocales)

1: 参数locale的值为当前的当前的系统语言设置 2: supportedLocales 为当前应用支持的locale列表,是开发者在MaterialApp中通过supportedLocales属性注册的。 3: 返回值是一个Locale,此Locale为Flutter APP最终使用的Locale。通常在不支持的语言区域时返回一个默认的Locale。

使用localeListResolutionCallback方法 前者接收的是一个Locale列表,而后者接收的是单个Locale

Locale Function(List<Locale> locales, Iterable<Locale> supportedLocales)

  1. Localization 组件

1: 前面提到的Localizations组件用于加载和查找应用当前语言下的本地化值或资源 2: 通过Localizations.of(context,type) (opens new window)来引用这些对象 3: 如果设备的Locale区域设置发生更改,则Localizations 组件会自动加载新区域的Locale值,然后重新build使用(依赖)了它们的组件, 4: 大型应用程序中,不同模块或Package可能会与自己的本地化值捆绑在一起 5: Localizations.of()表达式会经常使用,所以MaterialLocalizations类提供了一个便捷方法

static MaterialLocalizations of(BuildContext context) {
  return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}

// 可以直接调用便捷方法
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
  1. flutter软件包中仅提供美国英语值的MaterialLocalizationsWidgetsLocalizations接口的实现

7/7

  1. 昨天的camera报错,
import 'package:camera/camera.dart';
List<CameraDescription> cameras;
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  cameras = await availableCameras();
  runApp(MyApp());
}

重新添加依赖和 把mian文件修改成上面的代码没有报错了 参考的cameras博客

camera有报错

pubspec 引入

video_player: ^0.10.11+1
path_provider: ^1.6.9

接下来尝试打开 电脑打开失败 MissingPluginException (MissingPluginException(No implementation found for method availableCameras on channel plugins.flutter.io/camera)) 尝试手机打开 没有设备 注释了首页的 // cameras = await availableCameras(); 可以打开

  1. 尝试国际化

没有想到使用的实际场景

  • 实现Localizations
  1. GlobalMaterialLocalizationsGlobalWidgetsLocalizations 只是Material组件库的本地化实现,

想要让自己的布局支持多语言,那么就需要实现在即的Localizations

  1. 开始

1: 实现 LocalizationsLocalizations类 实现提供了本地化值 示例 Localizations 2: 实现 DelegateDelegate类 在Locale改变时加载新的Locale资源 Delegate类需要继承自LocalizationsDelegate类 它有一个load方法 示例 Delegate shouldReload 方法返回值决定,build时,是否调用load方法重新加载Locale资源 一版返回false就好 而且,每当Locale改变时Flutter都会再调用load方法加载新的Locale 3: 添加多语言支持 先注册DemoLocalizationsDelegate类 在MaterialApp或WidgetsApp的localizationsDelegates列表中添加 Delegate实例即可完成注册 再通过DemoLocalizations.of(context)来动态获取当前Locale文本 接下来我们可以在Widget中使用Locale值 示例 (在main文件中添加)

return Scaffold(
  appBar: AppBar(
    //使用Locale title  
    title: Text(DemoLocalizations.of(context).title),
  ),
  ... //省略无关代码
)

当在美国英语和中文简体之间切换系统语言时,APP的标题会自动切换

  1. 问题

1: 如果我们要支持的语言不是两种而是8种甚至20几种时 如果为每个文本属性都要分别去判断到底是哪种Locale从而获取相应语言的文本 将会是一件非常复杂的事 2: 通常情况下翻译人员并不是开发人员 能否可以将翻译单独保存为一个arb文件交由翻译人员去翻译 翻译好之后开发人员再通过工具将arb文件转为代码

  1. 可以通过Dart intl包来实现这些

7/8

  • Intl 包
  1. 干嘛的

实现国际化 ,把字符串分离成单独的文件(方便开发和翻译人员) 1: 从代码中提取要国际化的字符串到单独的arb文件 2: 根据arb文件生成对应语言的dart代码

  1. 依赖
dependencies:
  intl: ^0.15.7 
dev_dependencies:
  intl_translation: ^0.17.2  

intl包主要是引用和加载intl_translation生成后的dart代码

  1. 使用

1: 根目录创建一个l10n-arb目录(用来保存intl_translation命令生成的 arb文件) arb文件内容示例

{
  "@@last_modified": "2018-12-10T15:46:20.897228",
  "@@locale":"zh_CH",
  "title": "Flutter应用",
  "@title": {
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {}
  }
}

示例中@@locale的值表示为中文 示例中title对应中文简体翻译 示例中@title对title的一些描述信息 2: 根目录创建l10n目录(保存从arb文件生成的dart代码文件) 3: 实现Localizations和Delegate类 示例 localization_intl.dart 4: 添加需要国际化的属性 DemoLocalizations类中添加需要国际化的属性或方法 这时我们就要用到Intl库提供的一些方法 示例 一个电子邮件列表页,我们需要在顶部显示未读邮件的数量 在未读数量不同事,我们展示的文本不同

未读邮件数 提示语
0 There are no emails left
1 There is 1 email left
n(n>1) There are n emails left

实现方法: Intl.plural(...) // Intl 包还有一些其他的方法,这里只是示例

remainingEmailsMessage(int howMany) => Intl.plural(
  howMany,
  zero: 'There are no emails left',
  one: 'There is $howMany email left',
  other: 'There are $howMany emails left',
  name: "remainingEmailsMessage",
  args: [howMany],
  desc: "How many emails remain after archiving.",
  examples: const {'howMany': 42, 'userName': 'Fred'},
);

5: 生成arb文件 可以通intl_translation包的工具来提取代码中的字符串到一个arb文件 执行命令 flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb \ lib/l10n/localization_intl.dart 报错: The pubspec.yaml file has changed since the pubspec.lock file was generated, please run "pub get" again. a: 执行 'flutter pub get' 报错 Because flutter_app_vscode depends on flutter_localizations any from sdk which depends on intl 0.17.0, intl 0.17.0 is required. So, because flutter_app_vscode depends on intl ^0.15.7, version solving failed. 修改pubspec文件的intl版本为17以上, 重新执行 还是提示pub get 报错

Because intl_translation >=0.17.7 depends on intl >=0.15.3 <0.17.0 and intl_translation >=0.17.0 <0.17.7 depends on intl ^0.15.3, intl_translation >=0.17.0 requires intl >=0.15.3 <0.17.0.

解决问题失败,回到之前**intl ^0.15.7,**问题 参考 博客 使用 dependency_overrides暂时解决 接下来继续 (上面文字部分是在7/9日早上写的) 7/10早上继续 执行pub get 警告

you are using these overrideden dependencies

博客说不用太在意 执行

flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb \ lib/l10n/localization_intl.dart

报错 The getter 'elements2' isn't defined for the class 'ListLiteral' 博客上没答案,应该是版本问题 结束

  • Intl 的总结
  1. 第二步和第一步只在第一次需要,开发的主要工作在第三步
  2. 最后两步命令,可以放在shell脚本里面(完成第三步或者完成arb文件翻译后执行)

创建intl.sh 文件 执行chmod +x intl.sh(chmod +x xxx.sh: 表示为xxx文件增加可执行权限) 然后就可以执行./intl.sh

  • 国际化的常见问题
  1. 默认的Locale不是中文简体:

非大陆行货渠道买的一些Android和iOS设备,会出现的情况 为了防止设备获取的Locale与实际的地区不一致 app都必须提供一个手动选择语言的入口

  1. 对应用标题进行国际化

MaterialApp有一个title属性来指定APP的标题 问题在于: 无法在构建MaterialApp时通过Localizations.of来获取本地化资源

MaterialApp(
  title: DemoLocalizations.of(context).title, //不能正常工作!
  localizationsDelegates: [
    // 本地化的代理类
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    DemoLocalizationsDelegate() // 设置Delegate
  ],
);

1: Localizations.of会从当前的context沿着widget树向顶部查找DemoLocalizations 2: 但是实际上DemoLocalizations是在当前context的子树中的 DemoLocalizations.of(context)会返回null 3: 解决办法 设置一个onGenerateTitle回调

MaterialApp(
  onGenerateTitle: (context){
    // 此时context在Localizations的子树中
    return DemoLocalizations.of(context).title;
  },
  localizationsDelegates: [
    DemoLocalizationsDelegate(),
    ...
  ],
);

为英语系的国家指定同一个locale 1: 提供一种英语(如美国英语)供所有英语系国家使用 可以在前面介绍的localeListResolutionCallback中来做兼容:

localeListResolutionCallback:
  (List<Locale> locales, Iterable<Locale> supportedLocales) {
  // 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')     
}
  • 触碰到核心
  1. UI系统的含义

1: 基于一个平台(操作系统) 2: 在该平台实现GUI的一个系统

  1. 注意

1: 各个平台UI系统的原理是相通的 2: 无论是Android还是iOS,他们将一个用户界面展示到屏幕的流程是相似的

3: UI系统的原理

1: 屏幕显示图像的基本原理 显示器 显示器由一个个物理显示单元(物理像素点)组成 显示器成相原理: 在不同的物理像素点上显示不同的颜色构成完整的图像 位色 位色: 是显示器的一个重要指标 位色指: 一个像素点能发出的所有颜色总数是2的几次方 例如:1600万即2的24次方,称为24位色 刷新频率 显示画面 就是: 以固定的频率刷新 刷新需要从GPU获取数据 每次刷新: 显示器会发出一个垂直同步信号,用来同步CPU、GPU和显示器的 CPU、GPU和显示器的协作方式: CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,视频控制器按照同步信号从帧缓冲区取帧数据,并且传递给显示器显示 (CPU主要用于基本数学和逻辑计算,GPU的主要作用就是确定最终输送给显示器的各个像素点的色值) 例如: 一部手机屏幕的刷新频率是 60Hz,就是屏幕就会一秒内发出 60次这样的信号 2: 操作系统绘制API a: 图形计算和绘制是由相应的硬件来完成 b: 直接操作硬件的指令通常都会有操作系统屏蔽 c: 操作系统提供一些封装后的API,供操作系统之上的应用调用 d: 操作系统提供的API往往比较基础,直接调用比较复杂和低效的,需要了解API的很多细节 e: 几乎所有关于开发GUI程序的编程语言都会在操作系统之上再封装一层(操作系统原生API封装在一个编程框架和模型中,然后定义一种简单的开发规则来开发GUI应用程序) f: 我们所说的“UI”系统,就是指这个 g:

ui系统 被封装的系统
Android SDK(中:UI描述文件XML+Java操作DOM) Android操作系统
UIKit ios操作系统

3: Flutter UI系统 Flutter的原理 a: 使用同一种编程语言开发 b: 不同操作系统API抽象一个的中间层(Dart API) c: 在打包编译时再使用相应的中间层代码 d: 底层使用OpenGL这种跨平台的绘制库(OpenGL只是操作系统API的一个封装库,相当于直接调用操作系统API) 4: 组合和响应式 Flutter UI系统对应用开发者定义的开发标准就是: 组合和响应式 理解: Flutter中,一切都是Widget,一个UI界面通过组合其它Widget来实现 理解: UI要发生变化时,不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI

7/9

  • Element与BuildContext
  1. 基础知识

最终的UI树其实是由一个个独立的Element节点构成 组件最终的Layout、渲染都是通过RenderObject来完成的

  1. 流程

1: 根据Widget生成Element 2: 创建相应的RenderObject 3: 关联到Element.renderObject属性上 4: 通过RenderObject来完成布局排列和绘制

  1. 功能

Widget 树: 咱们写的组件 Element 树: 页面上的一个个节点 RenderObject 树: 每个节点对应的渲染对象

  1. Element的生命周期

1: 创建, Framework会调用Widget.createElement 创建一个Element实例 2: active状态, Framework会调用element.mount方法会创建RenderObject对象,并且添加到渲染树后的状态 3: 复用, 在更新前会调用对应Widget的canUpdate方法,判断是否复用(可以通过指定不同的Key来避免复用) 4: inactive状态, 移除element 时,Element就会调用deactivateChild 方法来移除它,element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate方法,状态变成inactive,不会再显示到屏幕 5: defunct状态, inactive的element在动画执行结束后它还未能重新变成active状态,Framework就会调用其unmount方法将其彻底移除,变成defunct,不会再被插入到树中.

  1. BuildContext

1: context 的初步使用

功能 代码
获取主题 Theme.of(context)
获取主题 Theme.of(context)
入栈新路由 Navigator.push(context, route)

2: 介绍 BuildContext是一个抽象接口类

abstract class BuildContext {}

抽象类必须要有实现才能用 接下来看context的实现类

class StatelessElement extends ComponentElement {
  ...
  @override
  Widget build() => widget.build(this);
  ...
}

源码1: this 就是 StatelessElement 源码2: StatelessElement 继承 Element 类

class Element extends DiagnosticableTree implements BuildContext {
  ...
}

源码3: Element 有BuildContext 接口

结论: BuildContext就是widget对应的Element

7l/11

  • buildContext 进阶
  1. BuildContext 就是 Element对象,
  2. 大多数时候开发者只需要关注widget层,在build时传入Element对象, 就是为了在需要时候直接操作
  3. 考验理解力的两个问题
  1. 通过Element来搭建一个UI框架?(示例: HomeView ) 如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。
  2. flutter能不能做响应式?
  • 布局过程: 1 RenderObject 和 RenderBox
  1. 每个Element都对应一个RenderObject,RenderObject职责是Layout和绘制
  2. 简单理解

RenderObject就是渲染树中的一个对象, 它拥有一个parent和一个parentData 插槽(slot)(有点不理解)

  1. 布局过程

1: RenderBox 的layout是通过传递 BoxConstraints 对象实现的 BoxConstraints可以限制子节点的最大和最小宽高 布局时: 父节点会调用子节点的layout()方法

void layout(Constraints constraints, { bool parentUsesSize = false }) {
   ...
   RenderObject relayoutBoundary; 
    if (!parentUsesSize || sizedByParent || constraints.isTight 
    	|| parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    ...
    if (sizedByParent) {
        performResize();
    }
    performLayout();
    ...
}

1: constraints 是对子节点大小的限制 2: parentUsesSize(布尔值,表示子节点布局变化是否影响父节点) parentUsesSize 同时用于确定 relayoutBoundary 3: relayoutBoundary 想知道relayoutBoundary,得先知道markNeedsLayout markNeedsLayout(1) 前面的只是中,当Element标记为 dirty 时便会重新build markNeedsLayout(2) 调用 markNeedsBuild() 可以标记Element为dirty markNeedsLayout(3) 类似的, RenderObject中markNeedsLayout()方法也可以标记为dirty markNeedsLayout(4) 部分源码

void markNeedsLayout() {
  ...
  assert(_relayoutBoundary != null);
  if (_relayoutBoundary != this) {
    markParentNeedsLayout();
  } else {
    _needsLayout = true;
    if (owner != null) {
      ...
      owner._nodesNeedingLayout.add(this);
      owner.requestVisualUpdate();
    }
  }
}

markNeedsLayout(5) 源码可知: markNeedsLayout会向上查找到是 relayoutBoundary 的 RenderObject为止,然后再将其标记为 dirty markNeedsLayout(6) 也就是说:如果一个 RenderObject 是 relayoutBoundary,就表示它的大小变化不会再影响到 parent 的大小了,于是 parent 也就不用重新布局了

7/12

  • 布局过程: 2 performResize 和 performLayout
  1. performLayout 每次布局都会被调用
  2. performResize sizedByParent 为 true 时调用
  3. sizedByParent 该节点的大小(属性)和其子节点是否无关
  4. performLayout() 方法中除了完成自身布局,也必须完成子节点的布局,这是因为只有父子节点全部完成后布局流程才算真正完成。
  5. 所以最终的调用栈将会变成:layout() > performResize()/performLayout() > child.layout() > ... ,如此递归完成整个UI的布局。
  • 布局过程: 3 ParentData
  1. layout结束后, 节点的位置就确定了, RenderObject就可以进行绘制
  2. 那么,节点的位置信息怎么保存(假如多个子组件,给每一个子组件设置一个位置)
  3. RenderObject的parentData属性来保存

parentData属性默认是一个BoxParentData对象 该属性只能通过父节点的setupParentData()方法来设置

abstract class RenderBox extends RenderObject {
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! BoxParentData)
      child.parentData = BoxParentData();
  }
  ...
}
  1. ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中
  • 绘制过程: 1
  1. 通过paint()方法来完成具体绘制逻辑

签名

void paint(PaintingContext context, Offset offset) { }

context.canvas可以取到Canvas对象, 接下来调用Canvas API来实现具体的绘制逻辑

  1. 当有子节点的情况,除了完成自身绘制逻辑之外,还要调用子节点的绘制方法

部分源码

  // 如果子元素未超出当前边界,则绘制子元素  
  if (_overflow <= 0.0) {
    defaultPaint(context, offset);
    return;
  }

  // 如果size为空,则无需绘制
  if (size.isEmpty)
    return;

  // 剪裁掉溢出边界的部分
  context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint);
    assert(() {
    final String debugOverflowHints = '...'; //溢出提示内容,省略
    // 绘制溢出部分的错误提示样式
    Rect overflowChildRect;
    switch (_direction) {
      case Axis.horizontal:
        overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
        break;
      case Axis.vertical:
        overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
        break;
    }  
    paintOverflowIndicator(context, offset, Offset.zero & size,
                           overflowChildRect, overflowHints: debugOverflowHints);
    return true;
  }());

根据有无溢出,调用defaultPaint(context, offset)来完成绘制 当需要绘制的内容大小溢出当前空间时,将会执行paintOverflowIndicator() 来绘制溢出部分提示,这个就是我们经常看到的溢出提示 defaultPaint 部分代码

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData;
    //绘制子节点, 
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}

defaultPaint中会调用paintChild()来绘制子节点

7/14

  • 绘制过程 2 RepaintBoundary
  1. RepaintBoundary 和 RelayoutBoundary 相似
  2. 用于在确定重绘边界, 另外绘制边界需要由开发者通过RepaintBoundary 组件自己指定

例如:

CustomPaint(
  size: Size(300, 300), //指定画布大小
  painter: MyPainter(),
  child: RepaintBoundary(
    child: Container(...),
  ),
),
  1. RepaintBoundary的原理
void paintChild(RenderObject child, Offset offset) {
  ...
  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);
  }
  ...
}

isRepaintBoundary该属性决定这个RenderObject重绘时是否独立于其父元素 _compositeChild源码

void _compositeChild(RenderObject child, Offset offset) {
  // 给子节点创建一个layer ,然后再上面绘制子节点 
  if (child._needsPaint) {
    repaintCompositedChild(child, debugAlsoPaintedParent: true);
  } else {
    ...
  }
  assert(child._layer != null);
  child._layer.offset = offset;
  appendLayer(child._layer);
}

通过在不同的layer(层)上绘制的(正确使用isRepaintBoundary属性可以提高绘制效率,避免不必要的重绘) RenderObject也提供了一个markNeedsPaint()方法

void markNeedsPaint() {
 ...
  //如果RenderObject.isRepaintBoundary 为true,则该RenderObject拥有layer,直接绘制  
  if (isRepaintBoundary) {
    ...
    if (owner != null) {
      //找到最近的layer,绘制  
      owner._nodesNeedingPaint.add(this);
      owner.requestVisualUpdate();
    }
  } else if (parent is RenderObject) {
    // 没有自己的layer, 会和一个祖先节点共用一个layer  
    assert(_layer == null);
    final RenderObject parent = this.parent;
    // 向父级递归查找  
    parent.markNeedsPaint();
    assert(parent == this.parent);
  } else {
    // 如果直到根节点也没找到一个Layer,那么便需要绘制自身,因为没有其它节点可以绘制根节点。  
    if (owner != null)
      owner.requestVisualUpdate();
  }
}

调用 markNeedsPaint() 从当前 RenderObject 开始一直向父节点查找 直到找到 一个isRepaintBoundary 为 true的RenderObject 时,才会触发重绘 可以实现局部重绘 开发中通过RepaintBoundary Widget来指定isRepaintBoundary 为 true,绘制时仅会重绘自身而无需重绘它的 parent,如此便可提高性能。 如果使用了RepaintBoundary,其对应的RenderRepaintBoundary会自动将isRepaintBoundary设为true的(如下)

class RenderRepaintBoundary extends RenderProxyBox {
  /// Creates a repaint boundary around [child].
  RenderRepaintBoundary({ RenderBox child }) : super(child);

  @override
  bool get isRepaintBoundary => true;
}
  • 命中测试
  1. 一个对象是否可以响应事件,取决于其对命中测试的返回

当发生用户事件时,会从根节点(RenderView)开始进行命中测试 RenderView默认的hitTest()如下

bool hitTest(HitTestResult result, { Offset position }) {
  if (child != null)
    child.hitTest(result, position: position); //递归子RenderBox进行命中测试
  result.add(HitTestEntry(this)); //将测试结果添加到result中
  return true;
}

RenderBox默认的hitTest()如下

bool hitTest(HitTestResult result, { @required Offset position }) {
  ...  
  if (_size.contains(position)) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}
  1. hitTest

用来判断该RenderObject 是否在被点击的范围内 同时负责将被点击的 RenderBox 添加到 HitTestResult 列表中 参数 position 为事件触发的坐标 回 true 则表示有RenderBox 通过了命中测试 可以直接重写hitTest()方法

  • 总结

从头到尾实现一个RenderObject是比较麻烦的,我们必须去实现layout、绘制和命中测试逻辑Front-End

  • 运行机制
  1. 启动

入口: 在"lib/main.dart"的main()函数中,Dart应用程序的起点 main()函数

void main(){runApp(MyApp())}

runApp()方法

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

1: app是一个widget,它是Flutter应用启动后要展示的第一个Widget 2: WidgetsFlutterBinding是绑定widget 框架和Flutter engine的桥梁 WidgetsFlutterBinding

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

1: Binding 了解Binding之前先了解一下 Window Window

class Window {
    
  // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
  // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5 
  double get devicePixelRatio => _devicePixelRatio;
  
  // Flutter UI绘制区域的大小
  Size get physicalSize => _physicalSize;

  // 当前系统默认的语言Locale
  Locale get locale;
    
  // 当前系统字体缩放比例。  
  double get textScaleFactor => _textScaleFactor;  
    
  // 当绘制区域大小改变回调
  VoidCallback get onMetricsChanged => _onMetricsChanged;  
  // Locale发生变化回调
  VoidCallback get onLocaleChanged => _onLocaleChanged;
  // 系统字体缩放变化回调
  VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
  // 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
  FrameCallback get onBeginFrame => _onBeginFrame;
  // 绘制回调  
  VoidCallback get onDrawFrame => _onDrawFrame;
  // 点击或指针事件回调
  PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
  // 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
  // 此方法会直接调用Flutter engine的Window_scheduleFrame方法
  void scheduleFrame() native 'Window_scheduleFrame';
  // 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
  void render(Scene scene) native 'Window_render';

  // 发送平台消息
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) ;
  // 平台通道消息处理回调  
  PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
}

是 Flutter Framework连接宿主操作系统的接口 包含了当前设备和系统的一些信息以及Flutter Engine的一些回调

再回来看看 WidgetsFlutterBinding 混入的各种 Binding 这些binding 基本都是监听并处理Window对象的一些事件 再理解 WidgetsFlutterBinding, 它正是粘连Flutter engine与上层Framework的“胶水”

2: ensureInitialized: 负责初始化一个WidgetsBinding的全局单例 3: attachRootWidget: 负责将根Widget添加到RenderView上

void attachRootWidget(Widget rootWidget) {
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView, 
    debugShortDescription: '[root]',
    child: rootWidget
  ).attachToRenderTree(buildOwner, renderViewElement);
}

renderView: 是一个RenderObject,它是渲染树的根 renderViewElement: 是renderView对应的Element对象 attachToRenderTree

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
  if (element == null) {
    owner.lockState(() {
      element = createElement();
      assert(element != null);
      element.assignOwner(owner);
    });
    owner.buildScope(element, () {
      element.mount(null, null);
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

attachToRenderTree(RenderObjectToWidgetElement)负责创建根element, 并且将element与widget 进行关联(创建出 widget树对应的element树)

  1. 渲染

attachRootWidget后

WidgetsFlutterBinding.scheduleWarmUpFrame()方法 它被调用后会立即进行一次绘制 在本次绘制结束完成之前Flutter将不会响应各种事件(绘制结束前,该方法会锁定事件分发) 保证在绘制过程中不会再触发新的重绘

scheduleWarmUpFrame()方法

void scheduleWarmUpFrame() {
  ...
  Timer.run(() {
    handleBeginFrame(null); 
  });
  Timer.run(() {
    handleDrawFrame();  
    resetEpoch();
  });
  // 锁定事件
  lockEvents(() async {
    await endOfFrame;
    Timeline.finishSync();
  });
 ...
}

handleBeginFrame() 和 handleDrawFrame() 了解上面两个方法,先了解Frame 和 FrameCallback 的概念 Frame: 一次绘制过程,我们称其为一帧。Flutter engine受显示器垂直同步信号"VSync"的驱使不断的触发绘制。我们之前说的Flutter可以实现60fps(Frame Per-Second),就是指一秒钟可以触发60次重绘,FPS值越大,界面就越流畅。 FrameCallback:SchedulerBinding 类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行: FrameCallback回调队列: transientCallbacks:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。 FrameCallback回调队列: persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。 FrameCallback回调队列: postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册,注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。

再来看handleBeginFrame() 和 handleDrawFrame() 前者主要是执行了transientCallbacks队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列

  1. 绘制

渲染和绘制逻辑在RendererBinding中实现 RendererBinding中的initInstances()方法

void initInstances() {
  ... //省略无关代码
      
  //监听Window对象的事件  
  ui.window
    ..onMetricsChanged = handleMetricsChanged
    ..onTextScaleFactorChanged = handleTextScaleFactorChanged
    ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
    ..onSemanticsAction = _handleSemanticsAction;
   
  //添加PersistentFrameCallback    
  addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
}

addPersistentFrameCallback 向persistentCallbacks队列添加回调 _handlePersistentFrameCallback _handlePersistentFrameCallback方法直接调用了RendererBinding的drawFrame()

void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); //布局
  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
  pipelineOwner.flushPaint(); // 重绘
  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

flushLayout: 更新了所有被标记为“dirty”的RenderObject的布局信息 flushCompositingBits: 检查RenderObject是否需要重绘,然后更新RenderObject.needsCompositing属性 flushPaint: 该方法进行了最终的绘制,只重绘了需要重绘的 RenderObject compositeFrame: 将Canvas画好的Scene传给window.render()方法 window.render(): 会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上

  • 图片加载原理与缓存
  1. ImageProvider

Image 组件的image 参数是一个必选参数,它是ImageProvider类型 是一个抽象类 定义了图片数据获取和加载的相关接口 提供图片数据源, 和缓存图片

abstract class ImageProvider<T> {

  ImageStream resolve(ImageConfiguration configuration) {
    // 实现代码省略
  }
  Future<bool> evict({ ImageCache cache,
    ImageConfiguration configuration = ImageConfiguration.empty }) async {
    // 实现代码省略
  }

  Future<T> obtainKey(ImageConfiguration configuration); 
  @protected
  ImageStreamCompleter load(T key); // 需子类实现
}

load(T key)方法: 加载图片数据源的接口 以NetworkImage为例,看看其load方法的实现


@override
ImageStreamCompleter load(image_provider.NetworkImage key) {

  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
  
  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key, chunkEvents), //调用
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    ... //省略无关代码
  );
}

MultiFrameImageStreamCompleter 是ImageStreamCompleter子类 其中ImageStreamCompleter 是一个抽象类,定义了管理图片加载过程的一些接口,通过它来监听图片加载状态的 MultiFrameImageStreamCompleter 需要一个codec参数,类型为: Future<ui.Codec> Codec 是处理图片编解码的类的一个handler,它只是一个flutter engine API 的包装类 也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的 Codec代码

@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
  // 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
  @pragma('vm:entry-point')
  Codec._();

  /// 图片中的帧数(动态图会有多帧)
  int get frameCount native 'Codec_frameCount';

  /// 动画重复的次数
  /// * 0 表示只执行一次
  /// * -1 表示循环执行
  int get repetitionCount native 'Codec_repetitionCount';

  /// 获取下一个动画帧
  Future<FrameInfo> getNextFrame() {
    return _futurize(_getNextFrame);
  }

  String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';

Codec最终的结果是一个或多个(动图)帧

MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值 _loadAsync方法


 Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
  ) async {
    try {
      //下载图片
      final Uri resolved = Uri.base.resolve(key.url);
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok)
        throw Exception(...);
      // 接收图片数据 
      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int total) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
      );
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');
      // 对图片数据进行解码
      return PaintingBinding.instance.instantiateImageCodec(bytes);
    } finally {
      chunkEvents.close();
    }
  }

_loadAsync:下载图片。并且对下载的图片数据进行解码。 下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。 下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码

ImageProvider 中 obtainKey: 配合实现图片缓存 ImageProvider 中 resolve: ImageProvider的暴露的给Image的主入口方法(接受ImageConfiguration 包含图片和设备的相关信息,返回: ImageStream 图片数据流) 上面代码A处就是处理缓存的主要代码,这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性, ImageCache ImageProvider 中 ImageCache ImageCache类定义

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB

class ImageCache {
  // 正在加载中的图片队列
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  // 缓存队列
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};

  // 缓存数量上限(1000)
  int _maximumSize = _kDefaultSize;
  // 缓存容量上限 (100 MB)
  int _maximumSizeBytes = _kDefaultSizeBytes;
  
  // 缓存上限设置的setter
  set maximumSize(int value) {...}
  set maximumSizeBytes(int value) {...}
 
  ... // 省略部分定义

  // 清除所有缓存
  void clear() {
    // ...省略具体实现代码
  }

  // 清除指定key对应的图片缓存
  bool evict(Object key) {
   // ...省略具体实现代码
  }

 
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
    assert(key != null);
    assert(loader != null);
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    // 图片还未加载成功,直接返回
    if (result != null)
      return result;
 
    // 有缓存,继续往下走
    // 先移除缓存,后再添加,可以让最新使用过的缓存在_map中的位置更近一些,清理时会LRU来清除
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    try {
      result = loader();
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    void listener(ImageInfo info, bool syncCall) {
      final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
      final _CachedImage image = _CachedImage(result, imageSize);
      // 下面是缓存处理的逻辑
      if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
        _maximumSizeBytes = imageSize + 1000;
      }
      _currentSizeBytes += imageSize;
      final _PendingImage pendingImage = _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }

      _cache[key] = image;
      _checkCacheSize();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      _pendingImages[key] = _PendingImage(result, streamListener);
      // Listener is removed in [_PendingImage.removeListener].
      result.addListener(streamListener);
    }
    return result;
  }

  // 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内
  void _checkCacheSize() {
   while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
      final Object key = _cache.keys.first;
      final _CachedImage image = _cache[key];
      _currentSizeBytes -= image.sizeBytes;
      _cache.remove(key);
    }
    ... //省略无关代码
  }
}

缓存则使用缓存,没有缓存则调用load方法加载图片 加载成功后, 断图片数据有没有缓存,如果有,则直接返回ImageStream 没有缓存,则调用load(T key)方法从数据源加载图片数据,加载成功后先缓存,然后返回ImageStream setter: 有设置缓存上限的setter(所以,如果我们可以自定义缓存上限)

 PaintingBinding.instance.imageCache.maximumSize=2000; //最多2000张
 PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; //最大200M

其他: 对于网络图片来说,会将其“url+缩放比例”作为缓存的key。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。 总结: ImageProvider 加载图片数据并进行缓存、解码

  1. Image组件原理

研究一下Image是如何和ImageProvider配合: 获取最终解码后的数据,然后又如何将图片绘制到屏幕上的 MyImage(简易版Image)

About

接触flutter

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages