# 【Flutter】Flutter 学习文档
# 前言:如果你要学习 flutter,那么你一定要会 dart 语言,因为 flutter 是基于 dart 来封装的一个 UI 组件包
# 本文使用 Typort 书写,禁止转载。
# 学习基础要求:
# 后端:有语言基础(C/C++/C#/Java/python/Golang 都可以)
# 前端 :(JavaScript,Html,CSS) 的人来学习。如果 0 基础,请先学习任意一门后端语言并熟练掌握!
# Dart 语言学习:
安装 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 查看环境
1
| Dart VM version: 2.3.3-dev.0.0.flutter-b37aa3b036 (Tue Jun 11 13:00:50 2019 +0000) on "windows_x64"
|
# 注释:
1 2 3 4 5 6 7 8 9 10
| /* *多行注释 *多行注释 */ /** * 文档注释 与Java相同 */ ///文档注释 dart独有
|
# 变量定义:
dart 语言特点:
自动类型转换,var 声明的变量可以是任意类型!
dart 拥有两种变量定义方式。
指定类型:
String name = “brath”;
或者
类型推导
var name = “brath”; // 推导为任意类型
final name = “brath”; // 不可修改,定义常量,可以初始化
const name = “brath”; // 不可修改,定义常量,不可以初始化,可以被构造修改
1 2 3 4 5 6 7 8 9 10 11 12
| 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 =
默认情况下,dart 的所有 class 都是隐式接口!
# Dart 函数使用:
1 2 3 4 5 6 7 8
| void main(List<String> args) { print(sum(51, 14891)); }
int sum(int a,int b){ return a + b; }
|
# 函数参数:
必选参数,不能有默认值,可选参数,可以有默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| main(){ sayHello("why"); }
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"); }
|
# 函数是一等公民:
函数可以作为另外一个函数的参数!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void main(List<String> args) {
test(() => print("箭头")); }
void test(Function foo){ see();
}
void see(){ print("see!"); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
void main(List<String> args) {
var num = demo(); print(num(20,12)); }
typedef Calculate = int Function(int num1,int num2);
void test(Calculate calculate){ calculate(20,30); }
Calculate demo(){ return (num1,num2){ return num1 * num2; }; }
|
# 赋值运算符:
1 2 3 4 5 6 7
| Flutter中,有诡异的赋值运算符 比如 name ??="111"; 解释:当原来的变量有值时,不执行 当原来的变量为null时,执行
或者 var name = name ?? "11"; 解释: 当name不为空时使用name,为空使用后面的变量
|
# 级联运算符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void main(){ var p = Person() ..name = "brath" ..eat(); ..run(); }
class Person(){ String name; void eat(){ print("吃"); } void run(){ print("跑"); } }
|
# For 循环和 Switch 循环与 JS 和 Java 基本一致
# 构造函数:
1 2 3 4 5 6 7 8 9 10
| class Person{ String name; int age; double height; //默认构造函数 Person(this.name.this.age); //命名构造函数,指定名字的构造函数 Person.NameCon(this.name.this.age,this.height); }
|
# dynamic:
1 2 3 4 5 6 7 8
| dynamic代表任意类型 dynamic obj = "obj";
print(obj.subString(1));
Object obj = "obj";
print(obj.subString(1));
|
# 初始化列表:
1 2 3 4 5 6 7 8 9 10 11
| mian(){ var p = Person('brath'); }
class Person{ final String name; final int age; Person(this.name,{int age}): this.age = age ?? 10; }
|
# 构造函数重定向:
1 2 3 4 5 6 7 8 9 10 11 12
| mian(){ }
class Person{ String name; int age; Person(String name) : this._internal(name,0); Person._internal(this.name,this.age) }
|
# 工厂构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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"); } } }
|
# Getter 和 Setter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void main(List<String> args) { final p = Person(); p.name = "brath"; print(p.name); p.setName("brath.cloud"); print(p.getName); }
class Person{ late String name;
void setName(String name) => this.name = name; String get getName => name; }
|
# 隐式接口:
# 类的混入:
1 2
| 用class声明的类不可以混入其他类 要混入其他类,使用 mixin 声明该类,并在混入时用with关键字来连接被混入的类
|
# 类属性和类方法:
1 2 3 4
| 类属性:在类中不用static声明的变量,叫做成员变量,不可以被类直接调用 静态属性:在类中用static声明的变量,叫做静态属性,类属性,可以被类直接调用 类方法:在类中不用static声明的方法,叫做成员方法,不可以被类直接调用 静态方法:在类中用static声明的方法,叫做静态方法,类属性,可以被类直接调用
|
# 枚举的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 }
|
# 库的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
import 'utils/TimeUtil' as timeUtil;
show hide import 'utils/TimeUtil' show timeUtil; import 'utils/TimeUtil' hide timeUtil;
import 'utils/TimeUtil' show timeUtil, FileUtil; import 'utils/TimeUtil' hide timeUtil, FileUtil;
|
# 抽取公共库文件:
1 2 3 4 5 6 7 8 9 10
| 以上方法导入库的时候总是会遇到一些问题,比如如果有100个方法,你只想用50个,那么你就要用50个show或者50个hide,但是dart提供了一种方式,就是抽取库到一个公共类中。 前面提到过,dart中所有文件都是一个库,那么我们把需要导入的库,全部export到一个库中,在引用这个库,就不用担心过多引入了。 公共库: util.dart export 'util/TimeUtil' export 'util/FileUtil'
我的代码: import 'util';
|
# 使用第三方库:
1 2 3 4 5 6
| name: 库名 desciption: 描述 dependencies: 依赖 http: ^0.13.4 怎么找库? https://pub.dev/packages/http
|
点击 installing
把 dependencies 内容复制到代码中
1 2 3 4 5 6
| name: coderwhy desciption: a dart dependencies: http: ^0.12.0+4 environment: sdk: '>=2.10.0 < 3.0.0'
|
进入当前类文件夹,终端输入 pub get 就会下载对应库包
1 2 3 4 5 6 7 8 9
| import 'package:http/http.dart' as http;
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 来捕获异常
1 2 3 4 5 6 7 8 9 10 11 12
| void main() async{ try{ await test1(); }catch(e){ print(e); } }
test1() async{ print(11~/0); }
|
# 接下来介绍 我们的 Flutter!
# 最好的跨平台解决方案 Flutter
架构对比:
# GUP 绘制出图像,放到 Buffer 缓存中,手机屏幕根据刷新率来读取缓存的操作,就是展示图像。
# 引出了一个概念:垂直同步
为什么要有垂直同步?
来看一个例子:假设我 GPU 每秒帧率产生 60,手机屏幕每秒也是接受 60,这时可以正常显示。
如果突然每秒帧率提高到 120,手机屏幕可能会来不及读取缓存导致画面重叠、撕裂
开启垂直同后,会有两块缓存区域。
垂直同步就限制了手机屏幕读取缓存和 GPU 产生的速度,开启垂直同步后,GPU 将画面写入到第一个缓存中,第一个缓存会复制内容(地址交换)到第二个缓存中,当两份缓存都存在这一帧,就会发送一个 VSync 的信号,告诉 GPU 可以绘制下一张图,然后手机屏幕来显示第二个缓存中的内容,这样就可以避免图像撕裂。
# 一个简单的 flutter 结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import 'package:flutter/material.dart';
void main() { runApp(const MyApp()); }
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( 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 命令安装
在安装目录下面执行
1
| git clone -b stable https://github.com/flutter/flutter.git
|
安装完成之后,可以在安装根目录,找的 flutter_console.bat 文件,双击运行
# 配置 Flutter 运行环境变量
在用户变量里面编辑或者添加 Path 条目,把 Flutter 的 bin 目录路径添加在里面
# 运行 Flutter
在命令行运行 flutter doctor,它会下载它自己的依赖项并自行编译,一般情况是会报错提示,多半是 Android SDK 找不到什么的,如果出错了,就按照错误信息网上查一下就解决了。
我的已经处理完成的
# 编辑器设置
我用的 Android Studio,上面连接里面有不同系统和编辑器的流程,详情可以前往查看
Android Studio 的开发环境就不说了,需要的可以自行百度。Android Studio 配置 Flutter 开发主要是 Flutter 和 Dart 两个插件
File – Settings – Plugins – Marketplace 然后在搜索里面搜索 Flutter 和 Dart 安装就可以了。
安装完插件,重启一下 Android Studio 基本就配置完成了,可以新建 Flutter 项目了。
# 新建 Flutter 项目
File – New – New Flutter Project
选择 Flutter Application
然后到这个最后一步的时候,会有一点小问题
Flutter SDK path 这一栏第一次默认是空的,需要手动选择,选择我们最开始下载的 Flutter SDK,选择根目录,就可以了
# 至此 Flutter 的开发环境安装完毕!
# 现在开始学习 Flutter 的基础组件,以及进阶理论!
# flutter 学习笔记 auther:Brath
# 所有的重点都在代码的注释中标注!
# 创建项目:
到想存储项目的文件路径,打开 CMD,输入 flutter create 项目名称即可
vscode 下载好插件,dart 和 flutter 打开对应 flutter 文件,即可开始编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import 'package:flutter/material.dart';
main() { runApp(MyApp()); }
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( ); } }
|
# 特性:
无状态的 widget 是静态页面
有状态的 widget 是动态页面
# 要点:
# tips:flutter 的 main 入口调用第一个 widget 需要该 widget 使用 MaterialApp () 作为首个 widget
因为 MaterialApp 包含了路由主题等等组件,flutter 规定只能用 MaterialApp 当作根节点
# 使用 MaterialApp 的 home 属性来指定页面
1 2 3 4 5 6 7 8 9
| class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } }
|
均为可选参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| Container({ Key? key, this.alignment, this.padding, this.color, this.decoration, this.foregroundDecoration, double? width, double? height, BoxConstraints? constraints, this.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 文本组件 (widget):
Text 默认传一个文本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class TextDemo extends StatelessWidget @override Widget build(BuildContext context) { return Container( width: double.infinity, color: Colors.blue, child: Text( "文本" * 20, maxLines: 1, textDirection: TextDirection.ltr, textAlign: TextAlign.center, style: TextStyle( fontSize: 30, color: Colors.teal ), ) ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const Text( String this.data, { Key? key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, 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
1 2
| RaisedButton 已遗弃 FlatButton 已遗弃
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ ElevatedButton( onPressed:(){ }, child: Text( "漂浮按钮" ) ) ], ); } }
|
# TextButton:扁平按钮 / 文本按钮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ TextButton( onPressed: (){ }, child: Text( "扁平按钮" )) ], ); } }
|
# TextButton.icon:带图标的扁平按钮 / 文本按钮
1 2 3 4 5 6 7 8 9 10 11 12 13
| class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ TextButton.icon(onPressed: (){}, icon: Icon(Icons.add), label: Text("图标按钮")) ], ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ OutlinedButton(onPressed: (){}, child: Text("无阴影按钮")) ], ); } }
|
1 2 3 4 5 6 7 8 9 10 11
| class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ IconButton(onPressed: (){}, icon: Icon(Icons.home)) ], ); } }
|
flutter 提供了四种图片加载方式:
1、Image.network // 从网络获取图片
2、Image.asset // 从项目本地获取图片
3、Image.file // 从文件路径获取图片
4、Image.memory // 从手机内存,存储中获取图片
使用 asset 需要设置 pubspec.yaml 中的 assets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
Check 复选框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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){ setState(() { _check = res!; }); }), ], ); } }
|
Switch 开关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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){ setState(() { _switch = res; }); }) ], ); } }
|
flutter 为我们提供了几种进度条和指示器样式
1、LinearProgressIndicator 线性指示器
2、CircularProgressIndicator 圆圈指示器
3、CupertinoActivityIndicator IOS 风格的进度指示器
可以设置的参数:
value:可以设置 0 - 1,来表示当前进度
valueColor:使用 AlwaysStoppedAnimation (Colors.red) 动画包裹颜色设置进度指示器的颜色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class ProgressDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.all(10), child: Column( children: [ LinearProgressIndicator( value: .5, valueColor: AlwaysStoppedAnimation(Colors.red), ), SizedBox(height: 16), Container( height: 100, width: 100, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.red), ), ), SizedBox(height: 16), CupertinoActivityIndicator(), ]), ); } }
|
flutter 为我们提供了 GestureDetector 手势检测器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class ClickDemo extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( onTap: (){ print("点击"); }, onDoubleTap: (){ print("双击"); }, child: Text("点击组件"), ); } }
|
flutter 为我们提供了两种常用输入组件:
TextField:默认典型输入框,没有 validator 验证
TextFromField:特点是可以带参数校验 validator 一般用于登录注册表单验证
# TextField 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| 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 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| 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, 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, })
|
简易登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| class InputDemo extends StatefulWidget { @override State<InputDemo> createState() => _InputDemoState(); }
class _InputDemoState extends State<InputDemo> { GlobalKey _key = GlobalKey<FormState>(); TextEditingController _rootController = TextEditingController(); TextEditingController _passController = TextEditingController(); FocusNode _r = FocusNode(); FocusNode _p = FocusNode();
@override void dispose() { super.dispose(); _rootController.dispose(); _passController.dispose(); _r.dispose(); _p.dispose(); } @override Widget build(BuildContext context) { return Form( key: _key, child: Column( children: [ TextFormField( autofocus: true, focusNode: _r, controller: _rootController, decoration: InputDecoration( prefixIcon: Icon(Icons.add), labelText: "账号", hintText: "默认文字" ), validator: (v){ 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: (){ print((_key.currentState as FormState).validate().toString()); }, child: Text("提交"), ), ]), ); } }
|
# Flutter 路由工具:
var res = await Navigator.of (context).push ( // 跳转路由到 MenuPage 并可以接受返回值
这段代码用异步来监听返回值,优点是,无论是否点击按钮返回,都可以接收到返回值
还可以用 .then ((value) => print (value)); 的方式来获取,这样更简洁,只有返回的时候才会监听,不返回不监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import 'package:flutter/material.dart';
class LoginPage extends StatelessWidget { @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( MaterialPageRoute( builder: (context) { return MenuPage( 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) { 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
属性进行显式声明路由的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( initialRoute: "/", routes: { "A":(context) => Apage(), "B":(context) => Bpage(), "C":(context) => Cpage(), }, ); } } 注意:如果指定了home属性,routes表则不能再包含此属性。 如上代码中【home: RootPage()】 和 【"/":(context) => RootPage()】两则不能同时存在。
|
例如: RootPage
跳转 Apage
即: RootPage
—> Apage
1
| Navigator.of(context).pushNamed("A");
|
一般方法中带有 Name
多数是通过静态路由完成跳转的,如 pushNamed
、 pushReplacementNamed
、 pushNamedAndRemoveUntil
等。
# 动态路由
动态路由无需在 MaterialApp
内的 routes
中注册即可直接使用:RootPage —> Apage
1 2 3
| Navigator.of(context).push(MaterialPageRoute( builder: (context) => Apage(), ));
|
动态路由中,需要传入一个 Route
, 这里使用的是 MaterialPageRoute
,它可以使用和平台风格一致的路由切换动画,在 iOS 上左右滑动切换,Android 上会上下滑动切换。也可以使用 CupertinoPageRoute
实现全平台的左右滑动切换。
当然也可以自定义路由切换动画,使用 PageRouteBuilder
: 使用 FadeTransition
做一个渐入过渡动画。
1 2 3 4 5 6 7 8 9 10 11 12
| Navigator.of(context).push( PageRouteBuilder( transitionDuration: Duration(milliseconds: 250), pageBuilder: (BuildContext context,Animation animation, Animation secondaryAnimation){ return FadeTransition( opacity: animation, child: Apage() ); } ) );
|
到现在为止,可能对路由有了一定的认识,,下面就结合具体方法来详细说明。
在这之前有必要说明:
Navigator.of(context).push
和 Navigator.push
两着并没有特别的区别,看源码也得知,后者其实就是调用了前者。
of
:获取 Navigator
当前已经实例的状态。
# 路由拦截:
flutter 提供了 onGenerateRoute 来使用路由拦截器,作用于强制登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, initialRoute: "/", routes: { "/" :(context) => LoginPage(), }, onGenerateRoute: (RouteSettings s){ print(s.name); if(s.name != "menu"){ return MaterialPageRoute(builder: (context){ return LoginPage(); },settings: s); } switch(s.name){ case "menu" : return MaterialPageRoute(builder: (context){ return MenuPage(); },settings: s); break; } }, ); } }
|
# 路由方法解释:
# pop
返回当前路由栈的上一个界面。
Navigator.pop(context);
# push / pushNamed :
见上,两者运行效果相同,只是调用不同,都是将一个 page
压入路由栈中。直白点就是 push
是把界面直接放入, pushNames
是通过路由名的方式,通过 router 使界面进入对应的栈中。
结果:直接在原来的路由栈上添加一个新的 page
。
# pushReplacement / pushReplacementNamed / popAndPushNamed
替换路由,顾名思义替换当前的路由。
例如
Replacement.png
由图可知在 BPage
使用替换跳转到 Cpage
的时候, Bpage
被 Cpage
替换了在堆栈中的位置而移除栈, CPage
默认返回的是 APage
。
# pushReplacement 使用的动态路由方式跳转:
1 2 3
| Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => Cpage(), ));
|
# pushReplacementNamed 使用的静态路由方式,
1
| Navigator.of(context).pushReplacementNamed("/C");
|
两者运行效果相同。
# popAndPushNamed:
1
| Navigator.of(context).popAndPushNamed("/C");
|
其实和上面两个方法运行的结果也是一致,区别就是动画效果不一样: BPage
—> CPage
的时候, CPage
会同时有 pop
的转场效果和从 BPage
页 push
的转场效果。简单来说就是 CPage
先 pop
到 BPage
,在 push
到 CPage
。(不知道是不是卡顿的原因,笔者看起来区别不大)
综上:3 中方法结果一样,只是调用方式和过渡动画的区别,开发者自行选择。
# pushAndRemoveUntil / pushNamedAndRemoveUntil
在使用上述方式跳转时,会按次序移除其他的路由,直到遇到被标记的路由( predicate
函数返回了 true
)时停止。若 没有标记的路由,则移除全部。
当路由栈中存在重复的标记路由时,默认移除到最近的一个停止。
# 第一种
1 2 3
| Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), (Route router) => router == null);
|
或
1 2
| Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router == null);
|
此时的路由栈示意图:
RemoveUntil_all.png
可知出了要 push
的 CPage
,当前路由栈中所有的路由都被移除, CPage
变成根路由。
# 第二种:移除到 RootPage 停止
1 2 3 4 5 6 7
| Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/')) 或 Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => CPage()), (Route router) => router.settings.name == "/");
|
或
1 2 3
| 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
。
# popUntil
返回到指定的标记路由,若标记的路由为 null
,则程序退出,慎用!!!
有时候我们需要根据业务需求判断:可能返回上一级路由,也可能返回上上级路由或是返回指定的路由等。这个时候就不能使用 Replacemen
t 和 RemoveUntil
来替换、移除路由了。
例如:
until.png
1 2 3
| Navigator.of(context).popUntil((route) => route.settings.name == "/"); 或 Navigator.of(context).popUntil(ModalRoute.withName("/"));
|
再例如:
要实现上述功能,从 CPage
返回到 APage
,并且不在 MaterialApp
内的 routes
属性进行显式声明路由。因为笔者觉得一个应用程序的界面太多了,如果每个界面都要显示声明路由,实在是不优雅。
因为需要返回 APage
,还是需要标记路由,所有我们在之前跳转 APage
的时候设置 RouteSettings
,如下:
1 2 3 4 5
| Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name:"/A"), builder: (context) => APage(), ));
|
在 CPage
需要返回的时候,调用就行:
1
| Navigator.of(context).popUntil(ModalRoute.withName("/A"));
|
这样代码看起来很优雅,不会冗余。
另:
1 2
| Navigator.of(context).popUntil((route) => route.isFirst);
|
# canPop
用来判断是否可以导航到新页面,返回的 bool
类型,一般是在设备带返回的物理按键时需要判断是否可以 pop
。
# maybePop
可以理解为 canPop
的升级, maybePop
会自动判断。如果当前的路由可以 pop
,则执行当前路由的 pop
操作,否则将不执行。
# removeRoute/removeRouteBelow
删除路由,同时执行 Route.dispose
操作,无过渡动画,正在进行的手势也会被取消。
# removeRoute
removeRoute.png
BPage
被移除了当前的路由栈。
如果在当前页面调用 removeRoute
,则类似于调用 pop
方法,区别就是无过渡动画,所以 removeRoute
也可以用来返回上一页。
# removeRouteBelow
移除指定路由底层的临近的一个路由,并且对应路由不存在的时候会报错。
同上。
综上:这个两个方法一般情况下很少用,而且必须要持有对应的要移除的路由。
一般用于立即关闭,如移除当前界面的弹出框等。
# 路由传值
常见的路由传值分为两个方面:
要注意的是,我们一般说静态路由不能传值,并不是说一定不能用于传值,而是因为静态路由一般需要在 MaterialApp
内的 routes
属性进行显式声明,在这里使用构造函数传值无实际意义。
如:
1 2 3 4 5 6 7 8 9 10
| MaterialApp( initialRoute: "/", routes: { "/":(context) => RootPage(), "/A":(context) => APage("title"), "/B":(context) => BPage(), "/C":(context) => CPage(), }, );
|
# 向下级路由传值
# 1、构造函数传值
首先构造一个可以带参数的构造函数:
1 2 3 4 5 6
| class APage extends StatefulWidget { String title; APage(this.title); @override _APageState createState() => _APageState(); }
|
在路由跳转的时候传值:
1 2 3
| Navigator.of(context).push(MaterialPageRoute( builder: (context) => APage("这是传入的参数"), ));
|
在 APage 拿到传入的值:
1 2 3 4
| Container( child: Text(widget.title), )
|
# 2、ModalRoute 传值
在 Navigator.of(context).push
的跳转方式中, MaterialPageRoute
的构造参数中 可以看到有 RouteSettings
的属性, RouteSettings
就是当前路由的基本信息
1 2 3 4 5
| const RouteSettings({ this.name, this.isInitialRoute = false, this.arguments, });
|
路由跳转时设置传递参数:
1 2 3 4 5 6
| 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
中取值:
1 2
| Map argms = ModalRoute.of(context).settings.arguments; print(argms["argms"]);
|
# 返回上级路由时传值
就是在调用 APage
中调用 pop
返回路由的时候传参
1
| Navigator.of(context).pop("这是pop返回的参数值");
|
在上一级路由获取:
1 2 3 4 5 6 7
| Navigator.of(context).push(MaterialPageRoute( builder: (context) => APage(), )).then((value){ print(value); }); 或 String value = await Navigator.of(context).pushNamed('/xxx');
|
1 2 3 4
| textDirection: TextDirection.ltr, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
|
# Column - 纵向
概念:纵轴的宽度,默认使用子组件最大宽度
此时,红色和黄色容器宽度为 100 绿色为 150,整个容器就会使用 最大的子组件宽度 150 来表示自己
Column 代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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):
flutter 为我们提供了 Flex 这个 widget 来制造弹性布局
Flex 默认 必传方向 Axis
children 使用 Expanded 来包裹,可以设置 flex 权重,根据数字大小来设置权重
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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):
flutter 为我们提供了 Wrap 这个 widget 来制造弹性布局
使用 有状态的 StatefulWidget 来构建 wrap 布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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++) { 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):
flutter 为我们提供了 Stack 这个 widget 来制造层叠布局
我们设置了两个容器 div,在层叠布局中,如果后一个容器,比前面的容器大,那么就会遮挡,原理是为什么?
- flutter 在绘画时,从 x 0 y 0 开始绘画,也就是 左上角
- 意味着两个容器绘画开始的坐标都是相同的,只不过宽高不一样
- 那么如果第一个容器宽高为 100 第二个为 150 就理所应当的遮住啦!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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):
flutter 为我们提供了 Positioned 这个 widget 来制造层叠布局
如果 Positioned 设置了宽高,那么子组件不生效
1 2 3 4 5 6 7 8
| top: 10, bottom: 10, 那么就不能设置高度 height
left: 10, right: 10, 那么就不能设置宽度 width
|
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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( child: Container( color: Colors.yellow, width: 300, height: 300, ), top: 50, left: 150, right: 150, bottom: 50, ) ], ), ); } }
|
# Flutter 相对定位 (Align):
flutter 为我们提供了 Align 这个 widget 来制造层叠布局
要点:只会相对于父组件来定位,而不是屏幕
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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( size: 60, ), ), ); } }
|
# Flutter 的内外边距 Padding、Margin
flutter 为我们提供了 padding 和 margin 这量个 属性来设置内外边距
内边距:当前容器内的组件对于当前容器的距离
外边距:当前容器距离父类容器的距离
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class PaddingAndMarginDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: Colors.red, margin: EdgeInsets.all(10), padding: EdgeInsets.all(20), child: Text("我有边距"), ); } }
|
要点:子 widget 没有设置宽高的时候取自己设置的最大宽高
ConstrainedBox 的特点就是可以设置最大或者最小的宽高,子组件怎么设置都不可以超过这个宽高
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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, ), ); } }
|
要点:如果父容器指定了宽高,那么子组件不可以修改宽高
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class ConstrainedBoxDemo extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( child: Container( color: Colors.red, width: 200, height: 200, ), ); } }
|
flutter 为我们提供了 BoxDecoration 这量个 widget 来设置样式装饰
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class ConstrainedBoxDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(20), width: double.infinity, child: DecoratedBox( decoration: BoxDecoration( 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
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class ContarinerDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(100), width: 100, height: 100, child: Text("data"), decoration: BoxDecoration( color: Colors.red ), transform: Matrix4.rotationZ(0.5), ); } }
|
1.MateriaApp 是 flutter 的根节点,flutter 规定必须要 MateriaApp 来作为根节点展示
2. 在 MateriaApp 可以设置路由,每个子页面必须由 Scaffold 来包裹
3. 每个 Scaffold 包含两个部分 appBar(头部),body(展示体)
# Flutter 的 AppBar:
Scaffold 中的 AppBar 有很多特性:
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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) ), 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, ), ); } }
|
# Flutter 的顶部 TabBar 选项卡:
Flutter 提供 顶部 TabBar 选项卡
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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, )).toList(), ), ), body: Text(_index.toString()), ); } }
|
# Flutter 的顶部 TabBar 选项卡(进阶)
使用 Flutter 提供 顶部 TabBarView 组件来设置选项卡
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| class PageDemo extends StatefulWidget { 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( children: widget.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 样式
使用 Flutter 提供 侧抽屉 Drawer 组件来设置抽屉样式
# 要点:drawer 是 Scaffold 中的属性,并不是 AppBar 的
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 的底部选项卡
使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到点击卡片切换页面
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class BottomNavigatorBarDemo extends StatefulWidget { const BottomNavigatorBarDemo({ Key? key }) : super(key: key);
@override State<BottomNavigatorBarDemo> createState() => _BottomNavigatorBarDemoState(); }
class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> { int _index = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("底部选项卡"), ), bottomNavigationBar: BottomNavigationBar( items: [ BottomNavigationBarItem( icon: Icon(Icons.add), label: "新增" ), BottomNavigationBarItem( icon: Icon(Icons.home), label: "我的" ), BottomNavigationBarItem( icon: Icon(Icons.remove), label: "减少" ), ], currentIndex: _index, onTap: (v){ setState(() { _index = v; }); }, ), body: Center(child: Text(_index.toString())), ); } }
|
# Flutter 的底部选项卡(进阶版)
使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到按钮居中布局
要点:两种实现方式,BottomNavigationBar 中如果 BottomNavigationBarItem 超过三个需要设置 type👇否则不显示
1
| type: BottomNavigationBarType.fixed
|
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class BottomNavigatorBarDemo extends StatefulWidget { List<Widget> widgets = [ PageDemo(), LayoutDemo(), LoginPage(), LoginPage(), ]; @override State<BottomNavigatorBarDemo> createState() => _BottomNavigatorBarDemoState(); }
class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> { int _index = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("底部选项卡"), ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: Icon(Icons.add), label: "首页" ), BottomNavigationBarItem( icon: Icon(Icons.home), label: "我的" ), BottomNavigationBarItem( icon: Icon(Icons.remove), label: "登录" ), BottomNavigationBarItem( icon: Icon(Icons.remove), label: "登录" ), ], currentIndex: _index, onTap: (v){ setState(() { _index = v; }); }, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: FloatingActionButton( onPressed: (){ print("object"); }, ), body: widget.widgets[_index], ); } }
|
第二种实现方式:
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> { int _index = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("底部选项卡"), ), bottomNavigationBar: BottomAppBar( color: Theme.of(context).primaryColorDark, shape: CircularNotchedRectangle(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( onPressed: (){
}, icon: Icon(Icons.add)), SizedBox(height: 16), IconButton( onPressed: (){
}, icon: Icon(Icons.home)), ] ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: FloatingActionButton( onPressed: (){ print("object"); }, ), body: widget.widgets[_index], ); } }
|
flutter 为我们提供了 ListView 这个 widget 来展示我们的列表
源码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ListView({ Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, this.itemExtent, this.prototypeItem, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, List<Widget> children = const <Widget>[], int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge, })
|
# 用 ListView 实现滑动列表,并且可以细粒度显示每个 list 数据,并且可以点击按钮返回顶部
代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| class ListViewDemo extends StatefulWidget { @override State<ListViewDemo> createState() => _ListViewDemoState(); }
class _ListViewDemoState extends State<ListViewDemo> { List<int> list = []; ScrollController _controller = ScrollController(); bool show = false; @override void initState() { super.initState(); _controller = ScrollController(); _controller.addListener(() { if(_controller.offset >= 100 && show == false){ setState(() { show = true; }); }else if(_controller.offset < 100 && show == true){ setState(() { show = false; }); } }); for (var i = 0; i < 100; i++) { list.add(i); } }
@override void dispose() { super.dispose(); _controller.dispose(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("滚动列表"), ), floatingActionButton: show ? FloatingActionButton( child: Icon(Icons.vertical_align_top_outlined), onPressed: (){ _controller.animateTo( 0, duration: Duration(milliseconds: 300), curve: Curves.slowMiddle ); }, ): null, body: Scrollbar( child: RefreshIndicator( child: ListView.builder( itemBuilder: (BuildContext context,int index){ if(index == 2){ return Icon(Icons.add); } return Text(list[index].toString()); }, itemCount: list.length, controller: _controller, ), onRefresh: _onRefresh, ) ) ); } Future _onRefresh() async{ await Future.delayed( Duration(seconds: 3), (){ print("三"); } ); return "三"; } }
|
flutter 为我们提供了 GridView 这个 widget 来展示我们的网格数据
源码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| GridView({ Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, required this.gridDelegate, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, List<Widget> children = const <Widget>[], int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, Clip clipBehavior = Clip.hardEdge, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, })
|
代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Grid_view_demo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("网格布局演示"), ), body: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, ), children: [ Container( color: Colors.amber, ), Container( color: Color.fromARGB(255, 85, 76, 51), ), Container( color: Color.fromARGB(255, 14, 223, 125), ), Container( color: Color.fromARGB(255, 42, 45, 209), ), ], ), ); } }
|
flutter 为我们提供了 AlertDialog 这个 widget 来展示我们的弹窗数据
源码阅读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const AlertDialog({ Key? key, this.title, this.titlePadding, this.titleTextStyle, this.content, this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), this.contentTextStyle, this.actions, this.actionsPadding = EdgeInsets.zero, this.actionsAlignment, this.actionsOverflowDirection, this.actionsOverflowButtonSpacing, this.buttonPadding, this.backgroundColor, this.elevation, this.semanticLabel, this.insetPadding = _defaultInsetPadding, this.clipBehavior = Clip.none, this.shape, this.alignment, this.scrollable = false, })
|
# 图片为 IOS 风格的弹窗
代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| class AlertDialogDemo extends StatefulWidget { @override State<AlertDialogDemo> createState() => _AlertDialogDemoState(); }
class _AlertDialogDemoState extends State<AlertDialogDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("弹窗展示"), ), body: Column( children: [ ElevatedButton( onPressed: _showAlert, child: Text("对话框")) ], ), ); }
void _showAlert() async{ var res = await showDialog( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: Text("与Brath的聊天"), content: Text("确认删除"), actions: [ TextButton(onPressed: () { Navigator.of(context).pop(true); }, child: Text("确认")), TextButton(onPressed: () { Navigator.pop(context,false); }, child: Text("取消")), ], ); }, ); print(res); } }
|
flutter 为我们提供了 SimpleDialog 这个 widget 来展示我们的弹框数据
源码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const SimpleDialog({ Key? key, this.title, this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), this.titleTextStyle, this.children, this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), this.backgroundColor, this.elevation, this.semanticLabel, this.insetPadding = _defaultInsetPadding, this.clipBehavior = Clip.none, this.shape, this.alignment, })
|
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class AlertDialogDemo extends StatefulWidget { @override State<AlertDialogDemo> createState() => _AlertDialogDemoState(); }
class _AlertDialogDemoState extends State<AlertDialogDemo> { List<int> list = []; @override void initState() { super.initState(); for (var i = 0; i < 20; i++) { list.add(i); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("弹窗展示"), ), body: Column( children: [ ElevatedButton( onPressed: _showAlert, child: Text("对话框")), ElevatedButton( onPressed: _showList, child: Text("列表框")), ], ), ); }
void _showList() async{ var res = await showDialog( barrierDismissible: false, context: context, builder: (BuildContext context) { return SimpleDialog( title: Text("与Brath的聊天"), children: list.map((e) => GestureDetector( child: Text(e.toString()), onTap: (){ Navigator.pop(context,e); }, )).toList(), ); }, ); print(res); }
|
flutter 为我们提供了 Table 还有 DataTable 这两个常用 widget 来展示我们的表格
代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| class TableDemo extends StatefulWidget { @override State<TableDemo> createState() => _TableDemoState(); }
class _TableDemoState extends State<TableDemo> { List<Map> list = []; int _sortColumnIndex = 0; bool _sortAscending = true; @override void initState() { super.initState(); for (var i = 0; i < 10; i++) { list.add({ "name": "b" * i, "gender": i % 1 == 0 ? "男" : "女", "isSelect": false, "age": i.toString() + i.toString(), }); } }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("表格演示"), ), body: Padding( padding: EdgeInsets.all(10),
child: DataTable( sortColumnIndex: _sortColumnIndex, sortAscending: _sortAscending, columns: [ DataColumn( onSort: (columnIndex, ascending) { setState(() { _sortAscending = ascending; _sortColumnIndex = columnIndex; list.sort((begin,end){ if(!ascending){ var c = begin; begin = end; end = c; } return begin["name"].toString().length.compareTo(end["name"].toString().length); }); }); }, label: Text("姓名") ), DataColumn( label: Text("性别") ), DataColumn( label: Text("年龄") ), ], rows: list.map((e) => DataRow( selected: e["isSelect"], onSelectChanged: (v){ setState(() { if(e["isSelect"] != v){ e["isSelect"] = v; } }); }, cells: [ DataCell(Text(e["name"])), DataCell(Text(e["gender"])), DataCell(Text(e["age"])), ] ) ).toList(), ), ) ); } }
|
flutter 为我们提供了 Card 这个 widget 来展示我们的卡片数据
代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class CardDemo extends StatefulWidget { @override State<CardDemo> createState() => _CardDemoState(); }
class _CardDemoState extends State<CardDemo> { List<Map> list = []; @override void initState() { super.initState(); for (var i = 0; i < 10; i++) { list.add({ "age": 10 + i, "name": "barth" + i.toString(), }); } }
Widget _itemBuilder(BuildContext context,int index){ return Card( color: Colors.green, shadowColor: Colors.grey, elevation: 5.0, child: Column( children: [ SizedBox(height: 8), Text(list[index]["name"]), SizedBox(height: 8), Text(list[index]["age"].toString()), ], ), ); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("卡片数据演示"), ), body: Padding( padding: EdgeInsets.all(10), child: ListView.builder( itemBuilder: _itemBuilder, itemCount: list.length, ) ), ); } }
|
Flutter 为我们提供了 ListTile 这个 Widget 来展示标签
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( centerTitle: true, title: Text("卡片数据演示"), ), body: Padding( padding: EdgeInsets.all(10), child: ListView( children: [ ListTile( tileColor: Color.fromARGB(255, 204, 184, 128), leading: Icon(Icons.token_sharp), title: Text("Brath"), textColor: Color.fromARGB(255, 49, 54, 42), subtitle: Text("Flutter卡片数据演示数据 1 "), trailing: Icon(Icons.account_circle_rounded), ), SizedBox(height: 8), ListTile( tileColor: Color.fromARGB(255, 197, 124, 55), leading: Icon(Icons.token_sharp), title: Text("Braht 2"), textColor: Color.fromARGB(255, 49, 54, 42), subtitle: Text("Flutter卡片数据演示数据 2 "), trailing: Icon(Icons.account_circle_rounded), ), ], ), ), ); }
|
# Flutter 性能优化:
先看图片:
我们以看到,这张图片由三个容器构成,并且点击黄色容器,其数字会增加,理论上来说代码并没有任何问题。
但是,在我们打开 检测工具后,发现,当点击黄色容器时,所有容器都会重新渲染,这就造成了性能的损耗!
如何优化?
代码演示:
# 使用一个单独的 CountDemo 来对 黄色的容器进行封装,这样就可以做到单独渲染
# 因为 setState 会重新绘制当前组件(Column),单独封装后,他自己就是一个单独组件(CountDemo)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| class performanceDemo extends StatefulWidget { @override State<performanceDemo> createState() => _performanceDemoState(); }
class _performanceDemoState extends State<performanceDemo> { int count = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("性能优化专题"), ), body: Column( children: [ Container( width: double.infinity, height: 100, color: Colors.red, ), Container( width: double.infinity, height: 100, color: Colors.yellow, child: CountDemo(), ), Container( width: double.infinity, height: 100, color: Colors.blue, ) ], ), ); } }
class CountDemo extends StatefulWidget { @override State<CountDemo> createState() => _CountDemoState(); } class _CountDemoState extends State<CountDemo> { int count = 0; @override Widget build(BuildContext context) { return GestureDetector( child: Text(count.toString()), onTap: (){ setState(() { count ++; }); }, ); } }
|
# Flutter 的全局状态管理 Provider 非常重要!
# 我们分为四个步骤来学习全局状态管理 Provider
# 1、因为全局状态管理是单独的插件,所以我们第一步一定是导包
选择根目录下的 pubspec.yaml 依赖配置文件
以作者 Brath 的 flutter 版本 2.0 为例,使用 5.0.0 版本
# 2、下载好依赖后,我们在 lib 目录创建文件夹:Provider 并在文件夹内创建一个 Count_provider.dart,作为我们的第一个全局状态类
在类中写入代码:
# 要点:notifyListeners () 这个方法的作用就是实现局部刷新
1 2 3 4 5 6 7 8
| class CountProvider extends ChangeNotifier{ int _count = 0; get count => _count; void add(){ _count ++; notifyListeners(); } }
|
# 3、我们写一个新的类用于测试全局状态数据
要点:
1. 获取全局变量:
# 通过 Provider 的 of 方法(泛型我们的全局状态类)传入上下文对象,就可以获取 count
1
| Provider.of<CountProvider>(context).count.toString()
|
2. 修改全局变量:
# 通过上下文对象的 read 方法(泛型我们的全局状态类),就可以获取状态类中的方法,来操作
# Tips:不是从当前页使用方法修改全局变量,而是全局变量提供了方法供外部修改!
1
| context.read<CountProvider>().add();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class ProviderDemo extends StatefulWidget { @override State<ProviderDemo> createState() => _ProviderDemoState(); }
class _ProviderDemoState extends State<ProviderDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("Provider全局状态管理"), ), body: Column( children: [ ElevatedButton( onPressed: (){ Navigator.of(context).pushNamed("ProviderDemo2"); }, child: Icon(Icons.add_task_rounded)), Text( Provider.of<CountProvider>(context).count.toString() ) ], ), floatingActionButton: FloatingActionButton( child: Icon(Icons.sanitizer_sharp), onPressed: (){ context.read<CountProvider>().add(); }, ), ); } }
class ProviderDemo2 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("Provider2"), ), body: FloatingActionButton( child: Icon(Icons.sanitizer_sharp), onPressed: (){ context.read<CountProvider>().add(); }, ), ); } }
|
4、在 main.dart 中修改启动方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider( create: (context) => CountProvider(), ), ], child: MyApp(), )); }
|
# Flutter 的网络请求(DIO)
# Flutter 在 pub.dev 为我们提供了许多网络请求组件,我们选择用 DIO 来做请求组件
使用方法:
# 1、因为网络请求是单独的插件,所以我们第一步一定是导包
选择根目录下的 pubspec.yaml 依赖配置文件
以作者 Brath 的 flutter 版本 2.0 为例,使用 4.0.0 版本
# 2、编写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class DioDemo extends StatefulWidget { @override State<DioDemo> createState() => _DioDemoState(); }
class _DioDemoState extends State<DioDemo> { Dio _dio = Dio();
@override void initState() { super.initState(); _dio.options.baseUrl = "https://www.XXX.XXX:0000/"; }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("网络请求演示"), ), body: Column( children: [ ElevatedButton( onPressed: _get, child: Text("getUserinfo") ), ] ), ); }
void _get() async{
var res2 = await _dio.get( "get/getData", queryParameters: { "id": 1 }, options: Options( headers: { "token": "header-Token" } ) ); print(res2.toString()); } }
|
# 1. 请求本地连接,ip 地址错误
# 2. 未添加网络请求权限
# 3. 请求的地址是 http,不是 https
# 4. 与服务端的请求参数不同,导致无法请求到接口
# Flutter 的设计模式(MVVM)(Model View ViewModel)
# MVVM 就是 Model View ViewModel 的简写。
# Model :处理接口请求数据
# View :展示页面
# ViewModel:处理业务逻辑(相当于 Model、View 的链接层)
# 简单流程:view 通知 viewModel 层,viewModel 通知 Model 层调用接口,viewModel 层接收 Model 的数据,响应给 view 层展示
# 我们通过几个步骤来演示 MVVM 模式的请求流程,以及他的优缺点
# 1、首先创建 Model View ViewModel 三个文件夹
# 2、编写 Model 层 (请求数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class MvvmModel{ dynamic get(int id) async {
print("开始调用userinfo接口"); var res = await Dio().get( "https://xxx:0000/gerUserInfo", queryParameters: { "userId": id }, ); print("调用完毕"); return res; } }
|
# 3、编写 View 层 (接收、展示数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class MvvmViewDemo extends StatefulWidget { @override State<MvvmViewDemo> createState() => _MvvmViewDemoState(); }
class _MvvmViewDemoState extends State<MvvmViewDemo> { dynamic res;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text("View展示页面"), ), body: Column( children: [ ElevatedButton( onPressed: () async { context.read<MvvmViewModel>().get(1); }, child: Text("调用ViewwModel获取用户userinfo") ), ], ), ); } }
|
# 4、编写 ViewModel 层 (整合 view 以及 model:view 调用 ViewModel ,ViewModel 调用 model 返回结果给 view)
tips:在调用完接口后跳转时,因为 Navigator 需要一个上下文对象,但是我们当前没有上下文对象,所以要在 main 入口定义一个对象:
main.dart👇
1 2 3 4 5 6 7 8 9 10
| final GlobalKey<NavigatorState> navigatorkey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: navigatorkey, ); } }
|
MvvmViewModel.dart👇
1 2 3 4 5 6 7 8 9 10 11 12 13
| class MvvmViewModel extends ChangeNotifier{ MvvmModel _model = MvvmModel(); void get(int id) async { Response res = await _model.get(id); print(res.data); print(res.headers); print(res.statusCode); print(res.realUri); Navigator.of(navigatorkey.currentContext!).pushNamed("DioDemo"); } }
|
# 5、DIO 返回值 Response 介绍
1 2 3 4 5
| Response res = await _model.get(id); print(res.data); print(res.headers); print(res.statusCode); print(res.realUri);
|
本篇至此就结束啦!如果你读到了这里,不妨给我点个赞,我叫 Brath,是一个非科班出身的技术狂!
我的博客 brath.top 欢迎访问!
# 关于我
Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!