赞
踩
Flutter学习文档—Author:Brath
由于文章内容较干,请允许Brath打一波广告…
面试记APP
Github:https://github.com/Guoqing815/interview
安卓APP下载:https://www.pgyer.com/interview_app_release
Brath的个人博客:https://brath.top
面试记官方公众号,定期分享有趣的编程知识:https://mp.weixin.qq.com/s/jWs6lLHl5L-atXJhHc4YvA
前言:如果你要学习flutter,那么你一定要会dart语言,因为flutter是基于dart来封装的一个 UI 组件包
本文使用 Typort 书写,禁止转载。
本文仅限有后端有语言基础(C/C++/C#/Java/python/Golang/PHP 都可以),前端 ( JavaScript,Html,CSS)的人来学习。如果0基础,请先学习任意一门后端语言并熟练掌握!
安装Dart:https://github.com/GeKorm/dart-windows/releases/download/v1.6.0/Dart_x64.stable.setup.exe
安装好后配置环境变量:DART_HOME E:\dart\bin 安装路径
配置好后cmd输入 dart --version 查看环境
Dart VM version: 2.3.3-dev.0.0.flutter-b37aa3b036 (Tue Jun 11 13:00:50 2019 +0000) on "windows_x64"
/* *多行注释 *多行注释 */ /** * 文档注释 与Java相同 */ ///文档注释 dart独有
dart语言特点:
自动类型转换,var声明的变量可以是任意类型!
dart拥有两种变量定义方式。
指定类型:
String name = "brath";
或者
类型推导
var name = "brath"; //推导为任意类型
final name = "brath"; //不可修改,定义常量,可以初始化
const name = "brath"; //不可修改,定义常量,不可以初始化,可以被构造修改
void main(){ var name = 111111; String name1 = "brath用类型定义"; print("Hello World! Brath~"); print(name.runtimeType); print(name1.runtimeType); } console: Hello World! Brath~ int String
变量拼接:
与Java不同,拼接方式用 ${}
如果只拼接普通变量,可以直接 $变量名
如果拼接变量引用方法,必须要${变量名.方法名}
集合类型:
list集合: var names = ["111","222","333"];
set集合: var movies = {"111","222","333”};
map集合:var info = {"11":"1","22":"2"}
默认情况下,dart的所有class都是隐式接口!
Dart函数使用:
void main(List<String> args) { print(sum(51, 14891)); } int sum(int a,int b){ return a + b; }
函数参数:
必选参数,不能有默认值,可选参数,可以有默认值
main(){ sayHello("why"); } //必选参数:String name 必须传参 void sayHello(String name){ print(name) } //可选参数:位置可选参数 void sayHello2(String name, [int age, String desc]){ sayHello2("brath",12,"waa"); //位置可选参数:用[]包围的参数可传可不传,但是位置必须对应 } //可选参数:命名可选参数 重点,多用! void sayHello3(String name, {int age, String desc}){ sayHello3("brath",age: 13,desc: "212"); //位置可选参数:用{}包围的参数可传可不传,但是必须指定参数名 }
函数是一等公民:
函数可以作为另外一个函数的参数!
void main(List<String> args) { // test(see); //匿名函数 // test((){ // print("匿名"); // return 10; // }); test(() => print("箭头")); } void test(Function foo){ see(); } void see(){ print("see!"); }
void main(List<String> args) { // test((num1,num2){ // return num1+num2; // }); var num = demo(); print(num(20,12)); } //将函数声明式显示,利用 typedef 声明一个函数列表,调用 typedef 声明的函数 typedef Calculate = int Function(int num1,int num2); void test(Calculate calculate){ calculate(20,30); } // void test(int foo(int num1,int num2)){ // foo(20,30); // } Calculate demo(){ return (num1,num2){ return num1 * num2; }; }
Flutter中,有诡异的赋值运算符 比如 name ??="111"; 解释:当原来的变量有值时,不执行 当原来的变量为null时,执行 或者 var name = name ?? "11"; 解释: 当name不为空时使用name,为空使用后面的变量
void main(){ var p = Person() ..name = "brath" ..eat(); ..run(); } //利用 .. 连续调用对象中的方法,类似于Java中的链式调用 class Person(){ String name; void eat(){ print("吃"); } void run(){ print("跑"); } }
For循环和Switch循环与JS和Java基本一致
class Person{ String name; int age; double height; //默认构造函数 Person(this.name.this.age); //命名构造函数,指定名字的构造函数 Person.NameCon(this.name.this.age,this.height); }
dynamic代表任意类型 dynamic obj = "obj"; //可以调用 print(obj.subString(1)); Object obj = "obj"; //不能调用! print(obj.subString(1));
mian(){ var p = Person('brath'); } class Person{ final String name; final int age; //如果传了age参数,就用age参数,如果没传age参数就用10 Person(this.name,{int age}): this.age = age ?? 10; }
mian(){ } class Person{ String name; int age; //默认构造函数调用内部构造函数,重定向 Person(String name) : this._internal(name,0); Person._internal(this.name,this.age) }
//相比于普通构造函数来说,工厂构造函数可以手动返回对象 class Person{ String name; String color; static final Map<String,Person> _nameCache = {}; static final Map<String,Person> _colorCache = {}; //工厂构造函数,手动根据条件返回对象 factory Person.withName(String name){ if(_nameCache.containsKey(name)){ return _nameCache[name]; }else{ _nameCache[name] = Person(name,"default"); return Person(name,"default"); } } }
void main(List<String> args) { //直接访问属性 final p = Person(); p.name = "brath"; print(p.name); //get,set访问 p.setName("brath.cloud"); print(p.getName); } class Person{ late String name; // //get,set方法 // void setName(String name) { // this.name = name; // } // String get getName{ // return name; // } //get,set方法箭头函数 void setName(String name) => this.name = name; String get getName => name; }
//dart中没有interface关键字,默认所有类都是隐式接口 //当讲将一个类作为接口使用时,实现这个接口的类,必须实现这个接口中的所有方法
用class声明的类不可以混入其他类 要混入其他类,使用 mixin 声明该类,并在混入时用with关键字来连接被混入的类
类属性:在类中不用static声明的变量,叫做成员变量,不可以被类直接调用 静态属性:在类中用static声明的变量,叫做静态属性,类属性,可以被类直接调用 类方法:在类中不用static声明的方法,叫做成员方法,不可以被类直接调用 静态方法:在类中用static声明的方法,叫做静态方法,类属性,可以被类直接调用
void main(List<String> args) { final color = Colors.bule; switch(color){ case Colors.bule: print("蓝色"); break; case Colors.red: print("红色"); break; case Colors.yellow: print("黄色"); break; } print(Colors.values); } enum Colors{ red, bule, yellow }
//在Dart中,任何一个dart文件都是一个库,类似于Java中的包 //系统库导入: import 'dart:库名'; //自定会库导入: import '包名/类名'; //库别名:当本类引用其他库时,出现方法名冲突,可以用 as 来给导入的库起别名,再用别名引用 import 'utils/TimeUtil' as timeUtil; //默认情况下,导入一个库时,导入的是这个库中所有的内容 //dart提供两个关键字来单独导入方法或者隐藏某个方法: show hide import 'utils/TimeUtil' show timeUtil; //只导入timeUtil方法 import 'utils/TimeUtil' hide timeUtil; //只有timeUtil不会导入 //多个方法可以用逗号分割: import 'utils/TimeUtil' show timeUtil, FileUtil; //只导入timeUtil,FileUtil方法 import 'utils/TimeUtil' hide timeUtil, FileUtil; //只有timeUtil,FileUtil不会导入
以上方法导入库的时候总是会遇到一些问题,比如如果有100个方法,你只想用50个,那么你就要用50个show或者50个hide,但是dart提供了一种方式,就是抽取库到一个公共类中。 前面提到过,dart中所有文件都是一个库,那么我们把需要导入的库,全部export到一个库中,在引用这个库,就不用担心过多引入了。 公共库: util.dart export 'util/TimeUtil' export 'util/FileUtil' 我的代码: import 'util';
//dart使用第三方库需要创建一个文件 pubspec.yaml
name: 库名 desciption: 描述 dependencies: 依赖 http: ^0.13.4 怎么找库? https://pub.dev/packages/http
点击installing
把dependencies内容复制到代码中
name: coderwhy desciption: a dart dependencies: http: ^0.12.0+4 environment: sdk: '>=2.10.0 < 3.0.0'
进入当前类文件夹,终端输入 pub get 就会下载对应库包
import 'package:http/http.dart' as http; //引入第三方库,必须用package来开头 void main() async { var url = 'https://www.brath.cloud:9000/esn-user-service/user/getUserInfo?id=1'; var url2 = 'https://brath.cloud/image/back.png'; var response = await http.get(url); print(response.body); }
与Java相同但是有不一样的部分:
同步处理
在一个方法中用try捕获异常,如果调用方法就捕获不到了!
异步处理
调用一个异步方法如果发生异常,可以用自异步+await来捕获异常
void main() async{ try{ await test1(); }catch(e){ print(e); } } test1() async{ print(11~/0); }
接下来介绍 我们的Flutter!
架构对比:
GUP绘制出图像,放到Buffer缓存中,手机屏幕根据刷新率来读取缓存的操作,就是展示图像。
为什么要有垂直同步?
来看一个例子:假设我GPU每秒帧率产生60,手机屏幕每秒也是接受60,这时可以正常显示。
如果突然每秒帧率提高到120,手机屏幕可能会来不及读取缓存导致画面重叠、撕裂
开启垂直同后,会有两块缓存区域。
垂直同步就限制了手机屏幕读取缓存和GPU产生的速度,开启垂直同步后,GPU将画面写入到第一个缓存中,第一个缓存会复制内容(地址交换)到第二个缓存中,当两份缓存都存在这一帧,就会发送一个VSync的信号,告诉GPU可以绘制下一张图,然后手机屏幕来显示第二个缓存中的内容,这样就可以避免图像撕裂。
一个简单的flutter结构:
import 'package:flutter/material.dart'; // mian() => runApp(MyApp()); void main() { runApp(const MyApp()); } //APP主体 class MyApp extends StatelessWidget{ @override Widget build(BuildContext context) { return MaterialApp( home: BrathScaffoldPage() ); } } //页面主体 class BrathScaffoldPage extends StatelessWidget{ @override Widget build(BuildContext context) { return Scaffold( //appbar:顶部标签主体 appBar: AppBar( centerTitle: true, title: Text("第一个Fullter程序",style: TextStyle(fontSize: 20),), ), body: BrathBodyPage() ); } } //内容主体 class BrathBodyPage extends StatelessWidget{ @override Widget build(BuildContext context) { return Text("Hello Fullter"); } }
开始学习:
下载Flutter SDK
配置Flutter的第一步就是下载Flutter SDK,然后进行安装,上面两个地址都有给SDK下载地址,这里的问题是有的SDK安装包有时会报 没有.git文件的错误,所以最稳妥的方法是通过git clone命令安装 在安装目录下面执行
git clone -b stable https://github.com/flutter/flutter.git
安装完成之后,可以在安装根目录,找的 flutter_console.bat 文件,双击运行
在用户变量里面编辑或者添加 Path 条目,把Flutter的bin目录路径添加在里面
在命令行运行 flutter doctor,它会下载它自己的依赖项并自行编译,一般情况是会报错提示,多半是Android SDK找不到什么的,如果出错了,就按照错误信息网上查一下就解决了。 我的已经处理完成的
我用的Android Studio,上面连接里面有不同系统和编辑器的流程,详情可以前往查看
Android Studio的开发环境就不说了,需要的可以自行百度。Android Studio配置Flutter开发主要是 Flutter 和 Dart两个插件
File -- Settings -- Plugins -- Marketplace 然后在搜索里面搜索Flutter和Dart安装就可以了。 安装完插件,重启一下 Android Studio 基本就配置完成了,可以新建Flutter项目了。
File -- New -- New Flutter Project
选择Flutter Application
然后到这个最后一步的时候,会有一点小问题
Flutter SDK path 这一栏第一次默认是空的,需要手动选择,选择我们最开始下载的Flutter SDK,选择根目录,就可以了
现在开始学习Flutter的基础组件,以及进阶理论!
到想存储项目的文件路径,打开CMD,输入 flutter create 项目名称即可
vscode下载好插件,dart和flutter打开对应flutter文件,即可开始编写
import 'package:flutter/material.dart'; //导包 material main() { runApp(MyApp()); //运行app } class MyApp extends StatelessWidget { //继承无状态widget @override Widget build(BuildContext context) { return MaterialApp( //运行根节点MaterialApp ); } }
特性:
widget分为有状态(StatefulWidget)和无状态的 (StatelessWidget)
无状态的widget是静态页面
有状态的widget是动态页面
要点:
tips:flutter的main入口调用第一个widget需要该widget使用 MaterialApp()作为首个widget
因为 MaterialApp 包含了路由主题等等组件,flutter规定只能用MaterialApp当作根节点
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } }
均为可选参数
Container({ Key? key, this.alignment, this.padding, //边距 this.color, //颜色 使用 Clolrs枚举 this.decoration, //描述 this.foregroundDecoration, double? width, //宽度 使用double 常量 double? height, //高度 使用double 常量 BoxConstraints? constraints, this.margin, //margin this.transform, this.transformAlignment, this.child, //子组件 this.clipBehavior = Clip.none, }) : assert(margin == null || margin.isNonNegative), assert(padding == null || padding.isNonNegative), assert(decoration == null || decoration.debugAssertIsValid()), assert(constraints == null || constraints.debugAssertIsValid()), assert(clipBehavior != null), assert(decoration != null || clipBehavior == Clip.none), assert(color == null || decoration == null, 'Cannot provide both a color and a decoration\n' 'To provide both, use "decoration: BoxDecoration(color: color)".', ), constraints = (width != null || height != null) ? constraints?.tighten(width: width, height: height) ?? BoxConstraints.tightFor(width: width, height: height) : constraints, super(key: key);
Text默认传一个文本:
class TextDemo extends StatelessWidget @override Widget build(BuildContext context) { return Container( //容器 width: double.infinity, //宽度 使用double枚举 color: Colors.blue, //颜色 使用Colors枚举 child: Text( //容器的子组件 文本组件 "文本" * 20, //输入文本 20个 maxLines: 1, //最大行数 1 textDirection: TextDirection.ltr, //从左到右 textAlign: TextAlign.center, //剧中 style: TextStyle( //设置文本样式 fontSize: 30, //字体大小 30 color: Colors.teal //字体颜色 ), ) ); } }
const Text( //必传参数 String this.data, //可选参数 { Key? key, this.style, //文本风格,使用 TextStyle方法来指定 this.strutStyle, this.textAlign, //设置文本居中 靠左 靠右,使用 TextAlign枚举 this.textDirection, //文本排列:左到右 右到左 使用 TextDirection枚举 this.locale, this.softWrap, this.overflow, //溢出后按照什么风格显示,使用TextOverflow的枚举 this.textScaleFactor, this.maxLines, //最大行数 this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, }) : assert( data != null, 'A non-null String must be provided to a Text widget.', ), textSpan = null, super(key: key);
flutter中有几种常用按钮组件:
在 2.0 版本后遗弃按钮 RaisedButton改为ElevatedButton , FlatButton改为TextButton
RaisedButton 已遗弃 FlatButton 已遗弃
ElevatedButton:漂浮按钮/升降按钮
class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ ElevatedButton( onPressed:(){ //点击事件,如果为null未定义的话,按钮无法点击 }, child: Text( //这里是按钮文本,可以是图片可以是文本 "漂浮按钮" ) ) ], ); } }
TextButton:扁平按钮/文本按钮
class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ TextButton( onPressed: (){ //点击事件 }, child: Text( "扁平按钮" )) ], ); } }
TextButton.icon:带图标的扁平按钮/文本按钮
class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ TextButton.icon(onPressed: (){}, icon: Icon(Icons.add), //使用Icons枚举选择图标 label: Text("图标按钮")) ], ); } }
OutlinedButton.icon:无阴影按钮
class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ OutlinedButton(onPressed: (){}, child: Text("无阴影按钮")) ], ); } }
OutlinedButton.icon:图标按钮
class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ IconButton(onPressed: (){}, icon: Icon(Icons.home)) //图标用 Icons 枚举选择 ], ); } }
flutter提供了四种图片加载方式:
1、Image.network //从网络获取图片
2、Image.asset //从项目本地获取图片
3、Image.file //从文件路径获取图片
4、Image.memory //从手机内存,存储中获取图片
使用 asset 需要设置 pubspec.yaml 中的 assets
class ImageIconDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ Icon(Icons.home), //普通图标 IconButton(onPressed: (){}, icon: Icon(Icons.home)), //带点击事件的图标 Container( width: double.infinity, //最大宽度 child: Image.network( //从网络获取图片 "https://brath.cloud/love/GCLK6888.JPG?versionId=CAEQNxiBgID8yJjchBgiIDUzZGFiMWU3YWVlNDQ4YmJhMzMwNDY0Mzk1OGJiOTU1", fit: BoxFit.fill, //图片填充模式 ), ), Image.asset("images/image.jpeg"), //项目加载图片 ], ); } }
因为开关和复选框是动态的,有状态的,所以我们要使用 StatefulWidget 来做他们的widget
//Tips:在 onChanged 使用 setState 来改变状态
Check 复选框
class CheckDemo extends StatefulWidget { @override State<CheckDemo> createState() => _CheckDemoState(); } class _CheckDemoState extends State<CheckDemo> { bool _check = false; @override Widget build(BuildContext context) { return Column( children: [ Checkbox( value: _check, onChanged: (res){ //在 onChanged 使用 setState 来改变状态 setState(() { _check = res!; }); }), ], ); } }
Switch开关
class CheckDemo extends StatefulWidget { @override State<CheckDemo> createState() => _CheckDemoState(); } class _CheckDemoState extends State<CheckDemo> { bool _switch = false; @override Widget build(BuildContext context) { return Column( children: [ Switch( value: _switch, onChanged: (res){ //在 onChanged 使用 setState 来改变状态 setState(() { _switch = res; }); }) ], ); } }
flutter为我们提供了几种进度条和指示器样式
1、LinearProgressIndicator 线性指示器
2、CircularProgressIndicator 圆圈指示器
3、CupertinoActivityIndicator IOS风格的进度指示器
可以设置的参数:
value:可以设置 0 - 1,来表示当前进度
valueColor:使用 AlwaysStoppedAnimation(Colors.red) 动画包裹颜色设置进度指示器的颜色
class ProgressDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.all(10), child: Column( children: [ LinearProgressIndicator( //线性指示器 value: .5, //进度 从0-1, .5就是一半 valueColor: AlwaysStoppedAnimation(Colors.red), //设置颜色要用动画包裹 ), SizedBox(height: 16), //设置间隔 16 Container( //设置容器 height: 100, //高 100 width: 100, //宽 100 child: CircularProgressIndicator( //圆圈指示器 // value: .8, valueColor: AlwaysStoppedAnimation(Colors.red), ), ), SizedBox(height: 16), CupertinoActivityIndicator(), //IOS风格的进度指示器 ]), ); } }
flutter为我们提供了 GestureDetector 手势检测器
class ClickDemo extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( //创建手势检测器 onTap: (){ //单击 print("点击"); }, onDoubleTap: (){ //双击 print("双击"); }, child: Text("点击组件"), ); } }
flutter为我们提供了两种常用输入组件:
TextField:默认典型输入框,没有 validator 验证
TextFromField:特点是可以带参数校验 validator 一般用于登录注册表单验证
TextField 源码
const TextField({ Key? key, this.controller, //控制器 this.focusNode, //焦点 this.decoration = const InputDecoration(), //装饰器 TextInputType? keyboardType, this.textInputAction, //输入动作 键盘右下角(完成,搜索,下一行) this.textCapitalization = TextCapitalization.none, this.style, //样式 this.strutStyle, this.textAlign = TextAlign.start, //文本格式 默认从左开始 this.textAlignVertical, this.textDirection, //文本方向 this.readOnly = false, ToolbarOptions? toolbarOptions, this.showCursor, this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, this.maxLines = 1, //最大行数 this.minLines, //最小行数 this.expands = false, this.maxLength, //最大字数 @Deprecated( 'Use maxLengthEnforcement parameter which provides more specific ' 'behavior related to the maxLength limit. ' 'This feature was deprecated after v1.25.0-5.0.pre.', ) this.maxLengthEnforced = true, this.maxLengthEnforcement, this.onChanged, //当值改变 this.onEditingComplete, this.onSubmitted, this.onAppPrivateCommand, this.inputFormatters, this.enabled, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorColor, this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.selectionControls, this.onTap, this.mouseCursor, this.buildCounter, this.scrollController, this.scrollPhysics, this.autofillHints = const <String>[], this.clipBehavior = Clip.hardEdge, this.restorationId, this.enableIMEPersonalizedLearning = true, })
TextFromField 源码
Key? key, this.controller, String? initialValue, FocusNode? focusNode, InputDecoration? decoration = const InputDecoration(), TextInputType? keyboardType, TextCapitalization textCapitalization = TextCapitalization.none, TextInputAction? textInputAction, TextStyle? style, StrutStyle? strutStyle, TextDirection? textDirection, TextAlign textAlign = TextAlign.start, TextAlignVertical? textAlignVertical, bool autofocus = false, bool readOnly = false, ToolbarOptions? toolbarOptions, bool? showCursor, String obscuringCharacter = '•', bool obscureText = false, bool autocorrect = true, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, bool enableSuggestions = true, @Deprecated( 'Use maxLengthEnforcement parameter which provides more specific ' 'behavior related to the maxLength limit. ' 'This feature was deprecated after v1.25.0-5.0.pre.', ) bool maxLengthEnforced = true, MaxLengthEnforcement? maxLengthEnforcement, int? maxLines = 1, int? minLines, bool expands = false, int? maxLength, ValueChanged<String>? onChanged, GestureTapCallback? onTap, VoidCallback? onEditingComplete, ValueChanged<String>? onFieldSubmitted, FormFieldSetter<String>? onSaved, FormFieldValidator<String>? validator, //与TextFiled不同的点,增加了 validator验证方法 List<TextInputFormatter>? inputFormatters, bool? enabled, double cursorWidth = 2.0, double? cursorHeight, Radius? cursorRadius, Color? cursorColor, Brightness? keyboardAppearance, EdgeInsets scrollPadding = const EdgeInsets.all(20.0), bool enableInteractiveSelection = true, TextSelectionControls? selectionControls, InputCounterWidgetBuilder? buildCounter, ScrollPhysics? scrollPhysics, Iterable<String>? autofillHints, AutovalidateMode? autovalidateMode, ScrollController? scrollController, String? restorationId, bool enableIMEPersonalizedLearning = true, })
简易登录
class InputDemo extends StatefulWidget { //创建有状态 widget @override State<InputDemo> createState() => _InputDemoState(); } class _InputDemoState extends State<InputDemo> { GlobalKey _key = GlobalKey<FormState>(); //key的泛型是表单状态,这样就可以通过key提交 TextEditingController _rootController = TextEditingController();//账号控制器 TextEditingController _passController = TextEditingController();//密码控制器 FocusNode _r = FocusNode(); //账号焦点 FocusNode _p = FocusNode(); //密码焦点 //当退出时销毁controller,否则占用内存 @override void dispose() { super.dispose(); //销毁父类 _rootController.dispose(); //销毁 _passController.dispose(); //销毁 _r.dispose(); //销毁 _p.dispose(); //销毁 } @override Widget build(BuildContext context) { return Form( //构建表单 key: _key, //构建表单提交key child: Column( children: [ TextFormField( //构建表单输入框 autofocus: true, //默认焦点聚集 focusNode: _r, //账号焦点 controller: _rootController, //引用账号控制器 decoration: InputDecoration( //输入框描述 prefixIcon: Icon(Icons.add), //输入框图标 labelText: "账号", //输入框标题 hintText: "默认文字" //输入框默认value ), validator: (v){ //只有使用 TextFormField 才可以用验证 validator 不用验证使用 TextField if(v == null || v.isEmpty){ return "账号不能为空!"; } }, textInputAction: TextInputAction.next, //回车后跳转下个输入框 onFieldSubmitted: (v){ //监听回车键 print("brath"); }, ), SizedBox(height: 8), //设置间隔高度 TextFormField( focusNode: _p, //密码焦点 controller: _passController, decoration: InputDecoration( prefixIcon: Icon(Icons.add), labelText: "密码", hintText: "输入密码" ), obscureText: true, validator: (v){ if(v == null || v.length < 5){ return "密码不能小于5位数!"; } }, textInputAction: TextInputAction.send, //将小键盘右下角的回车设置图标 ), SizedBox(height: 16), ElevatedButton( onPressed: (){ //当校验通过时输出 true 否则 false print((_key.currentState as FormState).validate().toString()); }, child: Text("提交"), ), ]), ); } }
var res = await Navigator.of(context).push( //跳转路由到 MenuPage 并可以接受返回值
这段代码用异步来监听返回值,优点是,无论是否点击按钮返回,都可以接收到返回值
还可以用 .then((value) => print(value)); 的方式来获取,这样更简洁,只有返回的时候才会监听,不返回不监听
// ignore_for_file: prefer_const_constructors, use_key_in_widget_constructors import 'package:flutter/material.dart'; //新页面导包 class LoginPage extends StatelessWidget { //无状态widget @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( //标题 title: Text("登录"), elevation: 10.0, centerTitle: true, ), body: ElevatedButton( //登录按钮 onPressed: () async { var res = await Navigator.of(context).push( //跳转路由到 MenuPage 并可以接受返回值 MaterialPageRoute( builder: (context) { return MenuPage( //传参 menuTitle menuTitle: "菜单", ); }, settings: RouteSettings( //路由设置 name: "参数", arguments: "我是参数", //向目标传参的数据 ), maintainState: false, fullscreenDialog: true, )); print(res); //打印返回值 }, child: Text("登录"), ), ); } } class MenuPage extends StatelessWidget { final String menuTitle; const MenuPage({Key? key,required this.menuTitle}) : super(key: key); @override Widget build(BuildContext context) { //通过 ModalRoute.of(context)?.settings.arguments; 来获取传参 dynamic arguments = ModalRoute.of(context)?.settings.arguments; return Scaffold( appBar: AppBar( title: Text(menuTitle + " " + arguments), ), body: ElevatedButton( onPressed: (){ Navigator.of(context).pop("Brath"); }, child: Text("返回按钮"), ), ); } }
Flutter中管理多个页面时有两个核心概念和类:Route
和Navigator
。 一个route
是一个屏幕或页面的抽象,Navigator
是管理route
的Widget
。Navigator
可以通过route
入栈和出栈来实现页面之间的跳转。 路由一般分为静态路由(即命名路由)和动态路由。
静态路由在通过Navigator
跳转之前,需要在MaterialApp
组件内显式声明路由的名称,而一旦声明,路由的跳转方式就固定了。通过在MaterialApp
内的routes
属性进行显式声明路由的定义。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( initialRoute: "/", // 默认加载的界面,这里为RootPage routes: { // 显式声明路由 //"/":(context) => RootPage(), "A":(context) => Apage(), "B":(context) => Bpage(), "C":(context) => Cpage(), }, // home: LoginPage(),//当设置命名路由后,home不用设置 ); } } 注意:如果指定了home属性,routes表则不能再包含此属性。 如上代码中【home: RootPage()】 和 【"/":(context) => RootPage()】两则不能同时存在。
例如:RootPage
跳转Apage
即:RootPage
—>Apage
Navigator.of(context).pushNamed("A");
一般方法中带有Name
多数是通过静态路由完成跳转的,如pushNamed
、pushReplacementNamed
、pushNamedAndRemoveUntil
等。
动态路由无需在MaterialApp
内的routes
中注册即可直接使用:RootPage —> Apage
Navigator.of(context).push(MaterialPageRoute( builder: (context) => Apage(), ));
动态路由中,需要传入一个Route
,这里使用的是MaterialPageRoute
,它可以使用和平台风格一致的路由切换动画,在iOS上左右滑动切换,Android上会上下滑动切换。也可以使用CupertinoPageRoute
实现全平台的左右滑动切换。 当然也可以自定义路由切换动画,使用PageRouteBuilder
:使用FadeTransition
做一个渐入过渡动画。
Navigator.of(context).push( PageRouteBuilder( transitionDuration: Duration(milliseconds: 250), // //动画时间为0.25秒 pageBuilder: (BuildContext context,Animation animation, Animation secondaryAnimation){ return FadeTransition( //渐隐渐入过渡动画 opacity: animation, child: Apage() ); } ) );
到现在为止,可能对路由有了一定的认识,,下面就结合具体方法来详细说明。 在这之前有必要说明: Navigator.of(context).push
和Navigator.push
两着并没有特别的区别,看源码也得知,后者其实就是调用了前者。 of
:获取Navigator
当前已经实例的状态。
flutter提供了 onGenerateRoute 来使用路由拦截器,作用于强制登录
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, initialRoute: "/", routes: { "/" :(context) => LoginPage(), // "menu" :(context) => MenuPage(), }, onGenerateRoute: (RouteSettings s){ //路由拦截器 print(s.name); //路由名称 if(s.name != "menu"){ //当该路由不等于 menu 强制跳转回首页 return MaterialPageRoute(builder: (context){ return LoginPage(); },settings: s); } switch(s.name){ case "menu" : //当该路由等于 menu 跳转至 menu 菜单 return MaterialPageRoute(builder: (context){ return MenuPage(); },settings: s); break; } }, // home: LoginPage(),//当设置命名路由后,home不用设置 ); } }
返回当前路由栈的上一个界面。 Navigator.pop(context);
见上,两者运行效果相同,只是调用不同,都是将一个page
压入路由栈中。直白点就是push
是把界面直接放入,pushNames
是通过路由名的方式,通过router使界面进入对应的栈中。 结果:直接在原来的路由栈上添加一个新的 page
。
替换路由,顾名思义替换当前的路由。 例如
Replacement.png
由图可知在BPage
使用替换跳转到Cpage
的时候,Bpage
被Cpage
替换了在堆栈中的位置而移除栈,CPage
默认返回的是APage
。
pushReplacement 使用的动态路由方式跳转:
Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => Cpage(), ));
pushReplacementNamed 使用的静态路由方式,
Navigator.of(context).pushReplacementNamed("/C");
两者运行效果相同。
popAndPushNamed:
Navigator.of(context).popAndPushNamed("/C");
其实和上面两个方法运行的结果也是一致,区别就是动画效果不一样:BPage
—>CPage
的时候,CPage
会同时有pop
的转场效果和从BPage
页push
的转场效果。简单来说就是CPage
先pop
到BPage
,在push
到CPage
。(不知道是不是卡顿的原因,笔者看起来区别不大)
综上:3中方法结果一样,只是调用方式和过渡动画的区别,开发者自行选择。
在使用上述方式跳转时,会按次序移除其他的路由,直到遇到被标记的路由(predicate
函数返回了true
)时停止。若 没有标记的路由,则移除全部。 当路由栈中存在重复的标记路由时,默认移除到最近的一个停止。
第一种
// 移除全部 Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), (Route router) => router == null);
或
// 移除全部 Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router == null);
此时的路由栈示意图:
RemoveUntil_all.png
可知出了要push
的CPage
,当前路由栈中所有的路由都被移除,CPage
变成根路由。
第二种:移除到RootPage停止
// "/"即为RootPage,标记后,移除到该路由停止移除 Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/')) 或 Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), (Route router) => router.settings.name == "/"); // 只是写法不一样
或
Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router.settings.name == "/"); 或 Navigator.of(context).pushNamedAndRemoveUntil("/C", ModalRoute.withName("/"));
此时的路由栈示意图:
RemoveUntil_until.png
push
到CPage
的时候,移除到RootPage
停止,CPage
默认返回RootPage
。
返回到指定的标记路由,若标记的路由为null
,则程序退出,慎用!!! 有时候我们需要根据业务需求判断:可能返回上一级路由,也可能返回上上级路由或是返回指定的路由等。这个时候就不能使用Replacemen
t和RemoveUntil
来替换、移除路由了。 例如:
until.png
Navigator.of(context).popUntil((route) => route.settings.name == "/"); 或 Navigator.of(context).popUntil(ModalRoute.withName("/"));
再例如:
要实现上述功能,从CPage
返回到APage
,并且不在MaterialApp
内的routes
属性进行显式声明路由。因为笔者觉得一个应用程序的界面太多了,如果每个界面都要显示声明路由,实在是不优雅。 因为需要返回APage
,还是需要标记路由,所有我们在之前跳转APage
的时候设置RouteSettings
,如下:
// 设置APage的RouteSettings Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name:"/A"), builder: (context) => APage(), ));
在CPage
需要返回的时候,调用就行:
Navigator.of(context).popUntil(ModalRoute.withName("/A"));
这样代码看起来很优雅,不会冗余。 另:
// 返回根路由 Navigator.of(context).popUntil((route) => route.isFirst);
用来判断是否可以导航到新页面,返回的bool
类型,一般是在设备带返回的物理按键时需要判断是否可以pop
。
可以理解为canPop
的升级,maybePop
会自动判断。如果当前的路由可以pop
,则执行当前路由的pop
操作,否则将不执行。
删除路由,同时执行Route.dispose
操作,无过渡动画,正在进行的手势也会被取消。
removeRoute
removeRoute.png
BPage
被移除了当前的路由栈。 如果在当前页面调用removeRoute
,则类似于调用pop
方法,区别就是无过渡动画,所以removeRoute
也可以用来返回上一页。
removeRouteBelow
移除指定路由底层的临近的一个路由,并且对应路由不存在的时候会报错。 同上。
综上:这个两个方法一般情况下很少用,而且必须要持有对应的要移除的路由。 一般用于立即关闭,如移除当前界面的弹出框等。
常见的路由传值分为两个方面:
向下级路由传值
返回上级路由时传值
要注意的是,我们一般说静态路由不能传值,并不是说一定不能用于传值,而是因为静态路由一般需要在MaterialApp
内的routes
属性进行显式声明,在这里使用构造函数传值无实际意义。 如:
MaterialApp( initialRoute: "/", // 默认加载的界面,这里为RootPage routes: { // 显式声明路由 "/":(context) => RootPage(), "/A":(context) => APage("title"), // 在这里传参无实际意义,一般需要传入的参数都是动态变化的 "/B":(context) => BPage(), "/C":(context) => CPage(), }, // home: RootPage(), );
1、构造函数传值
首先构造一个可以带参数的构造函数:
class APage extends StatefulWidget { String title; APage(this.title); @override _APageState createState() => _APageState(); }
在路由跳转的时候传值:
Navigator.of(context).push(MaterialPageRoute( builder: (context) => APage("这是传入的参数"), ));
在APage拿到传入的值:
// 在 StatefulWidget 使用[widget.参数名] Container( child: Text(widget.title), )
2、ModalRoute 传值
在Navigator.of(context).push
的跳转方式中,MaterialPageRoute
的构造参数中 可以看到有RouteSettings
的属性,RouteSettings
就是当前路由的基本信息
const RouteSettings({ this.name, this.isInitialRoute = false, this.arguments, // 存储路由相关的参数Object });
路由跳转时设置传递参数:
Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name:"/A",arguments: {"argms":"这是传入A的参数"}), builder: (context) => APage(), )); 或使用静态路由pushName: Navigator.of(context).pushNamed("/A",arguments:{"argms":"这是传入A的参数"});
在APage
中取值:
Map argms = ModalRoute.of(context).settings.arguments; print(argms["argms"]);
就是在调用APage
中调用pop
返回路由的时候传参
Navigator.of(context).pop("这是pop返回的参数值");
在上一级路由获取:
Navigator.of(context).push(MaterialPageRoute( builder: (context) => APage(), )).then((value){ // 获取pop的传值 print(value); }); 或 String value = await Navigator.of(context).pushNamed('/xxx');
textDirection: TextDirection.ltr, //组件排列方式 mainAxisSize: MainAxisSize.max, //主轴最大值 mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴布局 crossAxisAlignment: CrossAxisAlignment.start, //纵轴排列方式
Column - 纵向
概念:纵轴的宽度,默认使用子组件最大宽度
此时,红色和黄色容器宽度为 100 绿色为150,整个容器就会使用 最大的子组件宽度 150 来表示自己
Column 代码演示:
class LayoutDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("布局练习"), ), body: Container( color: Colors.grey, child: Column(children: [ Container( width: 100, height: 100, color: Colors.red, ), Container( width: 150, height: 100, color: Colors.green, ), Container( width: 100, height: 100, color: Colors.yellow, ), ]), ) ); } }
Row - 横向
概念:和Colunm相似,纵轴的宽度,默认使用子组件最大高度
此时,红色和黄色容器高度为 100 绿色为200,整个容器就会使用 最大的子组件高度 200 来表示自己
Row 代码演示
class LayoutDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("布局练习"), ), body: Container( color: Colors.grey, child: Row( textDirection: TextDirection.ltr, //组件排列方式 mainAxisSize: MainAxisSize.max, //主轴最大值 mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴布局 crossAxisAlignment: CrossAxisAlignment.start, //纵轴排列方式 children: [ Container( width: 100, height: 200, color: Colors.red, ), Container( width: 150, height: 100, color: Colors.green, ), Container( width: 100, height: 100, color: Colors.yellow, ), ]), ) ); } }
flutter为我们提供了 Flex 这个 widget 来制造弹性布局
Flex 默认 必传方向 Axis
children使用 Expanded来包裹,可以设置 flex权重,根据数字大小来设置权重
class LayoutDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("布局练习"), ), body: Container( color: Colors.grey, child: Flex( direction: Axis.vertical, children: [ Expanded(child: Container( width: 100, height: 200, color: Colors.red, ),flex: 2,), Expanded(child: Container( width: 100, height: 200, color: Colors.green, ),flex: 2,), Expanded(child: Container( width: 100, height: 200, color: Colors.yellow, ),flex: 2,), ], ), )); } }
flutter为我们提供了 Wrap 这个 widget 来制造弹性布局
使用 有状态的 StatefulWidget 来构建 wrap 布局
class WrapDemo extends StatefulWidget { @override State<WrapDemo> createState() => _WrapDemoState(); } class _WrapDemoState extends State<WrapDemo> { var list = <int>[]; @override void initState() { super.initState(); for (var i = 0; i < 20; i++) { //初始化时向数组添加 20 个数据 list.add(i); } } @override Widget build(BuildContext context) { return Wrap( direction: Axis.horizontal, //设置方向 alignment: WrapAlignment.start, //布局参数 spacing: 1.0, //边距 runSpacing: 1.0, //边距 children: list.map((e) => Container( height: 100, width: 100, child: Text( e.toString(), style: TextStyle( color: Colors.black, fontSize: 20 ) ), color: Colors.blue, )).toList() ); } }
flutter为我们提供了 Stack这个 widget 来制造层叠布局
我们设置了两个容器div,在层叠布局中,如果后一个容器,比前面的容器大,那么就会遮挡,原理是为什么?
flutter在绘画时,从x 0 y 0开始绘画,也就是 左上角
意味着两个容器绘画开始的坐标都是相同的,只不过宽高不一样
那么如果第一个容器宽高为 100 第二个为150 就理所应当的遮住啦!
class StackDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.grey, width: double.infinity, child: Stack( alignment: AlignmentDirectional.center, //居中布局 children: [ Container( color: Colors.green, width: 150, height: 150, ), Container( color: Colors.red, width: 100, height: 100, ), ], ), ); } }
flutter为我们提供了 Positioned 这个 widget 来制造层叠布局
如果 Positioned 设置了宽高,那么子组件不生效
//如果设置了 • top: 10, • bottom: 10, • 那么就不能设置高度 height //如果设置了 • left: 10, • right: 10, • 那么就不能设置宽度 width
代码演示:
class StackDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.grey, width: double.infinity, child: Stack( alignment: AlignmentDirectional.center, children: [ Container( color: Colors.green, width: 150, height: 150, ), Container( color: Colors.red, width: 100, height: 100, ), Positioned( // width: 100, // height: 100, child: Container( color: Colors.yellow, width: 300, height: 300, ), top: 50, left: 150, right: 150, bottom: 50, ) ], ), ); } }
flutter为我们提供了 Align 这个 widget 来制造层叠布局
要点:只会相对于父组件来定位,而不是屏幕
class AlignDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 200, height: 200, color: Colors.green, child: Align( alignment: Alignment.center, //居中 child: FlutterLogo( //flutter的logo size: 60, //宽高60 ), ), ); } }
flutter为我们提供了 padding 和 margin 这量个 属性来设置内外边距
内边距:当前容器内的组件对于当前容器的距离
外边距:当前容器距离父类容器的距离
代码演示:
class PaddingAndMarginDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: Colors.red, //设置外边距(当前容器距离父类容器的距离) // margin: EdgeInsets.only(left: 10),//单独设置外边距 margin: EdgeInsets.all(10),//四个方向设置外边距 //设置内边距(当前容器内的组件对于当前容器的距离) padding: EdgeInsets.all(20), child: Text("我有边距"), ); } }
要点:子widget没有设置宽高的时候取自己设置的最大宽高
ConstrainedBox的特点就是可以设置最大或者最小的宽高,子组件怎么设置都不可以超过这个宽高
代码演示:
class ConstrainedBoxDemo extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints( maxHeight: 100, maxWidth: 100, minHeight: 50, minWidth: 50, ), child: Container( width: 500, height: 500, color: Colors.red, ), ); } }
要点:如果父容器指定了宽高,那么子组件不可以修改宽高
代码演示:
class ConstrainedBoxDemo extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( // width: 100, // height: 100, child: Container( color: Colors.red, width: 200, height: 200, ), ); } }
flutter为我们提供了 BoxDecoration 这量个 widget 来设置样式装饰
代码演示:
class ConstrainedBoxDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(20), width: double.infinity, child: DecoratedBox( //装饰器 decoration: BoxDecoration( // color: Colors.red gradient: LinearGradient( //渐变颜色 colors: [ Colors.red, //从红色 Colors.green, //到绿色 ], ), borderRadius: BorderRadius.circular(10.0), //圆角度 boxShadow: [ BoxShadow( color: Colors.black, offset: Offset(2.0,2.0), blurRadius: 2, ) ], ), child: Padding( padding: EdgeInsets.only( left: 100, right: 100, top: 20, bottom: 20 ), child: Text( "渐变色~", style: TextStyle( color: Colors.white ), textAlign: TextAlign.center, ), ), ), ); } }
要点:当Container设置了 foregroundDecoration(前景) 的背景颜色,那么子组件将不会显示
要点:当Container设置了 decoration(背景) 的背景颜色,那么子组件将会显示
设置内边距并旋转 0.5
代码演示
class ContarinerDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(100), //设置内边距 width: 100, height: 100, child: Text("data"), decoration: BoxDecoration( //设置背景 foregroundDecoration设置前景,会遮挡 color: Colors.red ), transform: Matrix4.rotationZ(0.5), //旋转,可选坐标轴 ); } }
1.MateriaApp是flutter的根节点,flutter规定必须要 MateriaApp 来作为根节点展示
2.在MateriaApp可以设置路由,每个子页面必须由 Scaffold 来包裹
3.每个 Scaffold 包含两个部分 appBar(头部),body(展示体)
Scaffold 中的 AppBar 有很多特性:
代码演示
class PageDemo extends StatefulWidget { @override State<PageDemo> createState() => _PageDemoState(); } class _PageDemoState extends State<PageDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( //设置左侧图标 onPressed: () { print("点击了!"); }, icon: Icon(Icons.home) //左边房子图片 ), // centerTitle: true, //设置centerTitle为true,可将标题居中 title: Text( "演示", style: TextStyle(fontSize: 15), ), actions: [ //设置左侧图标 IconButton( onPressed: () { print("点击了加!"); }, icon: Icon(Icons.add)), IconButton( onPressed: () { print("点击了减!"); }, icon: Icon(Icons.remove)), IconButton( onPressed: () { print("点击了灯!"); }, icon: Icon(Icons.wb_iridescent_rounded)), ], elevation: 10.0, ), // body: , ); } }
Flutter提供 顶部TabBar选项卡
代码演示:
class PageDemo extends StatefulWidget { @override State<PageDemo> createState() => _PageDemoState(); } class _PageDemoState extends State<PageDemo> with SingleTickerProviderStateMixin{ List tabs = ["Fullter", "Andiord", "IOS"]; //选项卡数组 //选项控制器 late TabController _controller = TabController(length: tabs.length, vsync: this); //选项索引 int _index = 0; /** * 初始化 **/ @override void initState() { _controller = TabController( //创建新控制器 initialIndex: _index, //设置初始索引 length: tabs.length, //长度为数组疮毒 vsync: this ); _controller.addListener(() { //监听器 setState(() { //监听状态,当状态改变,把控制器索引赋值到选项索引,用来做内容切换 _index = _controller.index; }); }); super.initState(); } /** * 销毁 **/ @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 10.0, //阴影 bottom: TabBar( controller: _controller, //选项接收控制器 tabs: tabs.map((e) => Tab( //遍历选项 text: e, //文本为map中的内容 )).toList(), //转为集合 ), ), body: Text(_index.toString()), //body可以根据index来输出不同内容 ); } }
使用Flutter提供 顶部TabBarView组件来设置选项卡
代码演示:
class PageDemo extends StatefulWidget { //指定三个容器页面,在下方调用 widget.widgets 因为泛型指定了 Widget 所以都是Widget数组 List<Widget> widgets = [FlutterView(),AndroidView(),IOSView()]; @override State<PageDemo> createState() => _PageDemoState(); } class _PageDemoState extends State<PageDemo> with SingleTickerProviderStateMixin{ List tabs = ["Fullter", "Andiord", "IOS"]; late TabController _controller = TabController(length: tabs.length, vsync: this); @override void initState() { _controller = TabController( length: tabs.length, vsync: this ); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 10.0, bottom: TabBar( controller: _controller, tabs: tabs.map((e) => Tab( text: e, )).toList(), ), ), body: TabBarView( //使用 TabBarView 包裹body children: widget.widgets, //内容就是widgets controller: _controller, //通过控制器来切换 ) ); } } class FlutterView extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Text("FlutterView"), ); } } class AndroidView extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Text("AndroidView"), ); } } class IOSView extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Text("IOSView"), ); } }
使用Flutter提供 侧抽屉 Drawer 组件来设置抽屉样式
要点:drawer是 Scaffold 中的属性,并不是 AppBar 的
代码演示:
class myDrawer extends StatelessWidget { @override Widget build(BuildContext context) { return Drawer( child: MediaQuery.removePadding( //删除边距 context: context, child: Column( crossAxisAlignment: CrossAxisAlignment.start, //从左开始 children: [ Padding(padding: EdgeInsets.only(top: 40), child: Text("Brath"), ) ], ), removeTop: true, //删除顶部 ), ); } }
使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到点击卡片切换页面
代码演示:
class BottomNavigatorBarDemo extends StatefulWidget { const BottomNavigatorBarDemo({ Key? key }) : super(key: key); @override State<BottomNavigatorBarDemo> createState() => _BottomNavigatorBarDemoState(); } class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> { int _index = 0; //页面index @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("底部选项卡"), ), bottomNavigationBar: BottomNavigationBar( //底部选项widget items: [ //三个选项 BottomNavigationBarItem( icon: Icon(Icons.add), label: "新增" ), BottomNavigationBarItem( icon: Icon(Icons.home), label: "我的" ), BottomNavigationBarItem( icon: Icon(Icons.remove), label: "减少" ), ], currentIndex: _index, //当前index onTap: (v){ //当点击时,把当前索引状态改为点击的索引 setState(() { _index = v; }); }, ), body: Center(child: Text(_index.toString())), //展示当前索引 ); } }
使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到按钮居中布局
要点:两种实现方式,BottomNavigationBar中如果BottomNavigationBarItem超过三个需要设置type
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。