Flutter了解续
目录
1. 模版示例(计数器)
2. Widget
3. 路由管理
4. 资源管理
Scaffold
1. 模版示例(计数器)
Android Studio和VS Code创建的Flutter应用模板默认是一个简单的计数器示例。
每点击一次右下角带“+”号的悬浮按钮,屏幕中央的数字就会加1。
lib目录下的main.dart文件(入口文件)
// import:导入包
// 导入Material UI组件库
import 'package:flutter/material.dart';
// 应用入口,启动Flutter应用后会调用
void main() {
// runApp方法接受一个根Widget组件参数。
// 框架强制根widget覆盖整个屏幕。
runApp(MyApp());
}
// 根组件
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// MaterialApp 是Material库中提供的Flutter APP框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。 该widget在应用程序的根部创建了一些有用的widget,其中包括一个Navigator, 它管理由字符串标识的Widget栈(即页面路由栈)。Navigator可以让应用程序在页面之间的平滑的过渡。
// MaterialApp也是一个widget。是否使用MaterialApp是可选的,推荐使用。使用了MaterialApp则必须使用Scaffold。
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// 主题色设置为蓝色,可以看到应用的导航栏颜色为蓝色
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// 应用首页路由,也是一个widget。
home: MyHomePage(title: 'Hello Home Page'),
);
}
}
// 首页,继承自StatefulWidget类,表示它是一个有状态的组件。
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
// _MyHomePageState类是MyHomePage类对应的状态类。和MyApp 类不同, MyHomePage类中并没有build方法,取而代之的是,build方法被挪到了_MyHomePageState方法中。
_MyHomePageState createState() => _MyHomePageState();
}
// State状态类
class _MyHomePageState extends State<MyHomePage> {
// 该组件的状态
// 由于需要维护一个点击次数计数器,所以定义一个_counter状态用于记录按钮点击的总次数
int _counter = 0;
// 设置状态的自增函数。点击按钮后会调用它。
// 当按钮点击时,会调用此函数,该函数的作用是先自增_counter,然后调用setState 方法。setState方法的作用是通知Flutter框架,有状态发生了改变,Flutter框架收到通知后,会执行build方法来根据新的状态重新构建界面, Flutter 对此方法做了优化,使重新执行变的很快,所以你可以重新构建任何需要更新的东西,而无需分别去修改各个widget。
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
// Flutter在构建页面时,会调用组件的build方法,widget的主要工作是提供一个build()方法来描述如何构建UI界面(通常是通过组合、拼装其它基础widget)。
// 构建UI界面的逻辑在build方法中,当MyHomePage第一次创建时,_MyHomePageState类会被创建,当初始化完成后,Flutter框架会调用Widget的build方法来构建widget树,最终将widget树渲染到设备屏幕上。
// 调用setState后重新调用
Widget build(BuildContext context) {
// Scaffold 是 Material 库中提供的页面脚手架,它提供了默认的导航栏、标题和包含主屏幕widget树(“组件树”或“部件树”)的body属性。
return Scaffold(
appBar: AppBar( // 导航栏
title: Text(widget.title),
),
// body的组件树中包含了一个Center 组件,Center 可以将其子组件树对齐到屏幕中心。
// 此例中, Center 子组件是一个Column 组件,Column的作用是将其所有子组件沿屏幕垂直方向依次排列; 此例中Column子组件是两个 Text,第一个Text 显示固定文本 “You have pushed the button this many times:”,第二个Text 显示_counter状态的数值。
body: Center( // 主体UI
// 一个layout widget,垂直排列。默认情况下,它会调整自身大小以适应
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
// floatingActionButton是页面右下角的带“+”的悬浮按钮,它的onPressed属性接受一个回调函数,代表它被点击后的处理器,本例中直接将_incrementCounter方法作为其处理函数。
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // 后面的逗号对于后期自动格式化排版更好。
);
}
}
根目录下的pubspec.yaml文件
#项目名
name: flutter_textfield_app
#项目描述
description: A new Flutter project.
#避免通过pub publish发布到pub.dev上。表明这是个私有项目。
publish_to: 'none'
#版本名和版本号通过+分开
#通过指定 --build-name 和 --build-number 来设置版本名、版本号
#在Android中,build name用作versionName,而build number用作versionCode。
#在iOS中,内部版本名用作CFBundleShortVersionString,而内部版本号用作CfBundLeverVersion。
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
#依赖包(生产环境)
dependencies:
flutter:
sdk: flutter
#依赖包(开发环境)
dev_dependencies:
flutter_test:
sdk: flutter
#指定图片、字体等资源
flutter:
#包含了Material Icons
uses-material-design: true
2. Widget
Flutter中几乎所有的对象(UI组件、功能组件、布局组件)都是一个Widget组件。
Widget的主要工作是实现一个build函数,用以构建自身UI。
一个widget通常由一些较低级别widget组成。Flutter框架将依次构建这些widget,直到构建到最底层的子widget时,这些最低层的widget通常为RenderObject,它会计算并描述widget的几何形状。
Widget的功能是“描述一个UI元素的配置数据”,即Widget并不是表示最终绘制在设备屏幕上的显示元素(Element才是),它只是描述显示元素的一个配置数据。一个Widget可以对应多个Element,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个Element节点都会对应一个Widget对象。
Widget树实际上是一个配置树,而真正的UI渲染树是由Element构成(二者存在对应关系)。在大多数场景,可以宽泛地认为Widget树就是指UI控件树或UI渲染树。
有状态的组件(Stateful widget) 和无状态的组件(Stateless widget)有两点不同:
1. Stateful widget可以拥有状态,这些状态在widget生命周期中是可以变的,而Stateless widget是不可变的。
2. Stateful widget至少由两个类组成:
一个StatefulWidget类。
一个 State类; StatefulWidget类本身是不变的,但是State类中持有的状态在widget生命周期中可能会发生变化。
build方法放在State类中,而不是放在StatefulWidget中的原因,如果将build()方法放在StatefulWidget中:
1. 状态访问不便
2. 继承StatefulWidget不便。
- Widget抽象类
Widget类继承自DiagnosticableTree(诊断树:主要作用是提供调试信息)
一般不会直接继承Widget类来实现新组件,而是继承StatelessWidget或StatefulWidget(也都是抽象类,且直接继承自Widget类)来间接继承Widget类。
@immutable
abstract class Widget extends DiagnosticableTree {
// 没有可变状态(所有字段都必须是final)
// key:主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在canUpdate()方法中
const Widget({ this.key });
final Key key;
// Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象。
// 此方法是Flutter Framework隐式调用的,开发过程中基本不会主动调用。
@protected
Element createElement();
// Widget的字符串描述
@override
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
// 覆写父类的方法,主要是设置诊断树的一些特性
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
@override
@nonVirtual
bool operator ==(Object other) => super == other;
@override
@nonVirtual
int get hashCode => super.hashCode;
// 一个静态方法,主要用于在Widget树重新build时复用旧的widget
// 具体来说,是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;只要newWidget与oldWidget的runtimeType和key同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element。key都为null也代表key相等。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
// 返回1是StatefulWidget,返回2是StatelessWidget,返回0是其他widget
static int _debugConcreteSubtype(Widget widget) {
return widget is StatefulWidget ? 1 :
widget is StatelessWidget ? 2 :
0;
}
}
- StatelessWidget 抽象类(无状态widget)
StatelessWidget继承自Widget,重写了createElement()方法。
用于不需要维护状态的场景,通常在build方法中通过嵌套其它Widget来构建UI。
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
// StatelessElement 间接继承自Element,是StatelessWidget的配置数据
// 一般子类不重写该方法
@override
StatelessElement createElement() => StatelessElement(this);
// context表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。
// 当widget插入到树中时调用
@protected
Widget build(BuildContext context);
}
无状态widget从它们的父widget接收参数, 它们被存储在final型的成员变量中,在构建widget时使用。
例
实现了一个灰色背景字符串的widget
class Echo extends StatelessWidget {
// 按照惯例,widget的构造函数参数应使用命名参数,命名参数中的必要参数要添加@required标注,这样有利于静态代码分析器进行检查。
// 在继承widget时,第一个参数通常应该是Key
// 如果Widget需要接收子Widget,那么child或children参数通常应被放在参数列表的最后
// 按照惯例,Widget的属性应尽可能的被声明为final,防止被意外改变
const Echo({
Key key,
@required this.text,
this.backgroundColor:Colors.grey,
}):super(key:key);
final String text;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
在其他widget的build中使用:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
例2
在子树中获取父级widget
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
child: Builder(builder: (context) {
// 在Widget树中向上查找最近的父级Scaffold widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
- StatefulWidget 抽象类(有状态widget)
StatefulWidget继承自Widget,重写了createElement()方法,添加了一个新的接口createState()。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
// 一般子类不重写该方法
// StatefulElement 间接继承自Element类,是StatefulWidget的配置数据。
// StatefulElement中可能会多次调用createState()来创建状态对象。
@override
StatefulElement createElement() => StatefulElement(this);
// 子类需要重写该方法,返回关联的state
// createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,本质上就是一个StatefulElement对应一个State实例。
// 在StatefulWidget调用`createState`之后,框架将新的状态对象插入树中,然后调用状态对象的initState。
@protected
State createState();
}
在继承StatefulWidget重写其方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中先调用父类方法。
在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的。即子widget到父widget是通过事件通信,而父到子是通过状态。
State
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态。
State中的保存的状态信息可以:
1.在widget构建时可以被同步读取。
2.在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。
State中有两个常用属性:
1. widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。
注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
2. context。StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。
StatefulWidget的生命周期
例
实现一个计数器widget,点击它可以使计数器加1,由于要保存计数器的数值状态,所以应继承StatefulWidget
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
// 当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在Widget树上获取离当前widget最近的一个父级InheritFromWidget),原因是在初始化完成后,Widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
@override
void initState() {
super.initState();
//初始化状态
_counter=widget.initValue;
print("initState");
}
/*
用于构建Widget子树的,会在如下场景被调用:
在调用initState()之后。
在调用didUpdateWidget()之后。
在调用setState()之后。
在调用didChangeDependencies()之后。
在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
*/
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//点击后计数器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
// 在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
// 在CounterWidget从widget树中移除时,deactive和dispose会依次被调用。
// 当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
@override
void deactivate() {
super.deactivate();
print("deactive");
}
// 当State对象从树中被永久移除时调用;通常在此回调中释放资源。
@override
void dispose() {
super.dispose();
print("dispose");
}
// 专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
// 当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
// 在其他widget的build方法
Widget build(BuildContext context) {
return CounterWidget();
}
终端输出
flutter: initState
flutter: didChangeDependencies
flutter: build
保存,终端输出
flutter: reassemble
flutter: didUpdateWidget
flutter: build
将return CounterWidget();改为return Text("xxx");并保存,终端输出
flutter: reassemble
flutter: deactive
flutter: dispose
将build方法放在State中,而不是放在StatefulWidget中,主要是为了提高开发的灵活性。
如果将build()方法在StatefulWidget中则会有两个问题:
1. 状态访问不便。如果我们的StatefulWidget有很多状态,而每次状态改变都要调用build方法,由于状态是保存在State中的,如果build方法在StatefulWidget中,那么build方法和状态分别在两个类中,那么构建时读取状态将会很不方便。
2. 继承StatefulWidget不便。例如,Flutter中有一个动画widget的基类AnimatedWidget,它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法build(BuildContext context),继承自AnimatedWidget的动画widget都要实现这个build方法。现在设想一下,如果StatefulWidget 类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个state对象,这就意味着AnimatedWidget必须将自己的State对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build方法中调用父类的build方法。这样很显然是不合理的,因为:AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部;如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
在Widget树中获取State对象
由于StatefulWidget的的具体逻辑都在其State中,所以很多时候,我们需要获取StatefulWidget对应的State对象来调用一些方法,比如Scaffold组件对应的状态类ScaffoldState中就定义了打开SnackBar(路由页底部提示条)的方法。
有两种方法在子widget树中获取父级StatefulWidget的State对象:
1. 通过Context获取
context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。
2. 通过GlobalKey获取
第一步:给目标StatefulWidget添加GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
第二步:通过GlobalKey来获取State对象
_globalKey.currentState.openDrawer()
GlobalKey是Flutter提供的一种在整个APP中引用element的机制。
如果一个widget设置了GlobalKey,那么便可以通过globalKey.currentWidget获得该widget对象、globalKey.currentElement来获得widget对应的element对象,如果当前widget是StatefulWidget,则可以通过globalKey.currentState来获得该widget对应的state对象。
注意:使用GlobalKey开销较大,如果有其他可选方案,应尽量避免使用它。GlobalKey在整个widget树中必须是唯一的,局部key只需要在同级中唯一。
例(通过Context获取)
Scaffold(
appBar: AppBar(
title: Text("子树中获取State对象"),
),
body: Center(
child: Builder(builder: (context) {
return RaisedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
//调用ScaffoldState的showSnackBar来弹出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
},
child: Text("显示SnackBar"),
);
}),
),
);
一般来说,如果StatefulWidget的状态是私有的(不应该向外部暴露),那么代码中就不应该去直接获取其State对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),则可以去直接获取其State对象。但是通过context.findAncestorStateOfType获取StatefulWidget的状态的方法是通用的,并不能在语法层面指定StatefulWidget的状态是否私有,所以在Flutter开发中便有了一个默认的约定:如果StatefulWidget的状态是希望暴露出的,应当在StatefulWidget中提供一个of静态方法来获取其State对象,开发者便可直接通过该方法来获取;如果State不希望暴露,则不提供of方法。这个约定在Flutter SDK里随处可见。
所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:
...//省略无关代码
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
状态(State)管理
响应式的编程框架中都会有一个永恒的主题——状态管理,无论是在React/Vue(两者都是支持响应式编程的Web开发框架)还是Flutter中,他们讨论的问题和解决的思想都是一致的。
管理状态的常见方法:
Widget管理自身状态。
Widget管理子Widget状态。
混合管理(父子Widget都管理状态)。
如何决定使用哪种管理方法:
如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父widget中管理(灵活会显得更重要一些)。
例
管理自身状态
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
@override
_TapboxAState createState() => new _TapboxAState();
}
/*
_TapboxAState 类:
管理TapboxA的状态。
定义_active:确定盒子的当前颜色的布尔值。
定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。
实现widget的所有交互式行为。
*/
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
_active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
管理子组件TapboxB的状态.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => new _ParentWidgetState();
}
/*
ParentWidgetState 类:
为TapboxB 管理_active状态。
实现_handleTapboxChanged(),当盒子被点击时调用的方法。
当状态改变时,调用setState()更新UI。
TapboxB 类:
继承StatelessWidget类,因为所有状态都由其父组件处理。
当检测到点击时,它会通知父组件。
*/
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return new Container(
child: new TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
混合管理
//---------------------------- ParentWidget ----------------------------
class ParentWidgetC extends StatefulWidget {
@override
_ParentWidgetCState createState() => new _ParentWidgetCState();
}
/*
_ParentWidgetStateC类:
管理_active 状态。
实现 _handleTapboxChanged() ,当盒子被点击时调用。
当点击盒子并且_active状态改变时调用setState()更新UI。
_TapboxCState 对象:
管理_highlight 状态。
GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
当按下、抬起、或者取消点击时更新_highlight状态,调用setState()更新UI。
当点击时,将状态的改变传递给父组件。
*/
class _ParentWidgetCState extends State<ParentWidgetC> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return new Container(
child: new TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
@override
_TapboxCState createState() => new _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
@override
Widget build(BuildContext context) {
// 在按下时添加绿色边框,当抬起时,取消高亮
return new GestureDetector(
onTapDown: _handleTapDown, // 处理按下事件
onTapUp: _handleTapUp, // 处理抬起事件
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: new Container(
child: new Center(
child: new Text(widget.active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white)),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? new Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
全局状态管理
当应用中需要一些跨组件(包括跨路由)的状态需要同步时,比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,APP中依赖应用语言的组件能够重新build一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。
正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
1. 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。
2. 使用一些专门用于状态管理的包,如Provider、Redux。
例(计数器示例 优化)
class Counter extends StatefulWidget {
@override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
========================优化=============
分离 显示 计数器(CounterDisplay)和 更改 计数器(CounterIncrementor)的逻辑。责任分离允许将复杂性逻辑封装在各个widget中,同时保持父项的简单性。
class CounterDisplay extends StatelessWidget {
CounterDisplay({this.count});
final int count;
@override
Widget build(BuildContext context) {
return new Text('Count: $count');
}
}
class CounterIncrementor extends StatelessWidget {
CounterIncrementor({this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: onPressed,
child: new Text('Increment'),
);
}
}
class Counter extends StatefulWidget {
@override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return new Row(children: <Widget>[
new CounterIncrementor(onPressed: _increment),
new CounterDisplay(count: _counter),
]);
}
}
例
void main() {
runApp(new MaterialApp(
title: 'Shopping App',
home: new ShoppingList(
products: <Product>[
new Product(name: 'Eggs'),
new Product(name: 'Flour'),
new Product(name: 'Chocolate chips'),
],
),
));
}
class ShoppingList extends StatefulWidget {
ShoppingList({Key key, this.products}) : super(key: key);
final List<Product> products;
@override
_ShoppingListState createState() => new _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
Set<Product> _shoppingCart = new Set<Product>();
void _handleCartChanged(Product product, bool inCart) {
setState(() {
if (inCart)
_shoppingCart.add(product);
else
_shoppingCart.remove(product);
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Shopping List'),
),
body: new ListView(
padding: new EdgeInsets.symmetric(vertical: 8.0),
children: widget.products.map((Product product) {
return new ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
// 商品
class Product {
const Product({this.name});
final String name;
}
typedef void CartChangedCallback(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({Product product, this.inCart, this.onCartChanged})
: product = product,
super(key: new ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
return inCart ? Colors.black54 : Theme.of(context).primaryColor;
}
TextStyle _getTextStyle(BuildContext context) {
if (!inCart) return null;
return new TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return new ListTile(
onTap: () {
onCartChanged(product, !inCart);
},
leading: new CircleAvatar(
backgroundColor: _getColor(context),
child: new Text(product.name[0]),
),
title: new Text(product.name, style: _getTextStyle(context)),
);
}
}
3. 路由管理(跳转)
两个核心概念:Route、Navigator
路由(Route):
在移动开发中通常指页面(Page),在Android中通常指一个Activity,在iOS中指一个ViewController。
路由管理:
管理页面之间如何跳转,通常也可被称为导航管理。导航管理维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
跳转到下一页面(Navigator.push)
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new SecondScreen()),
);
该push方法需要一个Route(自己创建或者MaterialPageRoute)
返回到上一页面(Navigator.pop)
Navigator.pop(context);
在计数器示例中,做如下修改:
1. 创建一个新路由,命名“NewRoute”
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("New route"),
),
body: new Center(
child: new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: new Text('Go back!'),
),
),
);
}
}
2. 在_MyHomePageState.build方法中的Column的子widget中添加一个按钮(FlatButton) :
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
... //省略无关代码
FlatButton(
child: Text("open new route"),
textColor: Colors.blue,
onPressed: () {
//导航到新路由
Navigator.push( context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}));
},
),
],
)
MaterialPageRoute
MaterialPageRoute继承自PageRoute类。PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。如果想自定义路由切换动画,可以继承PageRoute来实现。
MaterialPageRoute 是Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
对于Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
对于iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
说明:
1. builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget(新路由的实例)。
2. settings 路由的配置信息,如路由名称、是否初始路由(首页)。
3. maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为false。
4. fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在iOS中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。
Navigator 路由管理组件(提供了打开和退出路由的方法)
Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。
Navigator类中所有第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)
Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。
bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。
路由传值
很多时候,在路由跳转时需要带一些参数,比如打开商品详情页时需要带一个商品id,这样商品详情页才知道展示哪个商品信息;又比如在填写订单时需要选择收货地址,打开地址选择页并选择地址后,可以将用户选择的地址返回到订单页等等。
例
创建一个TipRoute路由,它接受一个提示文本参数,负责将传入它的文本显示在页面上,另外TipRoute中我们添加一个“返回”按钮,点击后在返回上一个路由的同时会带上一个返回参数
class RouterTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
onPressed: () async {
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "hello world",
);
},
),
);
//输出`TipRoute`路由返回结果,从push的路由返回时调用,
// 点击返回按钮,命令行输出 路由返回值: 我是返回值。点击箭头,命令行输出 路由返回值: null。
print("路由返回值: $result");
},
child: Text("打开提示页"),
),
);
}
}
class TipRoute extends StatelessWidget {
TipRoute({
Key key,
@required this.text, // 接收一个text参数
}) : super(key: key);
final String text;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("提示"),
),
body: Padding(
padding: EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(text),
RaisedButton(
onPressed: () => Navigator.pop(context, "我是返回值"),
child: Text("返回"),
)
],
),
),
),
);
}
}
1
例
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Todo {
final String title;
final String description;
Todo(this.title, this.description);
}
void main() {
runApp(new MaterialApp(
title: 'Passing Data',
home: new TodosScreen(
todos: new List.generate(
20,
(i) => new Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
),
),
));
}
class TodosScreen extends StatelessWidget {
final List<Todo> todos;
TodosScreen({Key key, @required this.todos}) : super(key: key);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Todos'),
),
body: new ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text(todos[index].title),
// When a user taps on the ListTile, navigate to the DetailScreen.
// Notice that we're not only creating a new DetailScreen, we're
// also passing the current todo through to it!
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new DetailScreen(todo: todos[index]),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
// Declare a field that holds the Todo
final Todo todo;
// In the constructor, require a Todo
DetailScreen({Key key, @required this.todo}) : super(key: key);
@override
Widget build(BuildContext context) {
// Use the Todo to create our UI
return new Scaffold(
appBar: new AppBar(
title: new Text("${todo.title}"),
),
body: new Padding(
padding: new EdgeInsets.all(16.0),
child: new Text('${todo.description}'),
),
);
}
}
命名路由
命名路由即有名字的路由,给路由起名字后通过路由名直接打开新的路由。
在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。
使用命名路由的好处:
1. 语义化更明确。
2. 代码更好维护;如果使用匿名路由,则必须在调用Navigator.push的地方创建新路由页,这样不仅需要import新路由页的dart文件,而且这样的代码将会非常分散。
3. 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑。
首先注册一个路由表,提供对应关系(路由名---路由组件)
在MaterialApp中添加routes属性:"new_page":(context) => NewHelloRoute(),
通过路由名打开新路由页
// Navigator的Future pushNamed(BuildContext context, String routeName,{Object arguments})方法
Navigator.pushNamed(context, "new_page");
路由表的定义如下:
Map<String, WidgetBuilder> routes;
它是一个Map。key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。
例
在MyApp类的build方法中找到MaterialApp,添加routes属性
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
... // 省略其它路由注册信息
} ,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
/*
将home也注册为命名路由
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名为"/"的路由作为应用的home(首页)。设置初始路由
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);
*/
修改FlatButton的onPressed回调代码,改为:
onPressed: () {
Navigator.pushNamed(context, "new_page");
//Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return NewRoute();
//}));
},
命名路由参数传递
在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数;
注册路由表
routes:{
"new_page":(context) => NewRoute(),
} ,
传递值
Navigator.of(context).pushNamed("new_page", arguments: "hi");
获取值(NewRoute的build方法中)
Widget build(BuildContext context) {
// 获取路由参数
var args=ModalRoute.of(context).settings.arguments;
//...省略无关代码
}
如果TipRoute接受一个text 参数,在不改变TipRoute源码的前提下,使用路由名来打开它需要:
routes: {
"new_page": (context){
return TipRoute(text: ModalRoute.of(context).settings.arguments);
},
},
路由生成钩子
MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用。当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。
onGenerateRoute只会对命名路由生效。
onGenerateRoute回调签名如下:
Route<dynamic> Function(RouteSettings settings)
例
放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制
MaterialApp(
... //省略无关代码
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其它情况则正常打开路由。
}
);
}
);
4. 资源管理
Flutter APP安装包中会包含代码和资源(assets)两部分。
在构建期间,Flutter将asset放置到称为 asset bundle 的特殊存档中,应用程序可以在运行时读取它们(但不能修改)。
常见assets类型:
静态数据(JSON文件)
配置文件
图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)
指定 assets
和包管理一样,Flutter也使用pubspec.yaml文件来管理应用程序所需的资源。
assets指定应包含在应用程序中的文件, 每个asset都通过相对于pubspec.yaml文件所在的文件系统路径来标识自身的路径。
asset的声明顺序是无关紧要的,asset的实际目录可以是任意文件夹。
例
flutter:
assets:
- assets/my_icon.png
- assets/background.png
这里的图片文件夹名是assets,也可是imgs/my_icon.png
Asset 变体(variant)
构建过程支持“asset变体”:不同版本的asset可能会显示在不同的上下文中。 在pubspec.yaml的assets部分中指定asset路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的asset一起被包含在asset bundle中。
在选择匹配当前设备分辨率的图片时,Flutter会使用到asset变体。
将来,Flutter可能会将这种机制扩展到本地化、阅读提示等方面。
例
如果应用程序目录中有以下文件:
…/pubspec.yaml
…/graphics/my_icon.png
…/graphics/background.png
…/graphics/dark/background.png
…etc.
pubspec.yaml文件中只需包含:
flutter:
assets:
- graphics/background.png
那么这两个graphics/background.png和graphics/dark/background.png 都将包含在asset bundle中。前者被认为是main asset (主资源),后者被认为是一种变体(variant)。
加载assets
应用可以通过AssetBundle对象访问其asset 。有两种主要方法允许从Asset bundle中加载字符串或图片(二进制)文件。
加载文本assets
2种方法
1. 通过rootBundle 对象加载
每个Flutter应用程序都有一个rootBundle对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset。
2. 通过 DefaultAssetBundle 加载(建议)
使用 DefaultAssetBundle 来获取当前BuildContext的AssetBundle。 这种方法不是使用应用程序构建的默认asset bundle,而是使父级widget在运行时动态替换的不同的AssetBundle,这对于本地化或测试场景很有用。
通常,可以使用DefaultAssetBundle.of()在应用运行时来间接加载asset(例如JSON文件)
而在widget上下文之外或其它AssetBundle句柄不可用时,可以使用rootBundle直接加载这些asset,例如:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
加载图片
AssetImage 可以将asset的请求逻辑映射到最接近当前设备像素比例(dpi)的asset。为了使这种映射起作用,必须根据特定的目录结构来保存asset:
…/image.png
…/Mx/image.png
…/Nx/image.png
…etc.
其中M和N是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。
主资源默认对应于1.0倍的分辨率图片。
例:
…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png
在设备像素比率为1.8的设备上,.../2.0x/my_icon.png 将被选择。对于2.7的设备像素比率,.../3.0x/my_icon.png将被选择。
如果未在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。即,如果.../my_icon.png是72px乘72px,那么.../3.0x/my_icon.png应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。
pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。
要加载图片,可以使用 AssetImage类
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('graphics/background.png'),
),
),
);
}
AssetImage 并非是一个widget, 它实际上是一个ImageProvider,有些时候可能期望直接得到一个显示图片的widget,那么可以使用Image.asset()方法,如:
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}
使用默认的 asset bundle 加载资源时,内部会自动处理分辨率等,这些处理对开发者来说是无感知的。 (如果使用一些更低级别的类,如 ImageStream或 ImageCache 时注意与缩放相关的参数)
要加载依赖包中的图像,必须给AssetImage提供package参数。包在使用本身的资源时也应该加上package参数来获取。
例:
假设应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:
…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png
…etc.
然后加载图像,使用:
new AssetImage('icons/heart.png', package: 'my_icons')
或
new Image.asset('icons/heart.png', package: 'my_icons')
如果在pubspec.yaml文件中声明了期望的资源,它将会打包到相应的package中。特别是,包本身使用的资源必须在pubspec.yaml中指定。
包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml中指定包含哪些图像。 例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:
…/lib/backgrounds/background1.png
…/lib/backgrounds/background2.png
…/lib/backgrounds/background3.png
要包含第一张图像,必须在pubspec.yaml的assets部分中声明它:
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
lib/是隐含的,所以它不应该包含在资产路径中。
上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用。
如果要给应用设置APP图标或者添加启动图,那必须使用特定平台的assets。
特定平台 assets
在Flutter框架加载时,Flutter会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。这意味着如果不在应用程序的main()方法中调用runApp 函数 (或者更具体地说,如果不调用window.render去响应window.onDrawFrame)的话, 启动屏幕将永远持续显示。
iOS
Assets.xcassets中设置App图标、启动图。直接拖入图片。
Android
App图标:.../android/app/src/main/res目录,里面包含了各种资源文件夹,替换。
启动图:.../android/app/src/main目录。在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面。
注意:如果重命名了.png文件,则必须在AndroidManifest.xml的<application>标签的android:icon属性中更新名称。
iOS
iOS
Scaffold(属于基础组件库)
用于快速搭建页面
包含了一个路由页的骨架,有导航栏(AppBar)、抽屉菜单(MyDrawer)、底部导航栏(BottomNavigationBar)、悬浮按钮(FloatingActionButton)。
例
实现一个页面,包含:
一个导航栏
导航栏右边有一个分享按钮
有一个抽屉菜单
有一个底部导航
右下角有一个悬浮的动作按钮
实现效果
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: 'Flutter Tutorial',
home: new ScaffoldRoute(),
));
}
class ScaffoldRoute extends StatefulWidget {
@override
_ScaffoldRouteState createState() => _ScaffoldRouteState();
}
class _ScaffoldRouteState extends State<ScaffoldRoute> {
int _selectedIndex = 1;
@override
Widget build(BuildContext context) {
// Material必须有Scaffold
return Scaffold(
appBar: AppBar( //导航栏
title: Text("App Name"),
actions: <Widget>[ //导航栏右侧菜单
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
// leading: new IconButton(
// icon: new Icon(Icons.menu),
// tooltip: 'Navigation menu',
// onPressed: null,
// ),
),
// body: this._pageList[this._selectedIndex],
drawer: new MyDrawer(), //抽屉
bottomNavigationBar: BottomNavigationBar( // 底部导航
items: <BottomNavigationBarItem>[ // items
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, // 点击后调用
// iconSize:35, // 图标大小
// type: BottomNavigationBarType.fixed 按钮过多时允许设置多个
),
floatingActionButton: FloatingActionButton( //悬浮按钮
child: Icon(Icons.add),
onPressed:_onAdd // 可为null
),
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
void _onAdd(){
}
}
抽屉效果
AppBar (导航栏)
一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。
AppBar({
Key key,
this.leading, // 导航栏最左侧Widget(常见为抽屉菜单按钮或返回按钮)
this.automaticallyImplyLeading = true, // 如果leading为null,是否自动实现默认的leading按钮
this.title, // 页面标题
this.actions, // 导航栏右侧菜单
this.bottom, // 导航栏底部菜单,通常为Tab按钮组
this.elevation = 4.0, // 导航栏阴影
this.centerTitle, // 标题是否居中
this.backgroundColor,
... // 其它属性见源码注释
})
如果给Scaffold添加了抽屉菜单,默认情况下Scaffold会自动将AppBar的leading设置为菜单按钮
点击它便可打开抽屉菜单。如果想自定义菜单图标,可以手动来设置leading,如:
Scaffold(
appBar: AppBar(
title: Text("App Name"),
leading: Builder(builder: (context) {
return IconButton(
icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标
onPressed: () {
// 打开抽屉菜单 Scaffold.of(context)可以获取父级最近的Scaffold 组件的State对象。
Scaffold.of(context).openDrawer();
},
);
}),
...
)
替换左侧菜单图标
TabBar组件(Tab菜单栏)
属于Material组件库
Tab组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。
Tab({
Key key,
this.text, // 菜单文本
this.icon, // 菜单图标
this.child, // 自定义组件样式
})
例
class _ScaffoldRouteState extends State<ScaffoldRoute>
with SingleTickerProviderStateMixin {
TabController _tabController; //需要定义一个Controller
List tabs = ["新闻", "历史", "图片"];
@override
void initState() {
super.initState();
// 创建Controller ,用于控制/监听Tab菜单切换
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
... //省略无关代码
bottom: TabBar( //生成Tab菜单
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()
),
),
... //省略无关代码
);
}
Tabbar
通过TabBar只能生成一个静态的菜单,真正的Tab页还没有实现。通过TabBarView来实现。
由于Tab菜单和Tab页的切换需要同步,需要通过TabController去监听Tab菜单的切换去切换Tab页
_tabController.addListener((){
switch(_tabController.index){
case 1: ...;
case 2: ... ;
}
});
如果Tab页可以滑动切换的话,还需要在滑动过程中更新TabBar指示器的偏移!显然,要手动处理这些是很麻烦的,为此,Material库提供了一个TabBarView组件,通过它不仅可以轻松的实现Tab页,而且可以非常容易的配合TabBar来实现同步切换和滑动状态同步,示例如下:
Scaffold(
appBar: AppBar(
... //省略无关代码
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()),
),
),
drawer: new MyDrawer(),
body: TabBarView(
controller: _tabController,
children: tabs.map((e) { //创建3个Tab页
return Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
);
}).toList(),
),
... // 省略无关代码
);
现在,无论是点击导航栏Tab菜单还是在页面上左右滑动,Tab页面都会切换,并且Tab菜单的状态和Tab页面始终保持同步。TabBar和TabBarView正是通过同一个controller来实现菜单切换和滑动状态同步的
抽屉菜单Drawer
Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。
例(一个左抽屉菜单)
class MyDrawer extends StatelessWidget {
const MyDrawer({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding(
context: context,
//移除抽屉菜单顶部默认留白
removeTop: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 38.0),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ClipOval(
child: Image.asset(
"imgs/avatar.png",
width: 80,
),
),
),
Text(
"Wendux",
style: TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
Expanded(
child: ListView(
children: <Widget>[
ListTile(
leading: const Icon(Icons.add),
title: const Text('Add account'),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Manage accounts'),
),
],
),
),
],
),
),
);
}
}
抽屉菜单通常将Drawer组件作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding可以移除Drawer默认的一些留白(比如Drawer默认顶部会留和手机状态栏等高的留白)
FloatingActionButton
FloatingActionButton是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口。
可以通过Scaffold的floatingActionButton属性来设置一个FloatingActionButton,同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置
Tab底部导航栏(Material组件库:BottomAppBar、BottomNavigationBar)
可以通过Scaffold的bottomNavigationBar属性来设置底部导航
例(打洞)
BottomAppBar组件可以和FloatingActionButton配合实现这种“打洞”效果,源码如下:
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
child: Row(
children: [
IconButton(icon: Icon(Icons.home)),
SizedBox(), //中间位置空出
IconButton(icon: Icon(Icons.business)),
],
mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
),
)
上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton的位置,上面FloatingActionButton的位置为:
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
所以打洞位置在底部导航栏的正中间。
BottomAppBar的shape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,我们也可以自定义外形
打洞