笔记结合了Flutter官方文档,Dart官方文档,B站这个视频,《Flutter实战第二版》这本书
Introduction
- Flutter 跨平台、高保真、高性能,使用Dart语言开发,Impeller将会取代Skia作为Flutter主渲染方案(和React Native需要native components和javascript通信,Dart可以直接操作UI层,不需要两层通信)
- 配环境
- Flutter SDK + 配置bin到环境变量
- Android Studio 配置Flutter,Dart插件, 配置模拟器; VS Code 配置Flutter, Dart插件; Idea 配置Flutter, Dart插件
- 用
flutter doctor
检查flutter环境和需要的依赖是否完整- 如果是customized Android SDK path:
flutter config --android-sdk <path>
- Android toolchain报错: 再Android
Studio中下载
Android SDK Command-line Tools (latest)
- 需要同意Android许可:
flutter doctor --android-licenses
- 如果是customized Android SDK path:
- 升级Flutter: 运行
flutter upgrade
- Android Studio 修改已安装模拟器的路径:
- 添加环境变量:
ANDROID_SDK_HOME
, 值是<path>
- 把
C:\Users\<User>\.android
中的avd
文件夹复制到<path>\.android
- 修改模拟器对应的
<emulator_name>.ini
文件,把path
改为<path>\.android\avd
- 添加环境变量:
- Dart SDK
Dart Language
- General concepts
- Everything in a variable is an object, every object is an instance of a class
- Dart is a strong-typed language, but it can do type inference when
using
var
- Dart introduced null safety in 2.12, if a variable can be of type x
or null, it should be declared as
x?
, if a variable can be of type x but Dart disagrees you cau usex!
to force it to be of type x - When any type is allowed, use
Object?
,Object
ordynamic
- Dart supports generic types
- Dart supports top-level functions/variables, as well as functions/variables tied to a class or object
- Dart does not have accessibility keywords, for private items, use
_
as the first character - Dart has both expressions (which have runtime values, like ternary expressions) and statements (which don’t, like if else statements)
- Dart tools can report two kinds of problems: warnings and errors
- Variables
- You can declare a variable with its type, or use type inference
- Uninitialized variables that have a nullable type have an initial value of null
- You can use
late
modifier to defer initialization until the first time the variable is used - A final variable can be set only once; a const variable is a compile-time constant(you must initialize a constant variable with a value, otherwise there will be a compile error)
- Built-int types
- Numbers:
int
,double
; Strings:String
; Booleans:bool
; Lists:List
; Sets:Set
; Maps:Map
; Runes:Runes
; Symbols:Symbol
, the value null:Null
. Other types include:Object
,Enum
,Future
andStream
,Iterable
,Never
,dynamic
,void
- 字符串转换成数字:
int.parse('1')
,double.parse('1.1')
- 用三引号创建多行字符串:
var a = """ a\n b\n c\n""";
, 用r
创建原始字符串 - 整形转换成字符串:
1.toString()
- 浮点型转换成字符串并且小数点后保留n位:
1.1.toStringAsFixed(n)
- 布尔类型:
true
和false
- 列表初始化: 空列表
var l0 = <int>[];
, 指定列表大小和初始值var l1 = List.filled(3, 0);
, 指定列表大小和赋值方法var l2 = List.generate(3, (i) => i);
, 创建constant列表:var l3 = const [1, 2, 3];
- 列表展开:
var l1 = [1, 2, 3]; var l2 = [0, ...l1];
(使用...
), 对于可能是null的列表进行展开:var l1 = [1, 2, 3]; var l2 = [0, ...?l1];
(使用...?
) - 带if和for的列表:
1
2
3
4List intList = List.generate(3, (index) => index);
// 下面不要再函数体加括号,想要什么格式直接写什么格式
List strList1 = ["#0", for (var item in intList) "#${item}"];
List strList2 = [...strList1, if (3 > 2) "#4"]; - 反转列表:
var l1 = [1, 2, 3]; var l2 = new List.from(l1.reversed);
- 集合初始化: 空列表
var s1 = <int>{};
, 字面量var s2 = {1, 2, 3};
, 从Iterable创建集合var s3 = Set.from([1,2,3])
- 一些集合操作:
s1.add(1)
,s1.remove(1)
,s1.clear()
- 字典初始化: 空字典
Map m1 = <String, int>{};
字面量Map m = {"a": 0, "b": 1};
, 构造器Map m = new Map(); m["a"] = 0; m["b"] = 1;
- 字典一些操作:
m.containsKey("a")
,m.remove("a")
,m.clear()
, 还包括使用if和for, 以及...
和...?
- Numbers:
- Functions
- functions are objects and have a type,
Function
- Every app must have a top-level main() function, which serves as the
entrypoint to the app. The main() function returns void and has an
optional
List<String>
parameter for arguments - 函数返回类型也可以进行类型推断,但是不推荐
- A function can have any number of required positional parameters. These can be followed either by named parameters or by optional positional parameters (but not both)
- Use curly braces
{}
for named parameters. If no default value is provided and marked as required (userequired
keyword), the parameter type must be nullable and its default value should be null1
2
3
4
5
6
7
8
9int addAge({int num1 = 0, int num2 = 0}) {
return num1 + num2;
}
void main() {
print(addAge()); // 0
print(addAge(num1: 1)); // 1
print(addAge(num2: 2)); // 2
print(addAge(num1: 1, num2: 2)); // 3
} - Use bracket
[]
for optional positional parameters. If no default value is provided, the parameter type must be nullable and its default value should be null1
2
3
4
5
6
7int addAge({int num1, [int? num2]) {
return num1 + (num2 == null ? 0 : num2);
}
void main() {
print(addAge(1)); // 1
print(addAge(1, 2)); // 3
} - Functions as first-class objects: you can pass a function as a parameter to another function
- Anonymous functions/lambda/closure
- functions are objects and have a type,
- Operators
- Some operators:
/
division,~/
integer division,??
if null operator (var a = b?? c;
if variable b is null, a is assigned to the value in variable c),type1 as type2
cast type1 to type2,type1 is type2
check if type1 is type2,type1 is! type2
check if type1 is not type2 - Cascade notation:
..
and?..
, usage:object..method1()..method2()..method3()
, the?..
guarantees that non of the methods will be called if the object is null. Cascade can be nested, you need to use()
to separate them
- Some operators:
- Control Flow statements
- In and else:
if(condition) {} else if (condition) {} else {}
- For loops:
for (initial; condition; increment) {}
,for (var i in iterable)
, forEach on iterableiterable.forEach((ele) => callback(ele));
, iterable forEach with conditioniterable.where((ele) => condition(ele)).forEach((ele) => callback(ele));
- While and do-while loops:
while (condition) {}
,do {} while (condition);
- Break and continue:
break
andcontinue
are used to control the flow of the loop - Switch and case:
switch (expression) { case value1: break; case value2: break; default: break; }
, expression can be integers, strings, or compile-time constants. If a case is empty and you want fall-through, you can omit break; for a non-empty case, if break is omitted, there will be a compile error; if you want fall-through with a non empty case, you need to use and labels1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26void main() {
print("Enter an integer");
String? command = stdin.readLineSync();
int input = int.parse(command!);
int res = 0;
switch (input) {
case 0:
print("${res}");
break;
case 1:
case 2:
print("positive ${res + input}");
continue label;
label:
case 3:
print("done!");
break;
default:
print("done!");
}
}
// for input 0, output is 0
// for input 1, output is positive 1 \n done!
// for input 2, output is positive 2 \n done!
// for input 3, output is done!
// for other inputs, output is done!
- In and else:
Assert:
assert(condition, message)
, if condition is false, anAssertionError
is thrown and the message is printedExceptions:
- Throw exception:
throw Object;
,throw Exception(meg);
- Catch exceptions
1
2
3
4
5
6
7
8
9
10
11
12
13try {
breedMoreLlamas();
} on OutOfLlamasException {
// A specific exception
buyMoreLlamas();
} on Exception catch (e) {
// Anything else that is an exception
print('Unknown exception: $e');
} catch (e, s) {
// No specified type, handles alls
print('Something really unknown: $e');
print('Stack trace:\n $s');
} - Finally
1
2
3
4
5
6
7try {
breedMoreLlamas();
} catch (e) {
print('Error: $e'); // Handle the exception first.
} finally {
cleanLlamaStalls(); // Then clean up.
}
- Throw exception:
Classes
- All classes except Null descend from Object
- Mixin-based inheritance means that although every class (except for the Object class) has exactly one superclass, a class body can be reused in multiple class hierarchies
- Use
.
to refer to an instance variable or method, use?.
to avoid exception when the leftmost operand is null - Use constructor to, the
new
keyword is optional, constructors can be eitherClassName
(can have one unnamed constructor) orClassName.identifier
(can have multiple named constructors), useconst
to create a compile-time constant - Use
object.runtimeType
to get the type of an object - All instance variables generate an implicit getter method, non-final
instance variables and
late final
instance variables without initializers also generate an implicit setter method - Constructors
- A syntactic sugar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class A {
// non-null variables must be initialized
String name = "";
int age = 0;
A (String name, int age) {
this.name = name;
this.age = age;
}
}
// recommended syntax in dart
class B {
// non-null variables and final variables must be initialized
String name;
final int age;
// the instance variables are initialized before the constructor body is executed
A (this.name, this.age);
} - Each class has a default unnamed constructor with no parameters, if you define your own unnamed constructor, the default constructor is not generated
- Named parameters, the order does not matter for named parameters
1
2
3
4
5
6
7
8
9
10
11class A {
final String name;
final String email;
// the user should input email, although it can be null
A (this.name, {required this.email});
}
void main() {
A a = A("name", email: "email");
} - Factory constructor can be used to create instances of subclasses,
to prepare calculated values to forward them as parameters to a normal
constructor so that final fields can be initialized with them
1
2
3
4
5
6
7
8
9
10
11class A {
final String? name;
final String? email;
A._internal(this.name, this.email);
factory A(String name) {
var email = name + "@gmail.com";
return A._internal(name, email);
}
} - Constructors cannot be inherited
- Order of execution in subclass constructor
- Initializer list
- Superclass's no-arg constructor (if it does not exist, you must manually call a superclass constructor after colon before the constructor body)
- Current class's no-arg constructor
- A syntactic sugar
- Abstract classes: use the
abstract class
keyword - Use
@override
to override a method in a subclass - If you want to down cast a class, use the
covariant
keyword - There is not
interface
keyword in dart, you need to implement all the instance members in the interface. Explicit interface: useabstract class
, implicit interface: every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements - Mixins: mixins are a way of reusing a class’s code in multiple class
hierarchies
- Use the
mixin
keyword to define a mixin - Use the
with
keyword to apply a mixin to a class, the order matters, inA extends B with C, D
, the top of the inheritance hierarchy is B, then B with C, then B with C and D, then A - Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Performer {
void perform() => print("Performs!");
}
mixin Guitarist {
void playGuitar() => print("Play guitar");
void perform() => playGuitar();
}
mixin Drummer {
void playDrums() => print("Play drums");
void perform() => playDrums();
}
class Musician extends Performer with Guitarist, Drummer {}
void main() {
Musician musician = new Musician();
// in Performer with Guitarist, Drummer, the perform method in mixin Drummer is called
musician.perform();
} - Use the
on
keyword to specify which class the mixin can be applied to,mixin A on B
means that mixin A can only be applied to a class that extends B
- Use the
- Use
extension
to extend built-in classes1
2
3
4
5
6
7
8
9extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
void main() {
print("42".parseInt());
}
Generics
- Generics are often required for type safety, but it can also result in better generated code and reduce code duplication
- You can use generics with collection literals:
var names = <String>['Seth', 'Kathy', 'Lars'];
- You can use parameterized types with constructors:
var nameSet = Set<String>.from(names);
- You can use generics to restrict the parameterized type:
class Foo<T extends SomeBaseClass>
- You can use generics in methods:
T first <T>(List<T> ts) {return ts[0];};
Libraries and visibility
- Use
import
to specify how a namespace from one library is used in the scope of another library - Dart built-in libraries, the URI has the special
dart:
scheme - You can specify a library prefix when there is a conflict between
two libraries
1
2
3
4
5
6
7import "package1:/lib1/lib1.dart";
import "package2:/lib2/lib2.dart" as lib2;
// use lib1
Element element1 = Element();
// use lib2
lib2.Element element2 = lib2.Element(); - Use part of the library only:
import 'package:lib1/lib1.dart' show foo;
import only foo,import 'package:lib1/lib1.dart' hide foo;
import everything except foo - Lazily loading a library: only load the library when it is needed,
import 'package:greetings/hello.dart' deferred as hello;
- Use
Asynchrony support
- Some functions return
Future
orStream
objects - Use
async
andawait
to write asynchronous code (await can only be used in async functions),try
,catch
,finally
can also help1
2
3
4
5
6void main() async {
print("Before!");
await Future.delayed(
new Duration(seconds: 5), () => print("Delayed 5 seconds"));
print("After");
}
- Some functions return
Generators
- Used to lazily produce a sequence of values
- Synchronous generator returns an Iterable object
- Asynchronous generator returns a Stream object
Flutter Introduction
- 跨平台技术简介
- H5 + 原生 (Cordova、Ionic、微信小程序): 将APP中需要动态变化的内容通过HTML5实现,通过原生网页加载控件来加载
- JavaScript开发 + 原生渲染 (React Native, Weex): 状态转移之后更新UI(RN中虚拟DOM会通过JavaScriptCore映射为原生控件)
- 自绘UI + 原生 (Qt for mobile, Flutter): 不同平台实现一个同意接口的渲染引擎来绘制UI,不依赖系统原生控件
- Flutter 高性能
- Flutter使用Dart,在JIT(Just-In-Time,实时编译)和JS差不多快,在AOT(Ahead-Of-Time, 预先编译)比JS快
- Flutter使用自绘引擎来绘制UI,不需要Javascript和Native之间通信
- Flutter架构
- Flutter framework: a modern, reactive framework written in the Dart
language
- 底层UI库: Foundation, Animation, Painting, Gestures
- 抽象布局层: Rendering
- 基础组件: Widgets, 在其之上还有Material和Cupertino风格组件库,分别实现了Material和iOS设计规范
- Flutter Engine: mostly written in C++ and supports the primitives necessary to support all Flutter applications
- Embedders:the embedder is written in a language that is appropriate for the platform
- Flutter framework: a modern, reactive framework written in the Dart
language
- First app
lib/main.dart
主要代码- 如果要使用Material风格,在
pubspec.yaml
的flutter
部分加入uses-material-design: true
,在主文件中加入import 'package:flutter/material.dart';
- In flutter, almost everything is a widget, your class should extend
StatelessWidget
orStatefulWidget
- A widget’s main job is to provide a build() method that describes how to display the widget in terms of other, lower level widget
- 简单来说, Stateful
widget有状态,这些状态在widget的生命周期中是可以变化的。一个stateful
widget至少由两个类组成,一个是
StatefulWidget
类,另一个是State
类。StatefulWidget
类本身是不变的,但是State
类中的状态时可能变化的。 一般来说Stateful widget中的build方法写在State类中,用来解耦,否则需要把State类传到build方法中 Scaffold
是Material库中的页面脚手架,它包括了导航栏,标题,包含主屏幕的widget树的body
属性
- Widget
- Flutter中万物都是widget
- Widget用来描述UI元素的配置信息,配置信息就是widget接收的参数
- Widget属性应该是final的,因为如果属性变化Flutter就会重新构建widget
- widget类继承自DiagnosticableTree,DiagnosticableTree主要作用是提供调试信息
- Flutter框架处理流程
- 根据Widget树生成Element树,Element树中的节点继承自Element类。Widget树和Element树是一一对应的
- Element树生成Render树,Render树中的节点继承自RenderObject类。Render树包含了真正的布局和渲染逻辑。Element树和Render树不是一一对应的
- Render树生成Layer树,Layer树中的节点继承自Layer类
- 按照惯例widget的constructor使用named parameters
StatelessWidget
- 用于不需要维护状态的场景,通常在build方法中嵌套其他widget来构建UI
BuildContext
表示当前widget在widget树中的上下文,每一个widget对应一个context对象,可以遍历widget树查找其他widget
StatefulWidget
createState()
用来创建和StatefulWidget相关的状态
State
- 常用属性:
widget
,context
- 生命周期
initState()
: widget第一次插入到widget树的时候调用,只调用一次didChangeDependencies()
: 当State对象的一来发生变化的时候调用build()
: 用于构建widget子树,返回一个widgetreassemble"()
: 在开发模式下热重载时会调用,该回调用于重新构建widgetdidUpdateWidget()
: 当widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果需要更新则会调用此回调deactivate()
: 当State对象从树中被移除时,会调用此回调dispose()
: 当State对象从树中被永久移除时调用,通常在此回调中释放资源
- 常用属性:
- 在widget中获取state对象:
- 私有state:
Type _state = context.findAncestorStateOfType<Type>()
- 非私有state:
Type _state = Type.of(context)
, 这里需要Type提供一个of静态方法 - 给StatefulWidget添加一个GlobalKey:
static GlobalKey<Type> _globalKey = GlobalKey();
, 通过_globalKey.currentState
获取state对象
- 私有state:
- 通过
RenderObject
自定义widget- Stateful和Stateless widget都是用于组合其他组件的,它们本身没有对应的RenderObject
- Flutter组件库中的很多基础组件不是通过Stateful/Stateless widget实现的,而是通过RenderObject实现的
- 如果组件不包含子组件,则可以继承
LeafRenderObjectWidget
,如果组件包含子组件,则可以继承RenderObjectWidget
, 再通过重写方法就可以串键对应的组件
- 常用基础组件
- 用法:
import 'package:flutter/widgets.dart';
Text
: 带格式的文本Row
/Column
: 类似flexboxStack
: 取消线性布局,使用Positioned
来定位与相对于Stack的上下左右四条边的位置Container
: 创建矩形视觉元素, 有margins, padding等
- 用法:
- Material组件
- 用法:
import 'package:flutter/material.dart';
Scaffold
,AppBar
,TextButton
等
- 用法:
- Cupertino组件
- 用法:
import 'package:flutter/cupertino.dart';
CupertinoApp
,CupertinoButton
,CupertinoNavigationBar
等
- 用法:
- 状态管理
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父widget管理
- 如果状态是有关界面外观效果的,如动画,那么状态最好由widget本身管理
- 如果状态是有关界面布局的,如文本框的宽度,则该状态最好由父widget管理
- 路由管理
- 路由(Route)在移动开发中通常指页面(Page),Route 在 Android中 通常指一个 Activity,在 iOS 中指一个 ViewController
- 路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理
- Flutter中的路由管理和原生开发类似,会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈
- 一个例子
1
2
3
4
5
6
7
8TextButton(
child: const Text("Navigation."),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return NewPage();
}));
},
), - MaterialPageRoute
class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T>
, 这个类继承了PageRoute,PageRoute是一个抽象类,它定义了路由构建及切换时过渡动画的相关接口和属性。MaterialPageRoute的页面切换动画和原生平台风格一致MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false})
builder
是一个回调函数,作用是构建路由页面的具体内容,返回值是一个widgetsettings
包含路由的配置信息,如路由名称、是否初始路由(首页)maintainState
默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中fullscreenDialog
表示新的路由页面是否是一个全屏的模态对话框
- 如果需要自定义路由切换动画,可以自己继承PageRoute来实现
- Navigator
- Navigator是一个路由器管理的组件,它提供了打开和退出路由页面的方法
Navigator.push(BuildContext context, Route route)
:打开route
对应的路由页面,返回一个Future对象,当路由出栈关闭时,Future才会结束Navigator.pop(BuildContext context, [ result ])
:关闭当前路由页面,返回上一个路由页面,result
是关闭时传递给上一个路由页面的数据- Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法,
比如
Navigator.push(BuildContext context, Route route)
等价于Navigator.of(context).push(Route route)
- 路由器传值
- Navigator.pop()方法可以传一个参数给上一个路由页面,这个参数可以是任意类型,如String、int、bool、Map等。这时候需要上一个路由页面使用
async
方法和await
来接受Navigator.push()
方法Future的返回值 - 注意直接点击导航栏是不会触发
Navigator.pop()
方法的,所以需要在onWillPop
回调中处理
- Navigator.pop()方法可以传一个参数给上一个路由页面,这个参数可以是任意类型,如String、int、bool、Map等。这时候需要上一个路由页面使用
- 命名路由
- 对于据大多数应用来说,不推荐使用命名路由
- 给路由器起一个名字,然后通过名字直接打开新的路由,
需要一个
Map<String, WidgetBuilder> routes
的路由表 - 在App中配置
routes
属性, 比如1
2
3
4
5
6routes: {
// new_page
"new_page": (context) => NewPage(),
// HomePage
"/": (context) => MyHomePage(title: 'Flutter Demo Home Page')
}, - 使用
Navigator.pushNamed(BuildContext context, String routeName,{Object arguments})
打开新的路由 - 传递参数
- 在
pushNamed
中传递参数 - 在路由页面通过
var args = ModalRoute.of(context)!.settings.arguments
获取参数
- 在
- 路由器生成钩子
onGenerateRoute
在打开命名路由的时候可能调用:如果路由表中没有注册,才会调用,如果注册了,就会直接调用路由的builder方法- 在进入页面前需要进行权限控制的时候(比如判断是否登录),
可以不使用路由表,而是在APP中重写这个方法
1
2
3
4
5
6
7onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其他情况则正常打开路由。
});
}
- 包管理
- Flutter通过配置
pubspec.yaml
文件来管理依赖包 - 在
dependencies
下面添加依赖包 - 在
dev_dependencies
下面添加开发依赖包 - Pub(https://pub.dev/ )是 Google 官方的 Dart Packages 仓库
- 还可以使用本地包,和Git仓库中的包
1
2
3
4
5
6
7
8
9dependencies:
pkg1:
# path of package
path: ../../code/pkg1
pkg2:
# git url of package
git:
# if the package is not in the root path, you can use the path attribute
url: git://github.com/xxx/pkg1.git
- Flutter通过配置
- 资源管理
- Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。常见类型的 assets 包括静态数据(例如JSON文件)、配置文件、图标和图片等
- 在
pubspec.yaml
文件中配置资源文件1
2
3assets:
- assets/images/
- assets/config.json - 在代码中加载资源文件
- 加载文本文件
1
2
3Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
} - 加载图片
1
2
3
4
5
6
7
8
9Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('graphics/background.png'),
),
),
);
}
- 加载文本文件
- 设置APP图标
- Android中在
.../android/app/src/main/res
修改 - iOS中在
.../ios/Runner/Assets.xcassets/AppIcon.appiconset
中修改
- Android中在
- 修改启动页图片
- Android中在
.../android/app/src/main/res/drawable/launch_background.xml
中修改 - iOS中在
.../ios/Runner/Assets.xcassets/LaunchImage.imageset
中修改
- Android中在
Basic Flutter Widgets
- Text
- Text用于显示简单样式文本,包含一些控制文本样式的一些属性
Text("<textContent>", textAlign: <align>, maxLines: n, overflow: <overflow>, textScaleFactor: <scale>)
- textAlign:
TextAlign.left
,TextAlign.right
,TextAlign.center
, etc. 对齐参考的是Text的宽度,如果只有一行,那么就是整个textContent的宽度 - maxLines: 最大行数, 默认自动换行,指定这个参数之后,文本不会超过n行
- overflow:
超出显示方式,
TextOverflow.clip
裁剪,TextOverflow.fade
渐隐,TextOverflow.ellipsis
省略号 - textScaleFactor: 字体缩放因子,相当于style中的fontSize
- textAlign:
- TextStyle
- 用于指定文本显示的样式
- 用法:
Text(..., style: TextStyle(key: val,...))
- 属性:
color
,fontSize
,height
(是个倍数因子,相当于行高 = fontSize * height),fontFamily
,background
- TextSpan
- 对于Text的内容的不同部分按照不同的样式显示
- 用法:
Text.rich(TextSpan(children: [TextSpan(text: "<content1>", style: TextStyle(...)), TextSpan(text: "<content2>", style: TextStyle(...))]))
- DefaultTextStyle
- 注意到父级别的设置会被子级别继承,因此可以用DefaultTextStyle作为父级别,给它设置一个默认样式,然后所有的子级别Text都会继承这个样式
- 用法:
DefaultTextStyle(style: TextStyle(...), child: Text(...))
,这里child可以是多个Text的列表之类的,他们都会继承在style中定义的样式,如果子级别不想继承父级别的样式,可以在它自己的TextStyle中加入属性inherit: false
- Font
- 如果需要外部字体,首先需要修改
pubspec.yaml
文件,添加字体文件的路径1
2
3
4
5
6
7
8
9
10
11flutter:
fonts:
- family: <fontFamily>
fonts:
# 下面代表了不同的写法
- asset: <fontPath>
- asset: <fontPath>
weight: <weight>
- asset: <fontPath>
weight: <weight>
style: <style> - 在代码中使用字体
1
2
3
4
5
6
7Text(
"Hello World",
style: TextStyle(
fontFamily: <fontFamily>,
fontSize: 18,
),
) - 使用package中的字体
1
2
3
4
5
6
7Text(
"Hello World",
style: TextStyle(
fontFamily: <fontFamily>,
package: <package>,
),
)
- 如果需要外部字体,首先需要修改
- Button
- Material组件库中有不同的按钮,比如
ElevatedButton
,TextButton
,OutlineButton
等 - Material Button的相同点
- 按下按钮都会有水波动画
- 按钮都有一个
onPressed
属性,用于设置点击事件,没设置这个callback不影响点击
- ElevatedButton
- 默认带有阴影和灰色背景,按下按钮之后,阴影会变大
- 用法:
ElevatedButton(child: <child>, onPressed: <callback>)
- TextButton
- 默认背景透明不带阴影,按下之后会有背景色
- 用法:
TextButton(child: <child>, onPressed: <callback>)
- OutlineButton
- 默认有一个边框,不带阴影并且背景透明,按下后,边框颜色会变亮、同时出现背景和阴影(较弱)
- 用法:
OutlineButton(child: <child>, onPressed: <callback>)
- IconButton
- 是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景
- 用法:
IconButton(icon: <icon>, onPressed: <callback>)
icon
举例:Icon(Icons.add)
,Icon(Icons.send)
,Icon(Icons.thumb_up)
,Icon(Icons.info)
,Icon(Icons.favorite)
ElevatedButton
,TextButton
,OutlineButton
都有一个icon构造函数,通过它门可以轻松创建带图标的按钮- 用法:
XXXXXButton.icon(onPressed: <callback>, icon: <icon>, label: <label>)
- 用法:
- Material组件库中有不同的按钮,比如
- Image and ICON
- 通过Image组件加载并且显示图片,数据源可以是asset,文件,内存和网路
ImageProvider
是一个抽象类,主要定义了图片数据获取的接口load()
,从不同数据源获取图片需要不同的ImageProvider,比如AssetImage
,NetworkImage
Image
- 必选属性
image
,对应了一个ImageProvider - 在asset中加载图片
- 图片放到
/images/
下面 - 在
pubsepc.yaml
中添加图片路径 - 在
Image
的image
属性中加入AssetImage("<assetPath>")
, 或者使用Image.asset("<assetPath>", ...)
- 图片放到
- 在网路加载图片:在Image的image属性中加入
NetworkImage("<URL>")
,或者使用Image.network("<URL>", ...)
- 其他参数
width
,height
(如果不指定长宽则会根据parent大小限制尽可能显示图片原始大小),fit
(值fill, cover, contain, fitWidth, fitHeight, none,在不指定长宽的时候大小适应模式),color
,colorBlendMode
(混合模式,值是BlendMode.<attr>
),repeat
(值是ImageRepeat.<attr>
, 图片重复方式)
- 必选属性
- Flutter对加载过的图片会缓存在内存中
ICON
- 默认有Material Design的图标,需要在
pubsepc.yaml
中配置1
2flutter:
uses-material-design: true - 使用
Icon
组件加载图标:Icon(Icons.<attr>, color: Colors.<attr>)
- 默认有Material Design的图标,需要在
- Switch/Checkbox/Radio
Switch
和Checkbox
都是StatefulWidget,但是他们本身不保存当前选中的状态,选中状态由parent管理。当点击的时候会触发onChanged
回调改变逻辑- Switch和Checkbox都有
value
属性,代表当前状态,value初始值可以自选true和false, 在onChanged
回调中通过setState
改变状态 - Switch和Checkbox都有
activeColor
属性,代表选中的时候的状态 - Radio是互斥多选框中的一个,有
value
属性和groupValue
属性,如果这两个相同就回被选中,在onChanged
回调中通过setState
把groupValue修改成当前value。一般多个多选框可以用Column
和ListTile
(一行中有Text和ICON)完成
- TextField and Form
TextField
用于文本输入controller
属性用来获取/编辑文本内容,监听时间等,一般是显示提供,否则会自动重建inputDecoration
属性用来控制显示外观keyboardType
属性用书设置输入的键盘类型,比如TextInputType.number
,TextInputType.emailAddress
,TextInputType.text
,TextInputType.multiline
等style
控制文本样式,textAlign
控制文本水平对齐,autofocus
是否自动获取焦点,obscureText
控制是否隐藏文本,适用于密码等,maxLines
最大行数,默认1,如果是null就是没有行数限制,maxLength
和maxLengthEnforcement
控制最大长度和截断方式,toolbarOptions
控制长按或者右键出现的菜单,onChange
内容改变时的回调,可以通过controller完成,onEditingComplete
和onSubmitted
输入完毕时的回调- 一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23Column(
children: const <Widget>[
TextField(
// 自动获取焦点,会弹出键盘
autofocus: true,
// 输入文本的样式
decoration: InputDecoration(
labelText: "Username",
hintText: "Username or email",
// 前置图标
prefixIcon: Icon(Icons.person),
),
),
TextField(
decoration: InputDecoration(
labelText: "Password",
hintText: "Password",
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
),
],
) - 使用controller获取输入内容
1
2
3
4
5
6TextEditingController _controller = TextEditingController();
TextField(
controller: _controller,
)
// Get TextField content
print(_controller.text); - 监听文本变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Approach 1
TextField(
onChanged: (value) {
print("onChanged: ${value}");
}
)
// Approach 2
TextEditingController _controller = TextEditingController();
void initState() {
super.initState();
_controller.addListener(() {
print("addListener: ${_controller.text}");
});
} - 控制焦点变化:
FocusNode
管理焦点,FocusScopeNode
在输入框之间移动焦点,设置默认焦点- Step1:
创建不同的
FocusNode
实例,并且关联给不同输入框的focusNode
属性 - Step2:
FocusScope.of(context).requestFocus(<FocusNode实例>)
, 其中FocusScope.of(context)
获取了FocusScopeNode
实例 - 让输入框失去焦点:
focusNode实例.unfocus()
- 获取当前节点是否有焦点:
focusNode实例.hasFocus
- 获取有焦点的节点:
FocusScope.of(context).focusedChild
- Step1:
创建不同的
Form
autovalidate
: 对于表单每一个子内容输入是否校验,onWillPop
决定Form所在的路由是否直接返回,这个回调返回一个Future,通常用来拦截返回按钮,onChanged
如果任意一个表单子内容改变会触发这个回调- Form的子元素必须是
FormField
类型,这是一个抽象类,它的子类有TextFormField
,DropdownButtonFormField
等 FormState
是Form的State类,可以通过Form.of(context)
或者GlobalKey
获得FormState.validate()
用来调用每一个FormField
的validate
方法,如果有一个返回false,那么整个表单就会返回falseFormState.save()
用来调用每一个FormField
的save
方法,用来保存表单内容FormState.reset()
用来调用每一个FormField
的reset
方法,用来重置表单内容
- 对于每一个Form添加验证,可以通过
validator
属性,这个属性是一个回调函数,返回一个字符串,如果为空则表示验证通过,否则验证失败 - 对于表达提交验证
1
2
3
4// globalKey.currentState 拿到FormState,调用validate方法会调用每一个FormField的validate方法
if ((globalKey.currentState as FormState).validate()) {
// Submit form
}
- ProgressIndicator
- Material组件库中有
LinearProgressIndicator
和CircularProgressIndicator
两种进度指示器, 他们都有精确进度指示和模糊进度指示 LinearProgressIndicator
value
属性在[0,1] 之间,如果是null则表示模糊进度(一个循环动画)backgroundColor
属性表示进度条背景色valueColor
属性表示进度条颜色,它是一个Animation<Color>
类型,可以通过AlwaysStoppedAnimation(Colors.<颜色>)
来指定一个固定的颜色
CircularProgressIndicator
value
,backgroundColor
,valueColor
属性与LinearProgressIndicator
一致,strokeWidth
属性表示进度条的宽度,默认是4.0
- LinearProgressIndicator和CircularProgressIndicator都是用父容器的尺寸作为边界的,因此可以使用
SizedBox
当作parent,从而可以指定进度条大小的尺寸
- Material组件库中有
Layout Widgets
- 布局原理和约束
- Flutter有两种布局模型:
RenderBox
和RenderSiler
- 布局流程: 上层组件向下传递约束条件,下层组件确认自己的大小并告诉上层组件,上层组件确认下层组件相对于自身的偏移并确认自身的大小
- RenderBox模型中,Widget对应的渲染对象继承于
RenderBox
类,父组件传递给子组件的约束信息通过BoxConstraints
描述
BoxConstraints
- 默认属性:
minWidth =0.0
,maxWidth = double.infinity
,minHeight = 0.0
,maxHeight = double.infinity
- 其他方法:
BoxConstraints.tight(Size size)
生成固定宽高的限制,BoxConstraints.expand()
可以生成一个尽可能大的用以填充另一个容器的BoxConstraints
- 默认属性:
ConstrainedBox
: 用来对于子组件添加额外的约束。用法ConstrainedBox(constraints: BoxConstraints(<>), child:Container(<>))
SizedBox(width:<>, height: <>, child: <>)
UnconstrainedBox
: 一般A包含B,B包含C,会导致B把A的约束传给C,如果不想让A影响到C,那么需要B是UnconstrainedBox
,注意这样A仍然会影响B
- Flutter有两种布局模型:
- 线性布局(Row, Column)
- Row水平是主轴,Column垂直是主轴
MainAxisAlignment
是主轴对齐,CrossAxisAlignment
是交叉轴对齐Row
和Column
的属性textDirection = TextDirection.ltr/rtl
表示文字方向mainAxisSize = MainAxisSize.max/min
表示主轴占用的空间, 默认是max,也就是占满主轴空间MainAxisAlignment.start/center/end
具体取决于textDirection
的方向CrossAxisAlignment.start/center/end/stretch
表示交叉轴对齐方式, 取决于verticalDirection.up/down
的方向- 对于交叉轴,长度取决于最大子元素的长度
- 嵌套Row/Column:
对于嵌套的情况,只有最外层默认主轴占满,内层占用实际大小空间,如需要内层占满外层主轴,需要使用
Expanded
包裹内层
- 弹性布局(Flex)
- Row和Column都继承了Flex
Flex
可以和Expanded
配合实现弹性布局。Expanded只能作为Flex的孩子组件- 用法:
Flex(direction: Axis.horizontal/vertical, children: <Widget>[Expanded(flex: <int数字>, child: <Widget实现类>), ...])
Spacer(flex: <int数字>)
是Expanded的简化版,作用是占据空间,但是不显示任何内容
- 流式布局(Wrap, Flow)
- 对于线性布局如果超出屏幕范围会报错,对于流式布局超出屏幕范围会自动换行。
Wrap
和Flow
都是流式布局 Wrap
direction = Axis.horizontal/vertical
表示主轴方向alignment = WrapAlignment.start/center/end/spaceAround/spaceBetween/spaceEvenly
表示主轴对齐方式runAlignment = WrapAlignment.start/center/end/spaceAround/spaceBetween/spaceEvenly
表示交叉轴对齐方式'spacing = <double>
表示主轴方向子Widget的间距runSpacing = <double>
表示交叉轴方向子Widget的间距
Flow
- 一般首先考虑Wrap是否能满足对象,Flow很少使用,因为需要自己实现Widget的位置转换
- Flow主要用于一些需要自定义布局策略或者性能要求比较高的场景
- Flow性能好,对于子组件的尺寸和位置调整效率非常高
- Flow比较灵活,需要自己计算每一个组建的位置,可以自定义布局策略
- Flow使用比较复杂
- Flow不能自动适应子Widget的大小,需要通过指定父容器大小或者实现
TestFlowDelegate
的getSize(BoxConstraints constraints)
来指定大小
- 注意
ListTile(title, leading)
本身和Chip(avatar, label)
差不多,但是ListTile自己独占一行,Chip可以一行多个
- 对于线性布局如果超出屏幕范围会报错,对于流式布局超出屏幕范围会自动换行。
- 堆叠布局(Stack, Positioned)
- 层叠布局中子组件可以通过父容器四个角的位置来确定自身位置,这和Web中的绝对定位,Android中的Frame布局是类似的
- Flutter中使用
Stack
和Positioned
来实现绝对定位。Stack允许子组件堆叠,Positioned用于根据Stack的四个角来确定子组件的位置 Stack
alignment = AlignmentDirectional.top/left/right/bottom/Start
决定如何对齐没有使用Positioned或者部分定位的子组件textDirection = TextDirection.ltr/rtl
决定alignment对齐的参考系fit = StackFit.loose/expand
决定没有定位的子组件如何去适应Stack的大小,loose表示使用子组件的大小,expand表示扩伸到Stack的大小clipBehavior = Clip.hardEdge
等,决定对超出Stack显示空间的部分如何裁剪,hardEdge表示直接裁剪
Positioned
left/right/width
三选二,top/bottom/height
三选二
- 对齐和相对位置(Align)
Align
Align
可以简单的调整一个元素在父元素中的位置alignment = Alignment(x, y)
表示相对于父元素的位置, Alignment(0, 0)是中心,Alignment(-1, -1)是左上角,Alignment(1, 1)是右下角widthFactor/heightFactor
表示相对于父元素的宽高比例
Center
- Center继承自Align, 比Align少了一个alignment参数
- LayoutBuilder, AfterLayout
- 通过LayoutBuilder可以在布局过程中拿到父组件传递的约束信息,然后根据约束信息动态的构建不同的布局
- 比如可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局
1
2
3
4
5
6
7
8
9
10
11
12
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constrains.maxWidth < <constraint>) {
// do something;
} else {
// do something else;
}
},
);
} - AfterLayout可以在布局完成后执行回调,比如可以获取组件大小和相对于屏幕/父组件的位置坐标
- Flutter的build和layout是可以交错执行的,并不是严格的先执行build再执行layout
Container Widget
- 填充(Padding)
- Padding可以给其他子节点填充(空白),和边距效果类似
- 用法:
Padding(EdgeInsetsGeometry padding, Widget child)
, 一般来说使用EdgeInsets
, 因为他提供了一些设置填充的便捷方法 EdgeInset
便捷方法fromLTRB(double left, double top, double right, double bottom)
:分别指定四个方向的填充all(double value)
: 所有方向均使用相同数值的填充only({key: value})
: 指定某个方向的填充,可以有多个方向,key是LTRB中的一个symmetric({vertical, horizontal})
: 分别指定垂直方向和水平方向的填充
- 装饰容器(DecoratedBox)
- DecoratedBox可以在组件上绘制一些装饰,比如背景,边框,渐变等等
- 属性
decoration = BoxDecoration()
:装饰的样式,可以通过BoxDecoration来指定position = DecorationPosition.background/foreground
:指定装饰的绘制位置,bg是背景装饰,fg是前景装饰
BoxDecoration
是Decoration的子类,一般decoration属性都是用BoxDecoration。可以定义color
,image
,border
(对边用BorderSide
来描述),borderRadius
(这个只能用在统一border上),gradient
(渐变),backgroundBlendMode
,shape
等属性
- 变换(Transform)
- Transform可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效
Transform.translate(offset: Offset(x, y), child: child)
平移Transform.rotate(angle: f(math.pi), child: child)
旋转, 注意需要导入import 'dart:math' as math;
Transform.scale(scale: 1.5, child: child)
缩放- 注意的是Transform实在绘制阶段而不是布局(layout)阶段,所以无论子组件如何变化,其占用的空间大小和在屏幕中的位置是固定不变的,因为这些是在布局阶段确定的。比如Row的一个子组件经过了Transform变化,那么其他子组件的位置是不会变的,因为这些是在布局阶段确定的
RotatedBox
和Transform.rotate
功能类似,但是他是在布局阶段就确定了,所以会影响其他子组件的位置
- 容器组件(Container)
- Container是一个组合类容器,本身不对应具体的RenderObject,它是多个组件组合的多功能容器,只通过一个Container组件可以同时实现装饰,变换,限制的场景。
height
,width
优先于constraints
color
和decoration
不能同时存在margin
和padding
实际上都是通过padding
来实现的,只不过margin
是在父容器中,而padding
是在自身中
- 裁剪(Clip)
- 裁剪类组件:
ClipOval
,ClipRRect
,ClipRect
,ClipPath
- 通过继承
CustomClipper
并且重写getClip()
方法和shouldReclip()
方法
- 裁剪类组件:
- 页面骨架(Scaffold)
AppBar
导航栏骨架title
导航栏标题leading
导航栏左侧按钮,通常是抽屉按钮。如果设置了drawer
属性,则此属性默认为抽屉按钮actions
导航栏右侧菜单,比如分享链接bottom
导航栏底部菜单,通常是Tab按钮组elevation
导航栏阴影backgroundColor
导航栏背景颜色centerTitle
标题是否居中drawer
左侧抽屉菜单,endDrawer
右侧抽屉菜单
MyDrawer
抽屉菜单BottomNavigationBar
底部导航栏, 它的items
属性是一个List<BottomNavigationBarItem>
,每个BottomNavigationBarItem
都有icon
和title
属性FloatingActionButton
浮动按钮, 通过floatingActionButtonLocation
指定悬浮按钮在页面中的位置- 可以配合使用
BottomAppBar
和FloatingActionButton
来实现一个打洞的导航栏。bottomNavigationBar: BottomAppBar
,在BottomAppBar的shape
属性设定打洞的形状,比如CircularNotchedRectangle()
, 在child
属性中放一个空的SizedBox()
FloatingActionButton的floatingActionButtonLocation
属性设置在这个洞的位置即可 body
页面内容,是一个Widget
Slidable Widgets
- RenderSilver按需求加载列表布局
- 通常可滚动组建的子组件可能非常多,占用的总高度非常大,因为一般子组件不会一次性构建出来
- RenderSilver模型使用了基于Sliver的列表按需加载模型,只有当Silver出现在视觉中才会去构建它
- Flutter中的可滚动组件主要有三个角色构成:
Scrollable
,Viewport
,Silver
- Scrollable用户处理滑动手势,确定滑动便宜,滑动偏移变化时构建Viewport
- Viewport显示视窗,也就是列表的可视区域
- Silver视窗中显示的元素
Scrollable
axisDirection = AxisDirection.down/right
滑动方向physics
接受ScrollPhysics
对象,Flutter默认有ClampingScrollPhysics
滑到边界的时候不能继续滑动,BouncingScrollPhysics
iOS的弹性效果controller
接受ScrollController
对象,控制滚动位置和监听滚动事件,默认是PrimaryScrollController
viewportBuilder
用来构建Viewport的回调
Viewport
ViewportOffset offset
描述了窗口的偏移量,也就是应该显示的内容cacheExtent
和cacheExtentStyle
用来控制渲染长度,对于Viewport最终渲染区域长度是cacheExtent * viewport
Silver
: Silver对应的渲染对象是RenderSilver
,它的约束是SilverConstraints
- 可滚动组件通用配置:
scrollDirection
(主轴),reverse
(是否在scrollDirection上面反向滑动),controller
,physics
,cacheExtent
Scrollbar
child
属性对应可滚动组件- 这是Material风格的滚动条,对于iOS上面的Cupertino风格会自动替换成
CupertinoScrollbar
- SingleChildScrollView
- 一般在期望内容不超过屏幕太多的时候使用,否则性能很差
- ListView
- 最常用可滚动组件
- 常用参数
itemExtent
:子组件在滚动方向上的长度,如果为null则子组件自己决定长度,但是一般指定一个值prototypeItem
: 用来计算子组件长度,和itemExtent
不能同时存在shrinkWrap
: 是否根据子组件的总长度来设置ListView的长度,默认为false,如果容器是无边界的话,那么必须是truepadding
: 内边距children
: 子组件列表,适合子组件不多的情况,反之(子组件很多或者不确定数量)使用ListView.builder
itemBuilder
: 用来生成子组件separatorBuilder
: 用来生成分割线
ListView.builder(itemCount: <cnt>, itemExtent: <len>, itemBuilder: (context, index) =><Widget>)
用来生成子组件ListView.separated(itemCount: <cnt>, itemExtent: <len>, itemBuilder: (context, index) =><Widget>, separatorBuilder: (context, index) =><Widget>)
用来生成子组件和分割线、】- 一个无限列表加载的思路
- 定义一个loading提示和一个加载完毕的提示
- 在
itemBuilder
中最后一个元素的时候判断是否需要加载更多- 如果需要加载更多数据,调用函数加载数据,并且最后一个元素显示loading提示
- 如果不需要加载更多,显示加载完毕的提示
- 滚动监听和控制
- 使用
ScrollController
来控制可滚动组建的滚动位置 ScrollController(initialScrollOffset:<offset>, keepScrollOffset:<true/false>)
设定初始滚动位置和是否保存滚动位置- 滚动方法
jumpTo(double offset)
: 跳转到指定位置animateTo(double offset, ...)
: 动画的方式跳转到指定位置
- 滚动监听,因为ScrollController继承了
Listenable
,所以可以加一个监听controller.addListener(() => {})
- 一个显示返回按钮的滚动控制组件写法
- 定义显示按钮状态,初始为false
- 在
iniState
方法中添加监听,判断offset和显示状态,从而更改显示状态。 先super.initState()
,然后controller.addListener(() => {})
- 在
dispose
方法中移除监听,避免内存泄漏。先controller.dispose()
,然后super.dispose()
- 在
build
方法中根据显示状态显示按钮,按钮使用floatingActionButton
并且在点击的时候重置offset
- 保存滚动位置
PageStorage
可以用来存储滚动的位置- 对于不同的滚动组件,需要指定不同的
PageStorageKey
- 使用
ScrollBar
和NotificationListener
来进行滚动监听NotificationListener
可以在不同地方监听滚动事件,而ScrollController
只能在和具体的滚动组件关联之后才可以NotificationListener
可以收到更多的滚动事件(ScrollMetrics metrics
属性中包含了pixels
当前滚动位置,maxScrollExtent
最大滚动长度,atEdge
是否到底等),而ScrollController
只能获取当前滚动位置
- 使用
- AnimatedList
- 和ListView相似,但是可以再插入和删除节点时执行一个动画
- 添加和删除方法
insertItem(int index, {Duration duration})
,removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration})
itemBuilder: (context, index, animation) {}
用来在创建item的时候执行的动画,会被insertItem
调用,removeItem
里面会传入一个删除item的时候执行的动画的builder- 一个例子: 点击按钮添加新的item,点击item的删除icon删除这个icon
- 用一个counter记录当前数据的数量,用一个list保存初始数据,用一个GlobalKey用来在AnimatedList之外获取state然后调用插入和修改方法
- 插入: 修改counter,
修改数据list,
globalKey.currentState!.insertItem(data.length - 1)
- 删除: 修改counter,
修改数据list,
globalKey.currentState!.removeItem(index, builder: (context, animation) {return <Transition>} ...)
, 如果什么效果都不想要,直接return一个空的widget即可 - 重置list:首先清空数据列表,在循环中调用
removeItem(<args>)
,然后初始化数据列表,初始化counter,在循环中调用insertItem(<args>)
(相当于initState中循环多了insertItem的调用)
- GridView
- 常见布局类型,是实现了网格布局的组件。和ListView很多属性相同,
gridDelegate
用来控制子组件如何排列 SilverGridDelegate
是一个抽象类,定义了GridView布局的相关接口,子类需要通过实现它们来实现具体的布局算法。Flutter中提供了两个子类SliverGridDelegateWithFixedCrossAxisCount
,SliverGridDelegateWithMaxCrossAxisExtent
- 常见布局类型,是实现了网格布局的组件。和ListView很多属性相同,
- PageView与页面缓存
- 如果需要实现页面切换和Tab布局(Tab换页,图片轮动,上下滑动页面切换等),可以使用
PageView
组件 - PageView的
children
里面放的是不同的Widget(不同页面),设置不同滑动方向来进行页面切换
- 如果需要实现页面切换和Tab布局(Tab换页,图片轮动,上下滑动页面切换等),可以使用
- 可滚动组件子项缓存
- ListView中
addAutomaticKeepAlives: true
会为每一个表项添加AutomaticKeepAlive
父组件,从而将对应的RenderObject的keepAlive
标记成true和false,从而决定是否缓存 - 当keepAlive是false的时候,如果列表项滑出加载区域的时候,列表组件将会被销毁, 当是true的时候会缓存,列表进入加载区域的时候会首先检查缓存
- keepAlive从true修改成false的时候需要释放缓存
AutomaticKeepAlive
- Flutter中AutomaticKeepAlive设置组件keepAlive的时机由开发者决定。AutomaticKeepAlive相当于一个server,子组件相当于client,AutomaticKeepAlive收到子组件的统治之后会修改子组件的keepAlive状态,同时进行一些必要的资源清理工作
- 需要类混入
AutomaticKeepAliveClientMixin
,在build
方法中调用super.build(context)
(这里调用了AutomaticKeepAliveClientMixin的方法,根据wantKeepAlive的值给AutomaticKeepAlive发送信息让其工作),并且在wantKeepAlive
返回true
- ListView中
- TabBarView
- TabBarView提供了Tab布局组件,通常和TabBar配合使用
- TarBarView封装了PageView(可以有页面缓存),TabController用来监听和控制TabBarView的页面切换
- TabBar一般在AppBar最底部(AppBar的
bottom
属性),和TabBarView的controller共同使用的话,使用同一个TabController - 两种写法
- 在
initState
中初始化TabController,dispose
中释放资源,然后在build
中对应到TabBar和TabBarView - 直接在
build
中创建一个DefaultTabController
,它的child是一个页面,此时TabBar和TabBarView的controller不需要显示指定,它们会自动在组件树上向上查找饼使用最近的一个DefaultTabController
- 在
Navigation Widget
- 导航栏返回拦截(WillPopScope)
- 为了防止用户误触返回按钮导致APP退出可以设置返回按钮拦截,
WillPopScope
可以用来实现这一点 WillPopScope( WillPopCallback onWillPop, Widget child)
, 其中onWillPop
回调在点击返回按钮和物理返回按钮的时候触发,返回Future对象,只有Future最终是true的时候路由才会返回- 一个onWillPop的例子
1
2
3
4
5
6
7
8onWillPop: () async {
if (_lastPressedAt == null || DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
// Prevent user from leaving the page by accident
_lastPressedAt = DateTime.now();
return false;
}
return true;
} - 使用的时候
WillPopScope
的child设置成Scaffold
即可
- 为了防止用户误触返回按钮导致APP退出可以设置返回按钮拦截,
- InheritedWidget
- Inherited可以在Widget树中共享数据,比如Flutter SDK中的Theme和Locale就是通过InheritedWidget共享的
- 使用InheritedWidget可以直接在两个Widget之间共享数据,不需要在多层Widget的每一层之间都传递同一个数据
- 使用方法
- 自定义class继承
InheritedWidget
- 重写
of
,maybeOf
方法(返回值是否允许为null) - 重写
updateShouldNotify
方法,当数据发生变化的时候,返回true,此时子类会重新渲染。这个方法的参数类型改成自定义的class类型 - 在子类中使用
class.of(context).field
来获取继承的数据
- 自定义class继承
- 跨组件状态共享
- 对于跨足剑状态共享,一般通过各个组件共同的父元素来管理
EventBus
- 这是Flutter的全局事件总线
- 它是一个观察者模式:状态持有方更新,发布状态,状态使用方监听状态变化
- 缺点是需要手动定义每一个事件,然后再持有方和使用方手动注册和注销事件
Provider
- 思路是InheritedWidget保存跨组件共享的状态,子孙组件引用InheritedWidget来获取状态即可
- 保存数据:
class InheritedProvider<T> extends InheritedWidget
- 数据发生变化时通过
ChangeNotifier
来通知子孙组件:class Model extends ChangeNotifier
, 状态变化的时候调用notifyListeners()
- 结合:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget
- 颜色和主题
- 颜色
- 将颜色字符串转换成Color对象,16进制字符串比如
0xffdc380d
,直接Color(0xffdc380d)
即可,普通字符串比如var c = "dc380d"
,使用Color(int.parse(c,radix:16)|0xFF000000)
- 颜色亮度,通过
color.computeLuminance()
可以获取颜色的亮度,返回值是0-1之间的值,0是黑色,1是白色 MaterialColor
通过[]
表示颜色深度,index从50, 100到900,每100表示一种颜色深度,可以使用color.shade<index>
来获取具体颜色,比如Colors.blue.shade900
- 将颜色字符串转换成Color对象,16进制字符串比如
- 主题
Theme
可以为MaterialApp定义主题数据,Theme
内部使用InheritedWidget来共享样式数据- 主题数据在
ThemeData
中,按照Material Design规范,一些是固定的,一些是可以自定义的
ThemeData
中primarySwatch
是主题颜色的样本色,可以生成一些其他的属性- 使用主题的方法
Theme(data: ThemeData(<args>), child: <Widget>)
- 复制属性,并且修改一部分:
Theme.of(context).copyWith(<args>)
在copyWith中传入需要修改的属性即可
- 颜色
- 异步UI更新
FutureBuilder
和StreamBuilder
可以在异步任务完成时更新UIFutureBuilder
FutureBuilder(future:<Future>, initialData: <data>, builder: <builder>)
- builder是
Function (BuildContext context, AsyncSnapshot snapshot)
,它会在Future构建的不同阶段被多次调用。snapshot
含有异步任务的状态信息和结果信息 - 应用场景:网路请求数据的时候显示加载框,请求完成后显示数据
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// Return a Future<String> that will resolve after 2 seconds
Future<String> mockNetworkData() async {
return Future.delayed(Duration(seconds: 2), () => "Data from network");
}
Widget build(BuildContext context) {
return Center(
child: FutureBuilder<String>(
future: mockNetworkData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
// If network request is done
if (snapshot.connectionState == ConnectionState.done) {
// If request filed, show error
if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
// If request succeeded, show data
return Text("Contents: ${snapshot.data}");
}
} else {
// If network request is still ongoing, show loading
return CircularProgressIndicator();
}
},
),
);
}
StreamBuilder
- 构造函数,builder和FutureBuilder的格式是一样的
- 应用场景:可以接收多个异步操作的结果,常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Stream<int> getStreamData() {
return Stream.periodic(const Duration(seconds: 4), (i) => (i)).take(5);
}
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: getStreamData(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Empty Stream');
case ConnectionState.waiting:
return Text('Awaiting for data...');
case ConnectionState.active:
return Text('active: ${snapshot.data}');
case ConnectionState.done:
return Text('Stream closed');
}
},
);
}
- 对话框
- Material库中有不同的对话框,比如
AlertDialog
、SimpleDialog
等,它们都使用了Dialog
类 AlertDialog
- 属性
title
,titlePadding
,titleTextStyle
,content
,contentPadding
,contentTextStyle
,actions
(按钮),backgroundColor
,shape
- 关闭对话框:
Navigator.of(context).pop(<args>)
- 属性
showDialog()
- 用法:
Future<T?> showDialog(BuildContext context, WidgetBuilder builder, bool barrierDismissible = true)
。barrierDismissible表示点击对话框外部是否关闭对话框。如果点击对话框是点击外部关闭的话,返回值是null,否则返回的是Navigator.of(context).pop(<args>)
中的args - 用法:
自定义函数,返回Future<T?>类型,在函数中调用
showDialog<T>()
,在showDialog<T>()
的builder中返回对话框,对话框中不同按钮的Navigator.of(context).pop(<args>)
返回不同T类型值,或者null。在需要的Widget中调用自定义函数,使用await
关键字获取返回值
- 用法:
SimpleDialog
- 这个对话框会展示一个列表,用来进行不同选择。具体选项是
SimpleDialogOption
,选项们组成了children
属性 - 在自定义函数里面可以用switch case来处理不同选项的返回值:
switch(await SimpleDialog()) { case 1: ... }
- 这个对话框会展示一个列表,用来进行不同选择。具体选项是
- 日历选择器
- Material风格的日历选择器:
自定义函数,在其中返回
showDatePicker()
。这里涉及到DateTime的格式问题,如果只要日期的话用date.toString().split(' ')[0]
- iOS风格的日历选择器:
自定义函数,在其中返回
showCupertinoModalPopup()
,在showCupertinoModalPopup()
的builder中返回CupertinoDatePicker
- Material风格的日历选择器:
自定义函数,在其中返回
- Material库中有不同的对话框,比如
Event and Notification
- Pointer Event
- 完整的指针事件分成三个阶段:按下、移动、抬起,其他事件都是基于这些原始事件的
Listener
- 可以用Listener监听原始触摸事件
- Listener有
onPointerDown
,onPointerMove
,onPointerUp
回调,监听不同阶段的事件 PointerEvent
的属性position
(对全局坐标偏移),localPosition
(对本身布局坐标的偏移),delta
(对上一次事件的偏移),pressure
(压力值),orientation
(角度方向)
- 忽略指针事件:
IgnorePointer
,AbsorbPointer
这两个都会阻止子组件接收指针事件。IgnorePointer
使得本组件不接受指针事件,AbsorbPointer
允许本组件接受指针事件
- Gesture Detector
GestureDetector
是一个用于手势识别的功能性组件,可以通过它来识别各种手势。它内部封装了Listener- 点击事件:
onTap
点击,onDoubleTap
双击,onLongPress
长按。注意如果同时监听点击和双击,点击事件会有一个小延迟,因为要判断是否是双击。只监听点击事件的话没有延迟 - 拖动,滑动事件:
onPanDown
手指按下触发此事件,onPanUpdate
手指滑动触发此事件,onPanEnd
手指抬起触发此事件- 单一方向滑动可以用
onVerticalDragUpdate
和onHorizontalDragUpdate
onScaleUpdate(ScaleUpdateDetails details)
缩放事件details.scale.clamp(minScale, maxScale)
缩放倍数
- Flutter事件机制
- 事件处理流程
- 命中测试:事件发生的时候,对渲染树进行深度优先遍历,对每一个渲染对象进行命中测试,测试通过的组件会被添加到
HitTestResult
中 - 事件分发:命中测试完成后遍历HitTestResult列表,调用每一个对象的事件处理方法,这个过程叫做事件分发
- 时间清理:当事件结束或者取消的时候,会首先对时间进行分发,然后清空HitTestResult列表
- 命中测试:事件发生的时候,对渲染树进行深度优先遍历,对每一个渲染对象进行命中测试,测试通过的组件会被添加到
- 如果父子组件都监听了同一个事件,子组件会先响应,因为是DFS遍历的
- 同一个组建的多个GestureDetector是竞争关系的。解决冲突方法:使用Listener,自定义手势识别器(Recognizer)
- 事件处理流程
- 事件总线
- 事件总线通过订阅者模式可以用来进行跨页面广播
- 通知Notification
- Widget树中每一个节点都可以分发通知,通知沿着当前节点向上传递。所有父节点都可以通过
NotificationListener
来监听通知。这叫做通知冒泡。 - 通知冒泡可以终止,但是用户触摸事件冒泡不可以终止
- 自定义通知:自定义类继承
NOtification
类,使用dispatch(context)
分发通知,使用NotificationListener
监听通知,NotificationListener
的onNotification
方法返回值为bool
,如果返回true
则阻止冒泡,返回false
则继续冒泡
- Widget树中每一个节点都可以分发通知,通知沿着当前节点向上传递。所有父节点都可以通过
Animation
- 动画简介
- 动画原理: 一段时间内多次更改UI外观,从而达到动画效果。流畅度由FPS(Frame Per Second)决定,一般32FPS以上就可以了
- Flutter中动画抽象
- Flutter中动画主要涉及
Animation
,Curve
,Controller
,Tween
等 Animation
- 主要是用来保存动画的插值和状态,本身和UI渲染没有关系,在一段时间内依次生成一个区间
- 常用的是
Animation<double>
- 通过
Animation
对象的value
获取动画当前的状态值 - 通过Animation监听动画每一帧的执行状态变化
addListener()
,addStatusListener()
Curve
- 用来描述动画过程,比如线性和非线性
- 状态
Curve.<value>
, value可以是linear, decelerate(匀减速), ease(先加速后减速), easeln(先慢后快), easeOut(先快后慢), easeLnOut(先慢后加速再减速)
AnimationController
- 用来控制动画播放,比如
forward()
开始,stop()
,reverse()
反向播放等 AnimationController(duration: Duration(<args>), vsync: this)
- 默认在duration内从0线性到1,可以用
lowerBound
,upperBound
来控制 vsync
是一个TickerProvider
对象,可以创建Ticker
- 用来控制动画播放,比如
Tween
- 配合
AnimationController
使用,可以把其对象值从0.0到1.0修改成执行范围 - 用法
1
2
3
4
5
6final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
// 在500毫秒内生成0到255的整数值
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
- 配合
- Flutter中动画主要涉及
File Processing and Network Request
- 文件操作
- Dart IO库把敖汉了文件读写的相关类,它属于Dart语法标准的一部分。Dart VM下的脚本和Flutter都是通过Dart IO库来操作文件的
- APP目录:
Android和iOS的存储目录不同,可以使用插件
PathProvider
插件提供了一种平台透明的方式来访问文件系统上的常用位置(在pubspec.yaml
中声明path_provider:^<version>
)- 临时目录:
getTemporaryDirectory()
- 文档目录:
getApplicationDocumentsDirectory()
- 外部存储目录:
getExternalStorageDirectory()
- 临时目录:
File
,Directory
等类都是源于dart:io
库,并且都是异步操作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
36Future<File> getFile() async {
// getApplicationDocumentsDirectory返回了一个Directory对象
String path = (await getApplicationDocumentsDirectory()).path;
return File('$path/counter.txt');
}
Future<int> readCounter() async {
// 因为File可能不存在所以需要try catch
try {
File file = await getFile();
// 读取文件内容
String contents = await file.readAsString();
return int.parse(contents);
} on FileSystemException {
return 0;
}
}
Future<void> setCounter() async {
setState(() {
_counter += 1;
});
await (await getFile()).writeAsString('$_counter');
}
void initState() {
super.initState();
setState(() {
getCounter().then((value) {
setState(() {
_counter = value;
});
});
});
}