这个章节又讲解一些理论的东西,可能并不会直接讲授Flutter的知识,但是会对你以后写任何的代码,都具备一些简单的知道思想;
建议大家遇到一些问题的时候,可以先查看文档和源码,因为很多东西文档中都有说明
编程范式对于初学编程的人来说是一个虚无缥缈的东西,但是却是我们日常开发中都在默认遵循的一些模式和方法;
比如我们最为熟悉的 面向对象编程
就是一种编程范式,与之对应或者结合开发的包括:面向过程编程、函数式编程、面向协议编程;
另外还有两个对应的编程范式:命令式编程 和 声明式编程
从2009年开始(数据来自维基百科),声明式编程就开始流行起来,并且目前在Vue、React、包括iOS中的SwiftUI中以及Flutter目前都采用了声明式编程。
现在我们来开发一个需求:显示一个Hello World,之后又修改成了Hello Flutter
如果是传统的命令式编程,我们开发Flutter的模式很可能是这样的:(注意是想象中的伪代码)
final text = new Text();
var title = "Hello World";
text.setContent(title);
// 修改数据
title = "Hello Flutter";
text.setContent(title);
如果是声明式编程,我们通常会维护一套数据集:
var title = "Hello World";
Text(title); // 告诉Text内部显示的是title
// 数据改变
title = "Hello Flutter";
setState(() => null); // 通知重新build Widget即可
上面的代码过于简单,可能不能体现出Flutter声明式编程的优势所在,但是在以后的开发中,我们都是按照这种模式在进行开始,我们一起来慢慢体会;
在Android中,我们使用TextView,iOS中我们使用UILabel来显示文本;
Flutter中,我们使用Text组件控制文本如何展示;
在Flutter中,我们可以将文本的控制显示分成两类
控制文本布局的参数: 如文本对齐方式 textAlign、文本排版方向 textDirection,文本显示最大行数 maxLines、文本截断规则 overflow 等等,这些都是构造函数中的参数
文本默认情况下就是包裹内容的大小,也就是说Text()组件的大小取决于其内容的大小
控制文本样式的参数: 如字体名称 fontFamily、字体大小 fontSize、文本颜色 color、文本阴影 shadows 等等,这些参数被统一封装到了构造函数中的参数 style 中
示例属性的使用
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Text(
"《定风波》 苏轼 莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。",
textAlign: TextAlign.left,//设置文本左对齐
maxLines: 2,//设置最大行数2行
overflow: TextOverflow.ellipsis,//设置超出部分...
style: TextStyle(
fontSize: 30,//设置字体
color: Colors.orange//设置字体颜色
),
);
}
}
上述代码只是展示了一些常用的属性,更多的属性用法我们需要去看源码, 还有哪些属性,大家可以多尝试下。
代码示例:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
children: [
TextSpan(text: "《定风波》", style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: Colors.black)),
TextSpan(text: "苏轼", style: TextStyle(fontSize: 18, color: Colors.redAccent)),
TextSpan(text: "\n莫听穿林打叶声,何妨吟啸且徐行。\n竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。")
],
),
style: TextStyle(fontSize: 20, color: Colors.purple),
textAlign: TextAlign.center,
);
}
}
上述我们一直在写的Text()组件,其实不是flutter最终渲染的Widegt。
void main() => runApp(MyApp())
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("基础widget"),
),
body: GYHomeContent()
)
);
}
}
上述代码中main函数选择的MyApp对象吗? 其实不是,实际渲染的MyApp中build
中返回的widget,所以我们我们要想知道使用Text小组件,flutter最终渲染的widget是什么,我们查看Text组件的build方法
上述源码中我们可以看出我们最终渲染的是RichText
的一个组件,RichText是可以直接使用的
Option + command + b
: 查看抽象类的实现类Option + command + w
:将build出的widget抽取到一个单独的widget中Option + enter
: 将Statelesswidget转换成Statefulwidgetstl
:代表StatelessWidgetstf
:StatefulWidgetOption + Enter
:在widget包裹一个新的widget。Option + Up
:选择这个widget。Option + Command + M
:方法抽离或重构。Command + Shift + Enter
:代码快速补全。Option + Command + V
: 抽离局部变量。Option + Command + F
:抽离成员变量。Command + Option + L
: 代码格式化。Command + F12
:快速查看当前文件所有方法。Command + shift + F
:全局搜索。Command + shift + R
:全局替换。Command + F
:当前文件搜索。Command + R
:当前文件替换。Material widget库中提供了多种按钮Widget如FloatingActionButton、RaisedButton、FlatButton、OutlineButton等
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
FloatingActionButton(onPressed: (){print("FloatingActionButton click");}, child: Text("FloatingActionButton")),
/**
* @Deprecated(
'Use ElevatedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
'This feature was deprecated after v1.26.0-18.0.pre.',
)
该按钮在v1.26.0-18.0.pre版本后被弃用了, 使用ElevatedButton 来替代RaisedButton
*/
RaisedButton(onPressed: (){print("RaisedButton click");}, child: Text("RaisedButton")),
ElevatedButton(onPressed: (){print("ElevatedButton click");}, child: Text("ElevatedButton")),
/**
* @Deprecated(
'Use TextButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
'This feature was deprecated after v1.26.0-18.0.pre.',
该按钮在v1.26.0-18.0.pre版本后被弃用了, 使用TextButton 来替代 FlatButton
*/
FlatButton(onPressed: (){print("FlatButton click");}, child: Text("FlatButton")),
TextButton(onPressed: (){print("TextButton click");}, child: Text("TextButton")),
/**
* @Deprecated(
'Use OutlinedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
'This feature was deprecated after v1.26.0-18.0.pre.',
)
该按钮在v1.26.0-18.0.pre版本后被弃用了, 使用OutlinedButton 来替代 OutlineButton
*/
OutlineButton(onPressed: (){print("OutlineButton click");}, child: Text("OutlineButton")),
OutlinedButton(onPressed: (){print("OutlinedButton click");}, child: Text("OutlinedButton"))
],
);
}
}
如果想要自己定制一些按钮, 我们完全可以自己通过按钮中的一些属性来控制,达到自己想要的按钮样式,大家可以自行演示下
默认情况下Button上下会有一定的间距
RaisedButton
和ElevateButton
上面和下面好像都有些许的间距,主要是button有一个属性,MaterialTapTargetSize
这个属性,这是一个枚举,默认值是padded
,看源码描述是,如果按钮的大小不足48px
大小,会默认扩展到48px
的大小。如果我们想要按钮今包裹内容,那么我们可以设置MaterialTapTargetSize
这个属性的值为shrinkWrap
,这样按钮的大小就会包裹内容那么我们如果让Button的大小变小点了?这里以FlatButton为例
我们通过文档和源码,我们都可以看到,FlatButton
最终是继承自MaterialButton
,然后继承自StatelessWidget
,我们知道,如果widget是继承自StatelessWidget
的,那么flutter最终渲染的是build
返回的组件
文档描述:
通过文档我们可以看到,最终渲染的ButtonTheme
有一个默认大小,然后在结合源码,我们可以看到ThemeButton
有两个属性,minwidth
和minheight
表示最小宽度和最小高度的
由源码可知我们的ThemeButton
是通过上下文来获取,所以我们可以FlatButton
的外层包裹一个ThemeButton
然后设置minWidth、minHeight
,然后通过这个两个属性我们就可以调整任意大小的Button
了
虽然这里我们设置了最小宽度,但是如果我们的Button
是有内容的,这个Button
还是可以包裹内容的
Button
有一个属性padding
,通过修改这个属性我们可以达到修改Button
内边距的问题,这个属性和上面讲述的ButtonTheme
的最小宽和最小高的属性可能会冲突图片可以让我们的应用更加丰富多彩,Flutter中使用Image组件
Image组件有很多的构造函数,我们这里主要学习两个:
相对来讲,Flutter中加载网络图片会更加简单,直接传入URL并不需要什么配置,所以我们先来看一下Flutter中如何加载网络图片
我们先来看看Image有哪些属性可以设置
const Image({
required this.image, //必传参数 图片
...
this.width, //图片的宽
this.height, //图片的高
this.color, // 图片的混合色值
this.colorBlendMode, //混合模式
this.fit, //缩放模式
this.alignment = Alignment.center, //对齐方式
this.repeat = ImageRepeat.noRepeat, //重复方式
...
})
width、height
:用于设置图片的宽、高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width、height的其中一个,那么另一个属性默认会按比例缩放,但可以通过下面介绍的fit属性来指定适应规则。fit
:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit中定义,它是一个枚举类型,有如下值:
fill
:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。cover
:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。contain
:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。fitWidth
:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。fitHeight
:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。none
:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。color
和 colorBlendMode
:在图片绘制时可以对每一个像素进行颜色混合处理,color指定混合色,而colorBlendMode
指定混合模式;repeat
:当图片本身大小小于显示空间时,指定图片的重复规则。示例代码
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Image.network(
"http://img0.dili360.com/ga/M01/48/3C/wKgBy1kj49qAMVd7ADKmuZ9jug8377.tub.jpg",
alignment: Alignment.topCenter,//顶部居中
repeat: ImageRepeat.repeatY, //Y轴上重复
color: Colors.red,
colorBlendMode: BlendMode.colorBurn,
),
width: 300,
height: 300,
),
);
}
}
加载本地图片这里比较麻烦一点,需要配置。
pubspec.yaml
文件中配置图片资源class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Image.asset("images/test.jpeg"),
width: 300,
height: 300,
),
);
}
}
CircleAvatar可以实现圆角头像,也可以添加一个子Widget:
const CircleAvatar({
Key? key,
this.child, //子widget
this.backgroundColor, // 背景颜色
this.backgroundImage, // 背景图像
this.foregroundImage, // 前景颜色
...
this.foregroundColor,
this.radius, //半径
this.minRadius,//最小半径
this.maxRadius,//最大半径
})
我们需要实现一个圆形头像
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CircleAvatar(
child: Text("圆角图片", style: TextStyle(color: Colors.orange)),
backgroundImage: NetworkImage("https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg"),
radius: 80,
);
}
}
CircleAvatar
:本身就是一个圆角的widget,这里backgroundImage
,属性需要我们传入一个ImageProvider?
类型的小组件, 但是我们发现该组件是一个抽象类,然后我们可以查看该抽象类的实现类,所以这里我们传入的是NetworkImage
小组件ClipOval也可以实现圆角头像,而且通常是在只有头像时使用
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipOval(
child: Image.network(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg",
width: 200,
height: 200,
),
);
}
}
//如果图片是不规则的, 我们可以调整图片的填充方式,来达到圆角充满的效果,但是这里的图片可能会根据你设置的不同模式的填充方式,有拉伸效果
ClipOval(
child: Image.asset("images/test.jpeg",width: 200,height: 200,fit: BoxFit.fill,),
);
ClipRRect用于实现圆角效果,可以设置圆角的大小。
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(20),//随意调整圆角大小
child: Image.network(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg",
width: 200,
height: 200,
),
),
);
}
}
我们可以使用FadeImage
组件来加载网络图片,然后设置占位图,该组件本身自带淡出效果,我们可以通过设置两个属性来达到控制动画效果的时间
@override
Widget build(BuildContext context) {
// 1.占位图的问题: FadeInImage
return FadeInImage(
fadeOutDuration: Duration(milliseconds: 1),//占位图消失时间
fadeInDuration: Duration(milliseconds: 1),//网络图片展示时间
placeholder: AssetImage("assets/images/juren.jpeg"),//占位图
image: NetworkImage(imageURL),//真正显示的网络图片
);
}
首先在flutter开发中,我们可能多个地方使用同一张网络图片, 我们可能想这个图片的缓存怎么处理(如果不错缓存每次都加载同一张图片是非常消耗性能的)?
flutter默认情况下是会给我们的图片做缓存的,fliutter在加载图片的时候,如果发现图片的地址和图片的缩放,就会直接加载原来的那张图片
,所以flutter对图片默认情况下是有内存缓存的,默认最多缓存1000张图片,最大只能缓存100M的空间
根据文档我们发现可以去调整 缓存的大小(maximumSize
),但是我们一般不去调整
通过源码我们看出Icon
需要我们传入一个IconData
的widget,其实flutter已经帮我们创建好了很多这种IconData
,直接使用Icons
这个widget就可以了
@override
Widget build(BuildContext context) {
// return Icon(Icons.pets, size: 300, color: Colors.orange,);
// return Icon(IconData(0xe91d, fontFamily: 'MaterialIcons'), size: 300, color: Colors.orange,);
return Text("\ue91d", style: TextStyle(fontSize: 100, color: Colors.orange, fontFamily: "MaterialIcons"),);
}
TextField用于接收用户的文本输入,它提供了非常多的属性,我们来看一下源码:
const TextField({
Key key,
this.controller,
this.focusNode,
this.decoration = const InputDecoration(),
TextInputType keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
ToolbarOptions toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength,
this.maxLengthEnforced = true,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.onTap,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
})
我们来学习几个常见的属性把
keyboardType
键盘的类型,style
设置样式,textAlign
文本对齐方式,maxLength
最大显示行数等等;decoration
:用于设置输入框相关的样式
icon
:设置左边显示的图标labelText
:在输入框上面显示一个提示的文本hintText
:显示提示的占位文字border
:输入框的边框,默认底部有一个边框,可以通过InputBorder.none
删除掉filled
:是否填充输入框,默认为falsefillColor
:输入框填充的颜色suffixIcon
: 输入框右侧(尾部)显示widgetprefixIcon
:输入框左侧(前面)显示widgetcontroller
:关联的控制器onChanged
:监听输入框内容的改变,传入一个回调函数onSubmitted
:点击键盘中右下角的down
时,会回调的一个函数import 'package:flutter/material.dart';
class TextFieldDemo extends StatelessWidget {
final usernameTextEditController = TextEditingController();
final passwordTextEditController = TextEditingController();
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
primaryColor: Colors.red //可以达到控制输入框边框的颜色
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
TextField(
controller: usernameTextEditController,
decoration: InputDecoration(
labelText: "username",
icon: Icon(Icons.people),
hintText: "请输入用户名",
border: InputBorder.none,//去掉边框
filled: true,
fillColor: Colors.red[100]
),
onChanged: (value) {
print("onChange:$value");
},
onSubmitted: (value) {
print("onSubmitted:$value");
},
),
SizedBox(height: 10,),
TextField(
controller: passwordTextEditController,
decoration: InputDecoration(
labelText: "password",
icon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
SizedBox(height: 10,),
Container(
width: double.infinity,
height: 40,
child: FlatButton(
child: Text("登 录", style: TextStyle(fontSize: 20, color: Colors.white),),
color: Colors.blue,
onPressed: () {
// 1.获取用户名和密码
final username = usernameTextEditController.text;
final password = passwordTextEditController.text;
print("账号:$username 密码:$password");
usernameTextEditController.text = "";
passwordTextEditController.text = "";
},
),
)
],
),
),
);
}
}
运行结果:
TextField
添加一个控制器(Controller
),可以使用它设置文本的初始值,也可以使用它来监听文本的改变;TextField
提供一个Controller
,那么会Flutter
会默认创建一个TextEditingController
的,这个结论可以通过阅读源码得到: @override
void initState() {
super.initState();
// ...其他代码
if (widget.controller == null)
_controller = TextEditingController();
}
我们也可以自己来创建一个Controller控制一些内容:
class _TextFieldDemoState extends State<TextFieldDemo> {
final textEditingController = TextEditingController();
@override
void initState() {
super.initState();
// 1.设置默认值
textEditingController.text = "Hello World";
// 2.监听文本框
textEditingController.addListener(() {
print("textEditingController:${textEditingController.text}");
});
}
// ...省略build方法
}
在我们开发注册、登录页面时,通常会有多个表单需要同时获取内容或者进行一些验证,如果对每一个TextField都分别进行验证,是一件比较麻烦的事情。
做过前端的开发知道,我们可以将多个input标签放在一个form里面,Flutter也借鉴了这样的思想:我们可以通过Form对输入框进行分组,统一进行一些操作。
Form表单也是一个Widget,可以在里面放入我们的输入框。
但是Form表单中输入框必须是FormField类型的
我们通过Form的包裹,来实现一个注册的页面:
class FormDemo extends StatefulWidget {
@override
_FormDemoState createState() => _FormDemoState();
}
class _FormDemoState extends State<FormDemo> {
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
icon: Icon(Icons.people),
labelText: "用户名或手机号"
),
),
TextFormField(
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: "密码"
),
),
SizedBox(height: 16,),
Container(
width: double.infinity,
height: 44,
child: RaisedButton(
color: Colors.lightGreen,
child: Text("注 册", style: TextStyle(fontSize: 20, color: Colors.white),),
onPressed: () {
print("点击了注册按钮");
},
),
)
],
),
);
}
}
有了表单后,我们需要在点击注册时,可以同时获取和保存表单中的数据,怎么可以做到呢?
onPressed
传入的回调中来做即可。(当然,如果嵌套太多,我们待会儿可以将它抽取到一个单独的方法中)用户名
和密码
的表单信息。用户名
和密码
的表单信息?Form
的State
对象的save
方法,就会调用Form
中放入的TextFormField
的onSave
回调:TextFormField(
decoration: InputDecoration(
icon: Icon(Icons.people),
labelText: "用户名或手机号"
),
onSaved: (value) {
print("用户名:$value");
},
),
Form对象
来调用它的save方法呢?
案例代码演示:
class FormDemo extends StatefulWidget {
@override
_FormDemoState createState() => _FormDemoState();
}
class _FormDemoState extends State<FormDemo> {
final registerFormKey = GlobalKey<FormState>();
String username, password;
void registerForm() {
registerFormKey.currentState.save();
print("username:$username password:$password");
}
@override
Widget build(BuildContext context) {
return Form(
key: registerFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
icon: Icon(Icons.people),
labelText: "用户名或手机号"
),
onSaved: (value) {
this.username = value;
},
),
TextFormField(
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: "密码"
),
onSaved: (value) {
this.password = value;
},
),
SizedBox(height: 16,),
Container(
width: double.infinity,
height: 44,
child: RaisedButton(
color: Colors.lightGreen,
child: Text("注 册", style: TextStyle(fontSize: 20, color: Colors.white),),
onPressed: registerForm,
),
)
],
),
);
}
}
在表单中,我们可以添加验证器
,如果不符合某些特定的规则,那么给用户一定的提示信息
比如我们需要账号和密码有这样的规则:账号和密码都不能为空。
按照如下步骤就可以完成整个验证过程:
也可以为TextFormField添加一个属性:autovalidate
不需要调用validate方法,会自动验证是否符合要求;