flutter学习笔记
安装 Flutter(Dart)
安装 Flutter 默认安装Dart,如果只有Dart需求,可以查看官方文档。
使用 fvm 版本管理安装 Flutter
*项目使用 puro 版本管理安装 Flutter
可以查看本站点的 puro管理flutter版本 文档
安装 IDE 拓展
注意兼容
如果你使用 Android Studio/Intellij,安装拓展时请注意兼容的版本
- getx_template:一键生成每个页面必需的文件夹、文件、模板代码等等
- GetX Snippets:输入少量字母,自动提示选择后,可生成常用的模板代码
安装 GetX Cli
打开你的命令行,执行
pub global activate get_cli如果你的是 Flutter 环境,那么执行
flutter pub global activate get_cli环境变量添加
C:\Users\[User]\AppData\Local\Pub\Cache\bin
11.创建 Flutter 项目
flutter create my_project12.初始化 GetX 结构
get init如果执行后卡住
GetX Cli 命令卡住不动?
这是因为 GetX Cli 在检测更新,和 Flutter 命令卡住同理。源码里并没有提供更换更新地址的选项。所以我 fork 了源码,进行了更改
可以去仓库github的 release 界面,下载编译好的 get 。
赋予运行权限并重命名成 cnget 避免冲突,再设置一下环境变量。就可以直接使用 cnget 了 。使用它和使用 get 一样,它们本就同源。
初始化后的 lib 目录结构如下
.
├── app
│ ├── data
│ ├── modules
│ │ └── home
│ │ ├── bindings
│ │ │ └── home_binding.dart
│ │ ├── controllers
│ │ │ └── home_controller.dart
│ │ └── views
│ │ └── home_view.dart
│ └── routes
│ ├── app_pages.dart
│ └── app_routes.dart
└── main.dart是的,GetX Cli 已经帮你初始化好了一个路由结构以及 home 子页面,这个实例是可以直接运行的。
GetX Cli 将项目全部装进了 app 目录中,modules 存放页面,每个页面又分别存在 bindings,controllers,views
如果你知道 MVVM,那么你应该很熟悉,它们分别负责的是:
- bindings:负责 controller 与 view 的耦合,即 VM
- controller:负责响应式变量和非响应式变量的管理,即 M
- view:负责页面,即 V
即 view 与 controller 是解耦的,通过 binding 进行耦合。Flutter 官方示例大多数是一并写在一起,这将发生混乱,为协作和维护带来困难。
新增页面>
get create page:demo执行上面命令,你的 module 目录将会新增一个 demo 目录,结构如下
.
├── demo
│ ├── bindings
│ │ └── demo_binding.dart
│ ├── controllers
│ │ └── demo_controller.dart
│ └── views
│ └── demo_view.dart
└── home
├── bindings
│ └── home_binding.dart
├── controllers
│ └── home_controller.dart
└── views
└── home_view.dart这将为你生成和 home 一致结构的目录,并且自动将 demo 加进路由表中
新增 view
get create view:demo on home执行上面命令,将会在 module/home 目录下的 views 文件夹里新增一个 demo_view,结构如图
.
├── bindings
│ └── home_binding.dart
├── controllers
│ └── home_controller.dart
└── views
├── demo_view.dart
└── home_view.dart新增 controller
get create controller:demo on home执行上面命令,将会在 module/home 目录下的 controllers 文件夹下新增一个 demo_controller,结构如图
.
├── bindings
│ └── home_binding.dart
├── controllers
│ ├── demo_controller.dart
│ └── home_controller.dart
└── views
├── demo_view.dart
└── home_view.dart并且会自动在 home_binding 中耦合 demo_controller,但是此时并没有 view 与 demo_controller 耦合,这就需要我们手动去绑定它。
修改 demo_view
// 修改前
class DemoView extends GetView {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('DemoView'),
centerTitle: true,
),
body: Center(
child: Text(
'DemoView is working',
style: TextStyle(fontSize: 20),
),
),
);
}
}
// 修改后
class DemoView extends GetView<DemoController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('DemoView'),
centerTitle: true,
),
body: Center(
child: Text(
'DemoView is working',
style: TextStyle(fontSize: 20),
),
),
);
}
}是的,你只需要在 GetView 后加上绑定的 controller 即可
自定义 Controller 模板
从本地文件获取模板
get create controller:auth with examples/authcontroller.dart on your_folder从远程获取模板
get create controller:auth with 'https://raw.githubusercontent.com/jonataslaw/get_cli/master/samples_file/controller.dart.example' on your_folder如果你的模板长这样
@import
class @controller extends GetxController {
final email = ''.obs;
final password = ''.obs;
void login() {
}
}那么生成的 Controller 就是这样的
import 'package:get/get.dart';
class AuthController extends GetxController {
final email = ''.obs;
final password = ''.obs;
void login() {}
}12.引入 GetX(手动)
选择1:在你的 pubspec.yaml 文件下增加
dependencies:
get: ^4.1.1最新版本进pub仓库查看:pub.dev
选择2:安装:flutter pub add get
使用:import 'package:get/get.dart';
MaterialApp()改为GetMaterialApp()
21. GetX创建项目
get create project
官方计时器例子
首先我们创建我们的变量控制器controller
class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}然后创建我们的页面,使用StatelessWidget节省一些内存,使用Getx你可能不再需要使用StatefulWidget。
class Home extends StatelessWidget {
@override
Widget build(context) {
// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());
return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),
// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}
class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();
@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}如此便完成了官方的计时器
生成 Model Provider
准备你的数据 json 文件,放入 assets/models 下
例如:
user.json
{
"name": "",
"age": 0,
"friends": ["", ""]
}执行生成命令
get generate model on home with assets/models/user.json
get generate model on home with [https://url]如果报错
重新安装
dart pub global deactivate get_cli
dart pub global activate -s git https://github.com/knottx/get_cli.git --git-ref fix-generate-model将会为你生成一份 Model 文件和一份 Provider 文件
网络请求
网络请求
GetX建议将网络请求拆分成 Repository 与 Provider 两层
Repository 负责将请求的数据进行存储,预处理
Provider 负责发出请求,以及处理请求返回,请求错误等
class User {
String name;
int age;
List<String> friends;
User({this.name, this.age, this.friends});
User.fromJson(Map<String, dynamic> json) {
name = json['name'];
age = json['age'];
friends = json['friends'].cast<String>();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['name'] = this.name;
data['age'] = this.age;
data['friends'] = this.friends;
return data;
}
}中间件
如果你想通过监听Get事件来触发动作,你可以使用routingCallback来实现。
GetMaterialApp(
routingCallback: (routing) {
if(routing.current == '/second'){
openAds();
}
}
)如果你没有使用GetMaterialApp,你可以使用手动API来附加Middleware观察器。
void main() {
runApp(
MaterialApp(
onGenerateRoute: Router.generateRoute,
initialRoute: "/",
navigatorKey: Get.key,
navigatorObservers: [
GetObserver(MiddleWare.observer), // HERE !!!
],
),
);
}创建一个MiddleWare类
class MiddleWare {
static observer(Routing routing) {
///你除了可以监听路由外,还可以监听每个页面上的SnackBars、Dialogs和Bottomsheets。
if (routing.current == '/second' && !routing.isSnackbar) {
Get.snackbar("Hi", "You are on second route");
} else if (routing.current =='/third'){
print('last route called');
}
}
}现在,在你的代码上使用Get:
class First extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.add),
onPressed: () {
Get.snackbar("hi", "i am a modern snackbar");
},
),
title: Text('First Route'),
),
body: Center(
child: ElevatedButton(
child: Text('Open route'),
onPressed: () {
Get.toNamed("/second");
},
),
),
);
}
}
class Second extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.add),
onPressed: () {
Get.snackbar("hi", "i am a modern snackbar");
},
),
title: Text('second Route'),
),
body: Center(
child: ElevatedButton(
child: Text('Open route'),
onPressed: () {
Get.toNamed("/third");
},
),
),
);
}
}
class Third extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Third Route"),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Get.back();
},
child: Text('Go back!'),
),
),
);
}
}免context导航
SnackBars
用Flutter创建一个简单的SnackBar,你必须获得Scaffold的context,或者你必须使用一个GlobalKey附加到你的Scaffold上。
final snackBar = SnackBar(
content: Text('Hi!'),
action: SnackBarAction(
label: 'I am a old and ugly snackbar :(',
onPressed: (){}
),
);
// 在小组件树中找到脚手架并使用它显示一个SnackBars。
Scaffold.of(context).showSnackBar(snackBar);用Get:
Get.snackbar('Hi', 'i am a modern snackbar');有了Get,你所要做的就是在你代码的任何地方调用你的Get.snackbar,或者按照你的意愿定制它。
Get.snackbar(
"Hey i'm a Get SnackBar!", // title
"It's unbelievable! I'm using SnackBar without context, without boilerplate, without Scaffold, it is something truly amazing!", // message
icon: Icon(Icons.alarm),
shouldIconPulse: true,
onTap:(){},
barBlur: 20,
isDismissible: true,
duration: Duration(seconds: 3),
);
////////// ALL FEATURES //////////
// Color colorText,
// Duration duration,
// SnackPosition snackPosition,
// Widget titleText,
// Widget messageText,
// bool instantInit,
// Widget icon,
// bool shouldIconPulse,
// double maxWidth,
// EdgeInsets margin,
// EdgeInsets padding,
// double borderRadius,
// Color borderColor,
// double borderWidth,
// Color backgroundColor,
// Color leftBarIndicatorColor,
// List<BoxShadow> boxShadows,
// Gradient backgroundGradient,
// TextButton mainButton,
// OnTap onTap,
// bool isDismissible,
// bool showProgressIndicator,
// AnimationController progressIndicatorController,
// Color progressIndicatorBackgroundColor,
// Animation<Color> progressIndicatorValueColor,
// SnackStyle snackStyle,
// Curve forwardAnimationCurve,
// Curve reverseAnimationCurve,
// Duration animationDuration,
// double barBlur,
// double overlayBlur,
// Color overlayColor,
// Form userInputForm
///////////////////////////////////如果您喜欢传统的SnackBars,或者想从头开始定制,包括只添加一行(Get.snackbar使用了一个强制性的标题和信息),您可以使用 Get.rawSnackbar();它提供了建立Get.snackbar的RAW API。
Dialogs
打开Dialogs:
Get.dialog(YourDialogWidget());打开默认Dialogs:
Get.defaultDialog(
onConfirm: () => print("Ok"),
middleText: "Dialog made in 3 lines of code"
);你也可以用Get.generalDialog代替showGeneralDialog。
对于所有其他的FlutterDialogs小部件,包括cupertinos,你可以使用Get.overlayContext来代替context,并在你的代码中任何地方打开它。 对于不使用Overlay的小组件,你可以使用Get.context。 这两个context在99%的情况下都可以代替你的UIcontext,除了在没有导航context的情况下使用 inheritedWidget的情况。
BottomSheets
Get.bottomSheet类似于showModalBottomSheet,但不需要context:
Get.bottomSheet(
Container(
child: Wrap(
children: <Widget>[
ListTile(
leading: Icon(Icons.music_note),
title: Text('Music'),
onTap: () {}
),
ListTile(
leading: Icon(Icons.videocam),
title: Text('Video'),
onTap: () {},
),
],
),
)
);嵌套导航
Get让Flutter的嵌套导航更加简单。 你不需要context,而是通过Id找到你的导航栈。
Navigator(
key: Get.nestedKey(1), // create a key by index
initialRoute: '/',
onGenerateRoute: (settings) {
if (settings.name == '/') {
return GetPageRoute(
page: () => Scaffold(
appBar: AppBar(
title: Text("Main"),
),
body: Center(
child: TextButton(
color: Colors.blue,
onPressed: () {
Get.toNamed('/second', id:1); // navigate by your nested route by index
},
child: Text("Go to second"),
),
),
),
);
} else if (settings.name == '/second') {
return GetPageRoute(
page: () => Center(
child: Scaffold(
appBar: AppBar(
title: Text("Main"),
),
body: Center(
child: Text("second")
),
),
),
);
}
}
),
启动
flutter run
r 键 :Flutter热加载命令。
R键:Flutter热重启项目命令。
p 键:显示网格,这个可以很好的掌握布局情况,工作中很有用。
o 键:切换android和ios的预览模式。
q 键:退出调试预览模式。
get install [package]
get remove [package]
快速开始
基础
获取设备宽高
MediaQuery
// 获取设备宽度和高度
// build方法中使用
final size = MediaQuery.of(context).size;
// size.width size.height查看设备
flutter devices
$ flutter devices
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
3 connected devices:
Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.19043.1766]
Chrome (web) • chrome • web-javascript • Google Chrome 103.0.5060.114
Edge (web) • edge • web-javascript • Microsoft Edge 103.0.1264.49运行在所有的设备
flutter run -d all指定设备运行
flutter run -d chrome- 基础组件
按钮 Button
ElevatedButton(onPressed: (){}, child: const Text("普通按钮")),
TextButton(onPressed: (){}, child: const Text("文本按钮")),
OutlinedButton(onPressed: (){}, child: const Text("边框按钮")),
IconButton(onPressed: (){}, icon: const Icon(Icons.thumb_down)),// 图标按钮
ElevatedButton.icon(onPressed: (){}, icon: const Icon(Icons.send),label: const Text("带图标的按钮"),),
TextButton.icon(onPressed: (){}, icon: const Icon(Icons.send),label: const Text("带图标的文字按钮"),),
OutlinedButton.icon(onPressed: (){}, icon: const Icon(Icons.send),label: const Text("带图标的边框按钮"),),
// shape 圆角 圆形
// side 边框
//
//
ElevatedButton(onPressed: (){}, style:const ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.red),
foregroundColor: WidgetStatePropertyAll(Colors.amberAccent),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(42))// 圆角
//CircleBorder(side: BorderSide(color: Colors.yellow)) //圆形
)
),child: const Text("修改样式"),),图片 Image
Image.asset("assets/imgs/120572574_p0.png",fit: BoxFit.fill)圆形图片 ClipOval
ClipOval(child: Image.asset(
"assets/imgs/120572574_p0.png",
fit: BoxFit.cover,
),)圆形图片 CircleAvatar
const CircleAvatar(
radius: 100, // 半径
backgroundImage: AssetImage("assets/imgs/120572574_p0.png"),
)文本 Text
const Text(
"Text你好Flutter",
textAlign: TextAlign.center, // 对齐
style: TextStyle(
color: Colors.blueAccent, // 颜色
overflow: TextOverflow.ellipsis, // 裁剪
),
),单选 复选 Switch Checkbox
Switch(
value: _switchSelected,//当前状态
onChanged:(value){
//重新构建页面
setState(() {
_switchSelected=value;
});
},
),
Checkbox(
value: _checkboxSelected,
activeColor: Colors.red, //选中时的颜色
onChanged:(value){
setState(() {
_checkboxSelected=value;
});
} ,
)输入框及表单 TextField Form
TextField 输入框
Column(
children: <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)
),
obscureText: true,
),
],
);Form 表单
return Form(
key: _formKey, //设置globalKey,用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: <Widget>[
TextFormField(
autofocus: true,
controller: _unameController,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "用户名不能为空";
},
),
TextFormField(
controller: _pwdController,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock),
),
obscureText: true,
//校验密码
validator: (v) {
return v!.trim().length > 5 ? null : "密码不能少于6位";
},
),
// 登录按钮
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: <Widget>[
Expanded(
child: ElevatedButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text("登录"),
),
onPressed: () {
// 通过_formKey.currentState 获取FormState后,
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if ((_formKey.currentState as FormState).validate()) {
//验证通过提交数据
}
},
),
),
],
),
)
],
),
);进度指示器 条 圆
LinearProgressIndicator 条
// 模糊进度条(会执行一个动画)
LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50% 静止
LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
)CircularProgressIndicator 圆
// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%,会显示一个半圆 静止
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
),- 布局类组件
对其与相对定位 Align
alignment: Alignment.center同Center
Alignment(1,1) 右下角
Alignment(0,1) 左下角
Align(alignment: Alignment.center,child: MyGardView())Center
Center(child: MyGardView())线性布局 Row Column
Row( //外部没有Container时,row自适应占满一行
mainAxisSize: MainAxisSize.min, // 紧凑排列,宽度自适应内容
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴对齐
crossAxisAlignment: CrossAxisAlignment.center, // 次轴对齐 相对于外部容器对齐
children: [
IconContainer(Icons.home,color: Colors.green),
IconContainer(Icons.home,color: Colors.green),
IconContainer(Icons.home,color: Colors.green)
],
)Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴对齐
crossAxisAlignment: CrossAxisAlignment.center, // 次轴对齐 相对于外部容器对齐
children: [
IconContainer(Icons.home, color: Colors.green),
IconContainer(Icons.home, color: Colors.green),
IconContainer(Icons.home, color: Colors.green)
],
)Wrap 流式布局
一般在 Row Column 组件内使用
Padding(padding: EdgeInsets.all(10),
child: Wrap(
direction: Axis.horizontal, // 主轴方向
alignment: WrapAlignment.spaceBetween, // 主轴对齐
spacing: 10, // 水平间距
runSpacing: 10, // 垂直间距
children: [
Text("123"),
Text("123"),
Text("123"),
Text("123"),
Text("123"),
],
),),Flex 弹性布局
Expanded.child推荐套一层SizedBox()给个宽度或者高度
一般Expanded直接在Row和Column中结合使用
Flex(
direction: Axis.vertical, // 对齐方式 水平/垂直
children: [
Expanded(flex:2, child: IconContainer(Icons.home, color: Colors.green),),
Expanded(flex:1, child: IconContainer(Icons.home, color: Colors.green),),
],
)
// or
Row(
children: [
Expanded(flex:2, child: IconContainer(Icons.home, color: Colors.green),),
Expanded(flex:1, child: IconContainer(Icons.home, color: Colors.green),),
],
)Stack - Positioned 定位
相对于外部容器定位,如果没有,就是屏幕
Stack一般配合Container内使用
Positioned内的child如果是row和column,需要指定宽度和高度(Positioned中设置的宽度高度是子组件的)
Container(
width: 300,
height: 400,
color: Colors.pink,
child: Stack(
alignment: Alignment.center, // 对齐
children: [
Positioned( // 定位
left: 0,
bottom: 0,
child: Container(
width: 100,
height: 100,
color: Colors.yellow,
)),
const Text("123123")
],
),
)- 容器类组件
交互 InkWell 添加交互
onTap: 当用户点击InkWell时调用的回调函数。onDoubleTap: 当用户双击InkWell时调用的回调函数。onLongPress: 当用户长按InkWell时调用的回调函数。child: 需要被InkWell包裹的 widget。splashColor: 墨水涟漪动画的颜色。highlightColor:InkWell高亮时的背景颜色。
容器 Container
Container只设置宽高的话建议使用SizedBox
return Container(
width: 200,
height: 200,
alignment: Alignment.center, // 元素居中对齐
margin: const EdgeInsets.fromLTRB(0, 20, 0, 0),
decoration: const BoxDecoration(color: Colors.amber),
child: Text("123")
);宽高 SizedBox
Container只设置宽高的话建议使用SizedBox
const SizedBox(
height: 5,
width: 5
),填充 Padding
Padding(padding: EdgeInsets.all(10), child: MyGardView())装饰容器 BoxDecoration
要在容器中
BoxDecoration(
color: Color.fromARGB(183, 202, 190, 190), // 颜色
borderRadius: BorderRadius.circular(30), //圆角
)
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors:[Colors.red,Colors.orange.shade700]), //背景渐变
borderRadius: BorderRadius.circular(3.0), //3像素圆角
boxShadow: [ //阴影
BoxShadow(
color:Colors.black54,
offset: Offset(2.0,2.0),
blurRadius: 4.0
)
]
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
child: Text("Login", style: TextStyle(color: Colors.white),),
)
)屏幕宽度 AspectRatio
1 / 1, // 宽高等于屏幕宽度
2 / 1,// 宽等于屏幕宽度,高度为屏幕宽度一半
3 / 1,// 宽等于屏幕宽度,高度为屏幕宽度三分之一
16/9,// 16:9的宽高比 图片
AspectRatio(
aspectRatio: 1 / 1, // 容器 相对于屏幕宽度 子元素宽高比
child: Container(
color: Colors.tealAccent,
),
)卡片 Card
Card(
elevation: 20, // 阴影深度
shadowColor: Colors.amber, // 阴影颜色
color: Colors.black12, // 背景颜色
margin: const EdgeInsets.all(10),// 边距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10) // 圆角
),
child: const Column(
children: [
ListTile(
title: Text("张三"),
subtitle: Text("资深美食迁移家"),
),
Divider(),
ListTile(
title: Text("电话:123123123"),
),
ListTile(
title: Text("电话:123123123"),
),
],
),
),
// 实例
// 图文列表
Card(
elevation: 20, // 阴影深度
margin: const EdgeInsets.all(10), // 边距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10) // 圆角
),
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Image.asset(
"assets/imgs/120572574_p0.png",
fit: BoxFit.cover,
),
),
ListTile(
leading: ClipOval(child: Image.asset(
"assets/imgs/120572574_p0.png",
fit: BoxFit.cover,
),),
title: const Text("张三"),
subtitle: const Text("资深美食迁移家"),
),
],
),
),- Scaffold 页面骨架
drawer 抽屉组件
return Obx(
() => Scaffold(
appBar: AppBar(
title: const Text('HomeView'),
centerTitle: true,
backgroundColor: Colors.transparent, // 背景透明
elevation: 0, // 阴影取消
),
// body: controller.pages[controller.currentIndex.value], // 无缓存
body: PageView(
scrollDirection: Axis.horizontal,
controller: controller.pageController,
children: controller.pages,
onPageChanged: (index) {
controller.setCuttentIndex(index);
},
), // 带缓存 也可滑动 PageView
// 浮动按钮
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
bottomNavigationBar: BottomNavigationBar(
iconSize: 25, // icon大小
fixedColor: Colors.red, // 选中颜色
currentIndex: controller.currentIndex.value, // 选中索引
type: BottomNavigationBarType.fixed, // 超过3个菜单时需要配置
onTap: (index) {
controller.setCuttentIndex(index);
controller.pageController.jumpToPage(index); // PageView跳转
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "服务"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "购物车"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "用户"),
],
),
),
);AppBar
AppBar(
leading: controller.flag.value
? null
: Icon(FontAwesomeIcons.adn, color: Colors.white),
leadingWidth: controller.flag.value ? 0 : ScreenAdapter.width(140),
title: AnimatedContainer(
width: ScreenAdapter.width(controller.flag.value ? 920 : 620),
height: ScreenAdapter.height(96),
decoration: BoxDecoration(
color: Color.fromARGB(59, 202, 190, 190),
borderRadius: BorderRadius.circular(30),
),
duration: Duration(milliseconds: 300),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.fromLTRB(
ScreenAdapter.width(20),
ScreenAdapter.width(0),
ScreenAdapter.width(4),
ScreenAdapter.width(0),
),
child: Icon(
Icons.search,
size: ScreenAdapter.width(42),
color: Colors.black54,
),
),
Text(
"手机${controller.flag.value}",
style: TextStyle(
fontSize: ScreenAdapter.fontSize(36),
color: Colors.black54,
),
),
Expanded(
child: Align(
alignment: Alignment.centerRight, // 将 Icon 对齐到右侧
child: Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Icon(
Icons.qr_code_scanner_outlined,
size: ScreenAdapter.width(42),
color: Colors.black54,
),
),
),
),
],
),
),
centerTitle: true,
backgroundColor: controller.flag.value
? Colors.white
: Colors.transparent, // 背景透明
elevation: 0, // 阴影取消
actions: [
IconButton(onPressed: () {}, icon: Icon(Icons.qr_code)),
IconButton(onPressed: () {}, icon: Icon(Icons.message)),
],
)- 可滚动组件
列表 ListView-ListTile
return ListView(
children: [
Container(
height: 150,
decoration: const BoxDecoration(
color: Colors.blue,
),
child: ListTile(
leading: Image.asset("assets/imgs/120572574_p0.png"),
title: const Text("琳芙斯"),
subtitle: const Text("ID:1234567890"),
trailing: const Icon(Icons.chevron_right_sharp),
),
),
const Divider(),
ListTile(
leading: Image.asset("assets/imgs/120572574_p0.png"),// 左边图片
title: const Text("这里是一篇新闻内容标题"),// 标题
subtitle: const Text("这是而二级标题"),// 子标题
trailing: const Icon(Icons.chevron_right_sharp),// 右边图片
),
],
);AnimatedList
类似ListView,多了动画
网格 GridView
GridView.count(
padding:const EdgeInsets.all(10),// 边距
crossAxisSpacing: 10,// 水平间距
mainAxisSpacing: 10, // 垂直间距
crossAxisCount: 2,// 一行显示多少个
childAspectRatio: 0.8,// 宽高比
children: _initGridViewData(),
);PageView 与页面缓存 (轮播)
/// controller
final PageController pageController = PageController(initialPage: 2);
// controller.pageController.jumpToPage(index); // PageView跳转
/// view
PageView(
scrollDirection: Axis.vertical, // 滑动方向,默认水平
controller: controller.pageController,
physics: NeverScrollableScrollPhysics(), //禁止左右滑动
children: controller.pages,
onPageChanged: (index) {
controller.setCuttentIndex(index);
},
)禁止左右滑动
physics: const NeverScrollableScrollPhysics(), //禁止左右滑动示例
/// controller
class TabsController extends GetxController {
final currentIndex = 2.obs;
final PageController pageController = PageController(initialPage: 2);
final List<Widget> pages = const [
HomeView(),
CategoryView(),
GiveView(),
CartView(),
UserView(),
];
/// 更新tabbar
setCuttentIndex(index) {
currentIndex.value = index;
}
}
/// view
Widget build(BuildContext context) {
return Obx(
() => Scaffold(
// body: controller.pages[controller.currentIndex.value], // 无缓存
body: PageView(
scrollDirection: Axis.horizontal,
controller: controller.pageController,
physics: NeverScrollableScrollPhysics(),
children: controller.pages,
onPageChanged: (index) {
controller.setCuttentIndex(index);
},
), // 带缓存 也可滑动 PageView
// 浮动按钮
// floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// floatingActionButton: FloatingActionButton(
// onPressed: () {},
// child: const Icon(Icons.add),
// ),
bottomNavigationBar: BottomNavigationBar(
iconSize: 25, // icon大小
fixedColor: Colors.red, // 选中颜色
currentIndex: controller.currentIndex.value, // 选中索引
type: BottomNavigationBarType.fixed, // 超过3个菜单时需要配置
onTap: (index) {
controller.setCuttentIndex(index);
controller.pageController.jumpToPage(index); // PageView跳转
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "服务"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "购物车"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "用户"),
],
),
),
);
}- 功能型组件
导航返回拦截(WillPopScope)
* 配置自定义IconFont
flutter:
fonts:
- family: xmIcon
fonts:
- asset: assets/fonts/iconfont.ttf/// xmFonts.dart
import 'package:flutter/material.dart';
class XmFontsIcon {
static const IconData xiaomi=IconData(
0xe609, // "unicode": "e609", 0x + unicode
fontFamily: "xmIcon", // 对应 yaml 的 family
matchTextDirection: true
);
static const IconData weixiufuwu=IconData(
0xe609,
fontFamily: "xmIcon",
matchTextDirection: true
);
}使用
Icon(XmFontsIcon.xiaomi) // 对应上面的class* 透明状态栏
//// main.dart
import 'package:flutter/services.dart';
void main() => bootstrap();
bootstrap() async {
// flutter 修改状态栏颜色 透明状态栏
SystemUiOverlayStyle systemUiOverlayStyle = const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
// await ApplicationInit.init();
runApp(const Application());
}自定义AppBar
* 屏幕适配方案
安装
flutter_screenutil: ^5.9.3 # 屏幕适配方案配置
/// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_xmshop/application.dart';
void main() => bootstrap();
bootstrap() async {
// await ApplicationInit.init();
runApp(const Application());
}/// application.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_xmshop/app/routes/app_pages.dart';
import 'package:get/get.dart';
class Application extends StatelessWidget{
const Application({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(1080, 2400), //设计稿 宽 高
minTextAdapt: true,
splitScreenMode: true,
builder: (BuildContext context, Widget? child) =>
MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)),
child: GetMaterialApp(
debugShowCheckedModeBanner: false,
title: '小米商城',
// initialBinding: LoginBinding(),
// theme: AppTheme.light,
// darkTheme: AppTheme.dark,
initialRoute: AppPages.INITIAL, // 首页
getPages: AppPages.routes, // 路由表
defaultTransition: Transition.rightToLeft,
// builder: EasyLoading.init(),
)
),
);
}
}直接使用
200.w //宽度
200.h //高度
200.r //较宽高 小的一个
200.sp //字体
1.sw //设备宽度
1.sh //设备高度Container(
width: 1080.w, // 设计稿宽度.w
height: 200.h, // 设计稿高度.h
child: Text('CartView is working', style: TextStyle(fontSize: 20.sp)),
),封装使用
组件层改过3次调用方式,保险起见封装一层
ScreenAdapter.width(10);
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 封装flutter_screenutil
class ScreenAdapter {
static width(num v) {
return v.w;
}
static height(num v) {
return v.h;
}
static fontSize(num v) {
return v.sp;
}
static getScreenWidth() {
return 1.sw;
}
static getScreenHeight() {
return 1.sh;
}
}* Swiper轮播图插件
flutter_swiper_view: ^1.1.8 # 轮播图插件Swiper(
itemCount: controller.swiperList.length,
itemBuilder: (context, index) {
return Image.network(
controller.swiperList[index]["url"],
fit: BoxFit.fill,
);
},
pagination: SwiperPagination(
builder: SwiperPagination.rect,
),
// control: SwiperControl(),
autoplay: true, // 自动轮播
loop: true,
duration: 3000, // 切换时间
)* 瀑布流插件
flutter_staggered_grid_view: ^0.6.2 # 瀑布流插件MasonryGridView.count(
crossAxisCount: 2, // 2列
mainAxisSpacing: 15, // 水平间距
crossAxisSpacing: 15, // 垂直间距
itemCount: 20, // 数量
shrinkWrap: true, // 收缩,让元素自适应宽度
physics: NeverScrollableScrollPhysics(), //禁止滑动
itemBuilder: (context, index) {
var height = 50 + 150 * Random().nextDouble(); // 随机高度
return Container(
height: height,
decoration: BoxDecoration(
border: Border.all(color: Colors.yellow, width: 1),
borderRadius: BorderRadius.circular(10),
),
child: Text("data"),
);
},
)* Dio 网络请求
dio: ^5.8.0+1 getFocusData() async {
var response = await Dio().get("https://miapp.itying.com/api/focus");
swiperList.value = response.data["result"];
update();
}封装
* GetX
组件
通知 snackbar
Get.snackbar('title','messagee',
colorText?: Colors.blue,// 文字颜色
titleText?: Text("title"),// 覆盖第一个参数
messageText?: Text("messagee"),// 覆盖第二个参数
borderWidth?: 10, //边框宽
borderRadius?: 50,//边框圆角
borderColor?: Colors.blue,// 边框颜色
duration?: const Duration(seconds: 5),// 显示时间
icon: const Icon(Icons.add_comment_sharp), // 左侧icon
);默认弹窗 defaultDialog
如果配合其他组件,需要先关闭Get.back()再使用其他组件,因为同一时刻只能显示一个组件
Get.defaultDialog(
title: "提示",
content: const Text("这里是通知"),
confirm: TextButton(
onPressed: () {
Get.back(); // 关闭弹窗
Get.snackbar("提示", "完成通知"); // 如果配合其他组件,需要先关闭再使用,因为同一时刻只能显示一个组件
},
child: const Text("确认")),
cancel: TextButton(
onPressed: () {
Get.back();
},
child: const Text("取消")),
barrierDismissible: false // 点击外部暗区是否关闭
);底部弹窗 bottomSheet
Get.bottomSheet(
Container(
height: 200,
color: Colors.amber,
child: Center(
child: ElevatedButton(
onPressed: () {
Get.back();
},
child: const Text('退出')),
),
),
isDismissible: false, //是否点击暗区关闭
enableDrag: false, //是否向下拖拽关闭
);主题切换 changeTheme
Get.changeTheme( Get.isDarkMode ? ThemeData.light() : ThemeData.dark() );// 变更主题,亮暗切换路由管理
Getx支持 免上下文(context) 使用 路由/snackbars/dialogs/bottomsheets
普通导航
导航到新的页面。
Get.to(
const MyView(),
fullscreenDialog: true,
transition: Transition.upToDown,//线性
duration: const Duration(milliseconds: 500),// 过渡时间
curve: Curves.bounceIn // 过渡
);关闭SnackBars、Dialogs、BottomSheets或任何你通常会用Navigator.pop(context)关闭的东西。
Get.back(result: Text("返回上一页带的参数"));进入下一个页面,但没有返回上一个页面的选项(用于SplashScreens,登录页面等)。
Get.off(NextScreen());进入下一个界面并取消之前的所有路由(在购物车、投票和测试中很有用)。
Get.offAll(NextScreen());要导航到下一条路由,并在返回后立即接收或更新数据。
var data = await Get.to(Payment());在另一个页面上,发送前一个路由的数据。
Get.back(result: 'success');// 默认的Flutter导航
Navigator.of(context).push(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
);
// 使用Flutter语法获得,而不需要context。
navigator.push(
MaterialPageRoute(
builder: (_) {
return HomePage();
},
),
);
// get语法 (这要好得多)
Get.to(HomePage());别名导航
导航到下一个页面
Get.toNamed("/NextScreen");浏览并删除前一个页面。
Get.offNamed("/NextScreen");浏览并删除所有以前的页面。
Get.offAllNamed("/NextScreen");要定义路由,使用GetMaterialApp。
void main() {
runApp(
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyHomePage()),
GetPage(name: '/second', page: () => Second()),
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.zoom
),
],
)
);
}要处理到未定义路线的导航(404错误),可以在GetMaterialApp中定义unknownRoute页面。
void main() {
runApp(
GetMaterialApp(
unknownRoute: GetPage(name: '/notfound', page: () => UnknownRoutePage()),
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyHomePage()),
GetPage(name: '/second', page: () => Second()),
],
)
);
}路由传参
Get提供高级动态URL,就像在Web上一样。
Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo");获取参数:
print(Get.parameters['id']);
// out: 354
print(Get.parameters['name']);
// out: Enzo你也可以用Get轻松接收NamedParameters。
void main() {
runApp(
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => MyHomePage(),
),
GetPage(
name: '/profile/',
page: () => MyProfile(),
),
//你可以为有参数的路由定义一个不同的页面,也可以为没有参数的路由定义一个不同的页面,但是你必须在不接收参数的路由上使用斜杠"/",就像上面说的那样。
GetPage(
name: '/profile/:user',
page: () => UserProfile(),
),
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.cupertino
),
],
)
);
}发送数据
Get.toNamed("/second/34954");在第二个页面上,通过参数获取数据
print(Get.parameters['user']);
// out: 34954或像这样发送多个参数
Get.toNamed("/profile/34954?flag=true");在第二个屏幕上,通常按参数获取数据
print(Get.parameters['user']);
print(Get.parameters['flag']);
// out: 34954 true现在,你需要做的就是使用Get.toNamed()来导航你的别名路由,不需要任何context(你可以直接从你的BLoC或Controller类中调用你的路由),当你的应用程序被编译到web时,你的路由将出现在URL中。
获取路由参数 arguments
Get.arguments['name']重定向日志
你可以选择重定向所有来自GetX的日志信息。 如果你想使用你自己喜欢的日志包,并想查看那里的日志。
GetMaterialApp(
enableLog: true,
logWriterCallback: localLogWriter,
);
void localLogWriter(String text, {bool isError = false}) {
// 在这里把信息传递给你最喜欢的日志包。
// 请注意,即使enableLog: false,日志信息也会在这个回调中被推送。
// 如果你想的话,可以通过GetConfig.isLogEnable来检查这个标志。
}其他高级Api
// 给出当前页面的args。
Get.arguments
//给出以前的路由名称
Get.previousRoute
// 给出要访问的原始路由,例如,rawRoute.isFirst()
Get.rawRoute
// 允许从GetObserver访问Rounting API。
Get.routing
// 检查 snackbar 是否打开
Get.isSnackbarOpen
// 检查 dialog 是否打开
Get.isDialogOpen
// 检查 bottomsheet 是否打开
Get.isBottomSheetOpen
// 删除一个路由。
Get.removeRoute()
//反复返回,直到表达式返回真。
Get.until()
// 转到下一条路由,并删除所有之前的路由,直到表达式返回true。
Get.offUntil()
// 转到下一个命名的路由,并删除所有之前的路由,直到表达式返回true。
Get.offNamedUntil()
//检查应用程序在哪个平台上运行。
GetPlatform.isAndroid
GetPlatform.isIOS
GetPlatform.isMacOS
GetPlatform.isWindows
GetPlatform.isLinux
GetPlatform.isFuchsia
//检查设备类型
GetPlatform.isMobile
GetPlatform.isDesktop
//所有平台都是独立支持web的!
//你可以知道你是否在浏览器内运行。
//在Windows、iOS、OSX、Android等系统上。
GetPlatform.isWeb
// 相当于.MediaQuery.of(context).size.height,
//但不可改变。
Get.height
Get.width
// 提供当前上下文。
Get.context
// 在你的代码中的任何地方,在前台提供 snackbar/dialog/bottomsheet 的上下文。
Get.contextOverlay
// 注意:以下方法是对上下文的扩展。
// 因为在你的UI的任何地方都可以访问上下文,你可以在UI代码的任何地方使用它。
// 如果你需要一个可改变的高度/宽度(如桌面或浏览器窗口可以缩放),你将需要使用上下文。
context.width
context.height
// 让您可以定义一半的页面、三分之一的页面等。
// 对响应式应用很有用。
// 参数: dividedBy (double) 可选 - 默认值:1
// 参数: reducedBy (double) 可选 - 默认值:0。
context.heightTransformer()
context.widthTransformer()
/// 类似于 MediaQuery.of(context).size。
context.mediaQuerySize()
/// 类似于 MediaQuery.of(context).padding。
context.mediaQueryPadding()
/// 类似于 MediaQuery.of(context).viewPadding。
context.mediaQueryViewPadding()
/// 类似于 MediaQuery.of(context).viewInsets。
context.mediaQueryViewInsets()
/// 类似于 MediaQuery.of(context).orientation;
context.orientation()
///检查设备是否处于横向模式
context.isLandscape()
///检查设备是否处于纵向模式。
context.isPortrait()
///类似于MediaQuery.of(context).devicePixelRatio。
context.devicePixelRatio()
///类似于MediaQuery.of(context).textScaleFactor。
context.textScaleFactor()
///查询设备最短边。
context.mediaQueryShortestSide()
///如果宽度大于800,则为真。
context.showNavbar()
///如果最短边小于600p,则为真。
context.isPhone()
///如果最短边大于600p,则为真。
context.isSmallTablet()
///如果最短边大于720p,则为真。
context.isLargeTablet()
///如果当前设备是平板电脑,则为真
context.isTablet()
///根据页面大小返回一个值<T>。
///可以给值为:
///watch:如果最短边小于300
///mobile:如果最短边小于600
///tablet:如果最短边(shortestSide)小于1200
///desktop:如果宽度大于1200
context.responsiveValue<T>()
# 开发填充
圆角图片
Container中使用BoxDecoration
Container(
height: ScreenAdapter.height(420),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(ScreenAdapter.width(20)),
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage("assets/imgs/xiaomiBanner2.png"),
),
),
)