异常
原生平台的任务采用多线程调度,当一个线程出现未捕获的异常时,会导致整个进程退出。
Dart 单线程,任务采用事件循环调度,Dart 异常并不会导致应用程序崩溃,取而代之的是当前事件后续的代码不会被执行了。
这样带来的好处是一些无关紧要的异常不会闪退,坏处是这些异常可能没有明显的提示和异常表现,导致问题容易被隐藏。
同步异常
try {
String abc;
print("abc's length ${abc.length}");
} catch (error, stacktrace) {
//todo catch all error
}
异步异常
Future.delayed(Duration(seconds: 1), () {
throw '123';
}).then((value) {
print('value $value');
return value;
}).catchError((error, stack) {
print('error $error stack $stack');
});
全局异常捕获
一般布局中的异常,为UI 异常,可在
FlutterError.onError = (FlutterErrorDetails details) {
};
中捕获;
非 UI 绘制流程中的同步异常,可以全局捕获未 catch 的异常,包括异步异常:
# main.dart
runZoned(() {
runApp(MyApp());
}, onError: _handleError);
void _handleError(Object obj, StackTrace stack) {
// todo global uncaught exception handle
print('zone _handleError $obj stack $stack');
}
参考:https://juejin.cn/post/6906274131394691085#heading-1
async & await
在Flutter中,异步编程允许我们在应用程序中执行一些可能会阻塞UI线程的耗时操作,而不会使应用程序看起来冻结。async和await是两个用于实现异步编程的关键字。下面是它们的使用说明:
async关键字
async关键字用于标记一个函数是异步函数,即函数的执行过程中可能会发生异步操作。异步函数会返回一个Future对象,表示异步操作的结果。异步函数可以使用await关键字来等待异步操作的完成。
示例:
Future<String> fetchData() async {
// 异步操作
return "Hello World";
}
上述示例中,fetchData函数是一个异步函数,它返回一个Future对象,表示异步操作的结果。在函数体中,我们可以进行一些异步操作,例如发起HTTP请求等。
await关键字
await关键字用于等待异步操作的完成。当我们使用await关键字来调用一个异步函数时,Dart会暂停当前函数的执行,等待异步操作完成后再继续执行。
示例:
Future<String> fetchData() async {
// 异步操作
return "Hello World";
}
void main() async {
String data = await fetchData();
print(data);
}
上述示例中,我们使用await关键字来等待fetchData函数的执行结果。在main函数中,我们使用await关键字调用fetchData函数,并将返回值赋值给变量data。由于await会阻塞当前函数的执行,因此我们需要在main函数前添加async关键字,以标记该函数是一个异步函数。
需要注意的是,await关键字只能在异步函数中使用。如果在非异步函数中使用await关键字,编译器会报错。
如果一个异步函数不需要返回值,可以将函数返回类型设置为Future或者直接省略返回类型:
Future<void> doSomethingAsync() async {
// 异步操作
}
// 或者
Future doSomethingAsync() async {
// 异步操作
}
await只用于异步函数
在Dart中,await关键字只能在异步函数中使用,这是因为await关键字会阻塞当前函数的执行,等待异步操作的完成。由于Dart是单线程语言,如果在同步函数中使用await关键字会导致当前线程阻塞,使应用程序看起来像是冻结了一样,用户体验非常差。因此,Dart要求我们只能在异步函数中使用await关键字。
下面是一个示例,展示了在同步函数中使用await关键字会发生什么:
void doSomething() {
// 异步操作
String data = await fetchData(); // 编译错误
print(data);
}
Future<String> fetchData() async {
// 异步操作
return "Hello World";
}
在上述示例中,我们在同步函数doSomething中使用了await关键字。由于await关键字只能在异步函数中使用,因此编译器会报错,提示我们在同步函数中无法使用await关键字。
正确的方式应该是将doSomething函数改为异步函数,并使用await关键字等待fetchData函数的执行结果:
Future<void> doSomething() async {
// 异步操作
String data = await fetchData();
print(data);
}
Future<String> fetchData() async {
// 异步操作
return "Hello World";
}
在上述示例中,我们将doSomething函数改为异步函数,并使用await关键字等待fetchData函数的执行结果。由于await关键字只能在异步函数中使用,因此在异步函数doSomething中使用await关键字是合法的。
异步的线程切换
flutter中使用async异步函数,需要考虑该异步函数操作完成后切线程的问题吗?
在Flutter中,使用async异步函数不需要特别考虑该异步函数操作完成后切线程的问题。这是因为在使用async/await语法时,Dart会自动将异步任务加入到事件循环队列中,并在异步任务完成后切回原线程执行后续代码。
具体来说,当我们使用await关键字等待异步操作完成时,Dart会将当前函数挂起,并返回一个Future对象,告诉事件循环队列该异步任务需要等待。事件循环队列会不断地从队列中取出任务,并执行它们。当异步任务完成时,它会产生一个事件,告诉事件循环队列该异步任务已完成。事件循环队列会根据事件类型决定该任务需要切换到哪个线程执行,然后执行异步任务的回调函数。回调函数执行完毕后,事件循环队列会将控制权返回给原线程,继续执行后续代码。
因此,在Flutter中,我们可以放心地使用async异步函数,不需要特别考虑该异步函数操作完成后切线程的问题。Dart会自动处理线程切换,使异步操作的执行不会影响应用程序的性能和用户体验。
总结
在Flutter中,async和await是两个用于实现异步编程的关键字。使用async关键字标记一个函数为异步函数,使用await关键字等待异步操作的完成。异步函数会返回一个Future对象,表示异步操作的结果。异步编程是Flutter应用程序中非常重要的一部分,它可以提高应用程序的性能和用户体验。
Future
Dart类库有非常多的返回Future
或者Stream
对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。
Future
的所有API的返回值仍然是一个Future
对象,所以可以很方便的进行链式调用。
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
then
方法还有一个可选参数onError
,我们也可以用它来捕获异常:
Future.delayed(Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作。Future.wait
可以接受一个Future
数组参数,只有数组中所有Future
都执行成功后,才会触发then
的成功回调,只要有一个Future
执行失败,就会触发错误回调。
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
执行上面代码,4秒后你会在控制台中看到“hello world”。
Stream
和 Future
不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
输出:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
Mixin
在 Dart 中,使用 with
关键字将一个 Mixin 类添加到其他类中。例如:
mixin MyMixin {
void doSomething() {
print("Do something in MyMixin");
}
}
class MyClass with MyMixin {
void doSomething() {
super.doSomething();
print("Do something in MyClass");
}
}
void main() {
var obj = MyClass();
obj.doSomething();
}
在上面的示例中,MyMixin
是一个 Mixin 类,MyClass
使用 with
关键字将 MyMixin
添加到自己的实现中。当调用 doSomething()
方法时,MyClass
中的实现将覆盖 MyMixin
中的实现。可以通过使用 super
关键字在 MyClass
中调用 MyMixin
中的实现。
如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。
绘制四棵树
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自
Element
类。 - 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自
RenderObject
类。 - 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自
Layer
类。
三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。
自定义Widget
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
}):super(key:key);
final String text;
final Color backgroundColor;
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
context
build
方法有一个context
参数,它是BuildContext
类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context
是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。
// 在 widget 树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
widget 树中获取State对象
通过Context获取
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of
静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of
方法。这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold
也提供了一个of
方法,我们其实是可以直接调用它的:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单2'),
);
})
GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
_globalKey.currentState.openDrawer()
声明周期
状态管理Provider
基本使用
创建数据模型:
import 'package:flutter/material.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void incrementCount() {
_count++;
notifyListeners();
}
}
使用Provider包装应用程序以便在整个应用程序中共享数据:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
final counterModel = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${counterModel.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counterModel.incrementCount(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Consumer
Provider是Flutter中一种流行的状态管理框架,它可以让我们方便地在组件之间共享数据。但是,在Provider中使用Provider.of(context)来获取共享数据会在所有子部件中引入一个监听器,这样会导致应用性能下降。
为了解决这个问题,Provider提供了一个Consumer组件,它可以让我们更加方便地使用共享数据,同时避免引入过多的监听器。
Consumer组件是一个小部件,它接收一个builder函数作为参数,只有在数据发生变化时才会重新构建这个小部件和它的子部件。这样,我们就可以将共享数据放在builder函数中,只有在数据发生变化时才更新需要更新的小部件,从而避免在所有子部件中引入监听器的问题。
例如,在下面的代码中,我们使用Consumer组件来更新计数器的值:
Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Text('${counterModel.count}');
},
)
在这个例子中,当CounterModel发生变化时,只有Text小部件会被重新构建。这可以帮助我们避免在所有子部件中引入监听器,从而提高应用程序的性能。
因此,Consumer组件可以帮助我们更加方便地使用共享数据,并避免引入过多的监听器。它在Provider中扮演着重要的角色,为我们提供了一种高效的状态管理方式。
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Text('Counter: ${counterModel.count}');
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Text(
'${counterModel.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Provider.of<CounterModel>(context, listen: false).incrementCount(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
),
);
}
}
多Provider嵌套
MultiProvider(
providers: [
Provider(create: (_) => CounterModel1()),
Provider(create: (_) => CounterModel2()),
],
child: MyApp(),
)
路由
简单跳页
Navigator.push(context, MaterialPageRoute(builder: (context){
return MyHomePage(title: "");
}));
Navigator.push(BuildContext context, Route route)
等同Navigator.of(context).push(Route route)
路由传值
带参跳转&取值:
class RouterTestRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () async {
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "我是提示xxxx",
);
},
),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");
},
child: Text("打开提示页"),
),
);
}
}
反值:
ElevatedButton(
onPressed: () => Navigator.pop(context, "我是返回值"),
child: Text("返回"),
)
命名路由
格式:
Map<String, WidgetBuilder> routes;
例子:
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名为"/"的路由作为应用的home(首页)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);
通过路由名称来打开新路由:
Future pushNamed(BuildContext context, String routeName,{Object arguments})
传参:
Navigator.of(context).pushNamed("new_page", arguments: "hi");
class EchoRoute extends StatelessWidget {
Widget build(BuildContext context) {
//获取路由参数
var args=ModalRoute.of(context).settings.arguments;
//...省略无关代码
}
}
适配带参路由注册到路由表中:
MaterialApp(
... //省略无关代码
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
},
},
);
路由生成钩子
MaterialApp
有一个onGenerateRoute
属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)
打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder
函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute
来生成路由。onGenerateRoute
回调签名如下:
Route<dynamic> Function(RouteSettings settings)
有了onGenerateRoute
回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute
回调,然后在该回调中进行统一的权限控制,如:
MaterialApp(
... //省略无关代码
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其他情况则正常打开路由。
}
);
}
);
注意,
onGenerateRoute
只会对命名路由生效。
控件
Text
Text("Hello world",
textAlign: TextAlign.left,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textScaleFactor: 1.5,
style: TextStyle(
color: Colors.blue,
fontSize: 18.0,
height: 1.2,
fontFamily: "Courier",
background: Paint()..color=Colors.yellow,
decoration:TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed
),
);
Text.rich(TextSpan(
children: [
TextSpan(
text: "Home: "
),
TextSpan(
text: "https://flutterchina.club",
style: TextStyle(
color: Colors.blue
),
recognizer: _tapRecognizer
),
]
))
按钮:
ElevatedButton.icon(
icon: Icon(Icons.send),
label: Text("发送"),
onPressed: _onPressed,
),
OutlineButton.icon(
icon: Icon(Icons.add),
label: Text("添加"),
onPressed: _onPressed,
),
TextButton.icon(
icon: Icon(Icons.info),
label: Text("详情"),
onPressed: _onPressed,
),
Image
Image(
image: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
image: AssetImage("images/avatar.png"),
width: 100.0,
color: Colors.blue,
colorBlendMode: BlendMode.difference,
);
单选开关&复选框
Switch(
value: _switchSelected,//当前状态
onChanged:(value){
//重新构建页面
setState(() {
_switchSelected=value;
});
},
),
Checkbox(
value: _checkboxSelected,
activeColor: Colors.red, //选中时的颜色
onChanged:(value){
setState(() {
_checkboxSelected=value;
});
} ,
)
TextField
FocusNode focusNode1 = FocusNode();
TextEditingController _textEditingController = new TextEditingController();
TextField(
autofocus: true,
obscureText: true,
focusNode: focusNode1,
controller: _textEditingController,
decoration:
InputDecoration(labelText: "密码", hintText: "您的登录密码", prefix: Icon(Icons.person),
// 未获得焦点下划线设为灰色
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
//获得焦点下划线设为蓝色
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
),
),
onChanged: (v) {
print("onChange: $v");
},
)
进度条
// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%,会显示一个半圆
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
),
布局
布局类组件基类:
Widget | 说明 | 用途 |
---|---|---|
LeafRenderObjectWidget | 非容器类组件基类 | Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。 |
SingleChildRenderObjectWidget | 单子组件基类 | 包含一个子Widget,如:ConstrainedBox、DecoratedBox等 |
MultiChildRenderObjectWidget | 多子组件基类 | 包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等 |
Flutter 中有两种布局模型:
- 基于 RenderBox 的盒模型布局。
- 基于 Sliver ( RenderSliver ) 按需加载列表布局。
布局流程如下:
- 上层组件向下层组件传递约束(constraints)条件。
- 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
- 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
因为任何时候子组件都必须先遵守父组件的约束。
ConstrainedBox
用于对子组件添加额外的约束。
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity, //宽度尽可能大
minHeight: 50.0 //最小高度为50像素
),
child: Container(
height: 5.0,
child: redBox ,
),
)
UnconstrainedBox
- B 组件中在布局 C 时不约束C(可以为无限大)。
- C 根据自身真实的空间占用来确定自身的大小。
- B 在遵守 A 的约束前提下结合子组件的大小确定自身大小。
而这个 B组件就是 UnconstrainedBox
组件。
AppBar(
title: Text(title),
actions: <Widget>[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
)
],
)
我们会发现右侧loading按钮大小并没有发生变化!
AppBar(
title: Text(title),
actions: <Widget>[
UnconstrainedBox(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
)
],
)
,需要注意,UnconstrainedBox 虽然在其子组件布局时可以取消约束(子组件可以为无限大),但是 UnconstrainedBox 自身是受其父组件约束的,所以当 UnconstrainedBox 随着其子组件变大后,如果UnconstrainedBox 的大小超过它父组件约束时,也会导致溢出报错。
除了上面介绍的这些常用的尺寸限制类容器外,还有一些其他的尺寸限制类容器,比如AspectRatio
,它可以指定子组件的长宽比、LimitedBox
用于指定最大宽高、FractionallySizedBox
可以根据父容器宽高的百分比来设置子组件宽高等,由于这些容器使用起来都比较简单,我们便不再赘述,读者可以自行了解。
Row & Column
Container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
children: <Widget>[
Container(
color: Colors.red,
child: Column(
mainAxisSize: MainAxisSize.max,//无效,内层Colum高度为实际高度
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
)
],
),
),
);
特殊情况
如果Row
里面嵌套Row
,或者Column
里面再嵌套Column
,那么只有最外面的Row
或Column
会占用尽可能大的空间,里面Row
或Column
所占用的空间为实际大小。可用Expanded包裹。
Expanded
Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex
子组件所占用的空间。因为 Row
和Column
都继承自 Flex,所以 Expanded 也可以作为它们的孩子。
const Expanded({
int flex = 1,
required Widget child,
})
所有的Expanded
按照其 flex 的比例来分割主轴的全部空闲空间。
流式布局(Wrap、Flow)
我们把超出屏幕显示范围会自动折行的布局称为流式布局。
Wrap(
spacing: 8.0, // 主轴(水平)方向间距
runSpacing: 4.0, // 纵轴(垂直)方向间距
alignment: WrapAlignment.center, //沿主轴方向居中
children: <Widget>[
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
label: Text('Hamilton'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: Text('Lafayette'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
label: Text('Mulligan'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
label: Text('Laurens'),
),
],
)
层叠布局(Stack、Positioned)
//通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
alignment:Alignment.center, //指定未定位或部分定位widget的对齐方式
fit: StackFit.expand, //StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小。
clipBehavior: Clip.hardEdge, //此属性决定对超出Stack显示空间的部分如何剪裁
children: <Widget>[
Container(
child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
);
对齐与相对定位(Align)
Align
如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align
组件会更简单一些。
Align
组件可以调整子组件的位置,定义如下:
Align(
widthFactor: 2,
heightFactor: 2,
alignment: Alignment.topRight,
child: FlutterLogo(
size: 60,
),
),
widthFactor
和heightFactor
是用于确定Align
组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align
组件的宽高。如果值为null
,则组件的宽高将会占用尽可能多的空间。
Alignment
//右上角
static const Alignment topRight = Alignment(1.0, -1.0);
Alignment
Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)
。Alignment(-1.0, -1.0)
代表矩形的左侧顶点,而Alignment(1.0, 1.0)
代表右侧底部终点,而Alignment(1.0, -1.0)
则正是右侧顶点,即Alignment.topRight
。
Alignment
可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:
(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)
其中childWidth
为子元素的宽度,childHeight
为子元素高度。
FractionalOffset
FractionalOffset
继承自 Alignment
,它和 Alignment
唯一的区别就是坐标原点不同!FractionalOffset
的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset
的坐标转换公式为:
实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)
Container(
height: 120.0,
width: 120.0,
color: Colors.blue[50],
child: Align(
alignment: FractionalOffset(0.2, 0.6),
child: FlutterLogo(
size: 60,
),
),
)
Align和Stack对比
可以看到,Align
和Stack
/Positioned
都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:
- 定位参考系统不同;
Stack
/Positioned
定位的的参考系可以是父容器矩形的四个顶点;而Align
则需要先通过alignment
参数来确定坐标原点,不同的alignment
会对应不同原点,最终的偏移是需要通过alignment
的转换公式来计算出。 Stack
可以有多个子元素,并且子元素可以堆叠,而Align
只能有一个子元素,不存在堆叠。
Center组件
Center
继承自Align
,它比Align
只少了一个alignment
参数;由于Align
的构造函数中alignment
值为Alignment.center
,所以,我们可以认为Center
组件其实是对齐方式确定(Alignment.center
)了的Align
。
LayoutBuilder、AfterLayout待看
容器类组件
装饰容器(DecoratedBox)
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors:[Colors.red,Colors.orange.shade700]), //背景渐变
borderRadius: BorderRadius.circular(3.0), //3像素圆角
boxShadow: [ //阴影
BoxShadow(
color:Colors.black54,
offset: Offset(2.0,2.0),
blurRadius: 4.0
)
]
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
child: Text("Login", style: TextStyle(color: Colors.white),),
)
)
变换(Transform)& RotatedBox
Container
Container(
margin: EdgeInsets.only(top: 50.0, left: 120.0),
padding: EdgeInsets.all(20.0), //容器内补白
constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0),//卡片大小
decoration: BoxDecoration( //背景装饰
gradient: RadialGradient( //背景径向渐变
colors: [Colors.red, Colors.orange],
center: Alignment.topLeft,
radius: .98,
),
boxShadow: [
//卡片阴影
BoxShadow(
color: Colors.black54,
offset: Offset(2.0, 2.0),
blurRadius: 4.0,
)
],
),
transform: Matrix4.rotationZ(.2),//卡片倾斜变换
alignment: Alignment.center, //卡片内文字居中
child: Text(
//卡片文字
"5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),
),
)
剪裁类组clip
ClipOval(child: avatar), //剪裁为圆形
ClipRRect( //剪裁为圆角矩形
borderRadius: BorderRadius.circular(5.0),
child: avatar,
),
空间适配(FittedBox)
实例:单行缩放布局
class SingleLineFittedBox extends StatelessWidget {
const SingleLineFittedBox({Key? key,this.child}) : super(key: key);
final Widget? child;
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FittedBox(
child: ConstrainedBox(
constraints: constraints.copyWith(
minWidth: constraints.maxWidth,
maxWidth: double.infinity,
//maxWidth: constraints.maxWidth
),
child: child,
),
);
},
);
}
}
页面骨架(Scaffold)
build(BuildContext context) {
return Scaffold(
appBar: AppBar( //导航栏
title: Text("App Name"),
actions: <Widget>[ //导航栏右侧菜单
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
),
drawer: MyDrawer(), //抽屉
bottomNavigationBar: BottomNavigationBar( // 底部导航
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: _onItemTapped,
),
floatingActionButton: FloatingActionButton( //悬浮按钮
child: Icon(Icons.add),
onPressed:_onAdd
),
);
}
Widget
Sliver布局模型
只有当 Sliver 出现在视口中时才会去构建它,这种模型也称为“基于Sliver的列表按需加载模型”。
Sliver 的主要作用是配合:加载子组件并确定每一个子组件的布局和绘制信息,如果 Sliver 可以包含多个子组件时,通常会实现按需加载模型。
可滚动组件中有很多都支持基于Sliver的按需加载模型,如ListView
、GridView
,但是也有不支持该模型的,如SingleChildScrollView
。
Flutter Web
Flutter 中提供了两种不同的渲染器来运行和构建 Web 应用,分别是 html 渲染器和 CanvasKit 渲染器。
1. Html渲染器
应用体积相对较小,缺点是使用Html渲染器时大多数 UI 并不是 Flutter 引擎绘制的,所以可能会存在跨浏览器跨时UI出现不一致的情况。
2. CanvasKit 渲染器
保证跨端UI绘制的一致性,有更好的性能;包大2m;
run
和 build
命令:
flutter run -d chrome --web-renderer html
flutter build web --web-renderer canvaskit
可滚动组件
可滚动组件主要由三个角色组成:Scrollable、Viewport 和 Sliver:
- Scrollable :用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport 。
- Viewport:显示的视窗,即列表的可视区域,用于渲染当前视口中需要显示 Sliver;
- Sliver:视窗里显示的元素。
Scrollbar
要给可滚动组件添加滚动条,只需将Scrollbar
作为可滚动组件的任意一个父级组件即可,如:
Scrollbar(
child: SingleChildScrollView(
...
),
);
SingleChildScrollView
Android中的ScrollView
,它只能接收一个子组件
ListView
ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
],
);
shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView
的长度,默认值为false
。默认情况下,ListView
会在滚动方向尽可能多的占用空间。当ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
。
ListView.builder
ListView.builder
适合列表项比较多或者列表项不确定的情况,下面看一下ListView.builder
的核心参数列表:
ListView.builder(
itemCount: 100,
itemExtent: 50.0, //强制高度为50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);
ListView.separated
ListView.separated
可以在生成的列表项之间添加一个分割组件,它比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割组件生成器。
下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线。
class ListView3 extends StatelessWidget {
Widget build(BuildContext context) {
//下划线widget预定义以供复用。
Widget divider1=Divider(color: Colors.blue,);
Widget divider2=Divider(color: Colors.green);
return ListView.separated(
itemCount: 100,
//列表项构造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器构造器
separatorBuilder: (BuildContext context, int index) {
return index%2==0?divider1:divider2;
},
);
}
}
注意:
- ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。
- 一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。
- ListView 的 Sliver 默认是 SliverList,如果指定了
itemExtent
,则会使用 SliverFixedExtentList;如果prototypeItem
属性不为空,则会使用 SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。
无限加载列表
https://book.flutterchina.club/chapter6/listview.html#_6-3-6-%E5%AE%9E%E4%BE%8B-%E6%97%A0%E9%99%90%E5%8A%A0%E8%BD%BD%E5%88%97%E8%A1%A8
GestureDetector
支持点击、双击、长按、拖动、缩放
class _GestureTestState extends State<GestureTest> {
String _operation = "No Gesture detected!"; //保存事件名
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 200.0,
height: 100.0,
child: Text(
_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"), //点击
onDoubleTap: () => updateText("DoubleTap"), //双击
onLongPress: () => updateText("LongPress"), //长按
),
);
}
void updateText(String text) {
//更新显示的事件名
setState(() {
_operation = text;
});
}
}