Flutter页面-基础Widget
在Flutter几乎所有的对象都是一个`Widget`,在本土开发中**控件**不同的是,Flutter中的`widget`概念更广泛,不仅可以表示UI元素,也可以表示一些功能组件,如手势检测 `GestureDetector` widget、用于应用主题数据传输`Theme`等等。由于Flutter它主要用于构建用户界面。因此,在大多数情况下,可以认为widget它是一个控件,不用担心概念。
Widget描述一个的功能UI元素配置数据,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter屏幕上显示幕上显示元素的类别是`Element`,也就是说Widget只是描述`Element`的一个配置。一个Widget可对应多个`Element`,因为是一样的Widget可以添加对象UI当树的不同部分真正渲染时,UI树的每个节点都对应一个节点`Element`对象。
Widget
`StatelessWidget`和`StatefulWidget`是`flutter`日常开发中自定义的基本组件`Widget`他们都选择继承这两者之一。也是在未来的开放中,我们最接触的Widget:
- `StatelessWidget`:无状态的,向那些始终不变的人展示信息UI控件;
- `StatefulWidget`:如果有状态,可以通过改变状态使状态 UI 变化可以包括用户交互(例如弹出一个) dialog)。
在实际使用中,Stateless与Stateful这取决于选择 Widget 无论是有状态还是无状态,简单看界面是否需要更新。
Stateless Widget
StatelessWidget它通常用于不需要维护状态的场景`build`嵌套其他方法Widget来构建UI,其嵌套将在施工过程中递归施工Widget。
> `BuildContext`表示构建widget上下文是操作widget一个句柄位于树中,它包含了一些搜索,遍历现在Widget树的一些方法。widget都有自己的context对象。
import 'package:flutter/material.dart'; void main() => runApp(StatelessApp()); class StatelessApp extends StatelessWidget { ///在build嵌套其他方法Widget来构建UI,其嵌套将在施工过程中递归施工Widget @override Widget build(BuildContext context) { //嵌套 MaterialApp:实现应用程序封装Material Design需要的widget return MaterialApp( title: "Widget演示", //标题,显示在recent时候的标题 //主页面 //Scaffold : Material Design布局结构的基本实现。 home: Scaffold( //ToolBar/ActionBar appBar: AppBar(title: Text("Widget")), body: Text("Hello,Flutter!"), ) ); } }
Material Design: > 设计语言,Material Design 于2014年的 Google I/O 第一次亮相是谷歌推出的全新设计语言。说白了,是一种设计风格。
Stateful Widget
StatefulWidget它是动态的,添加了一个新的接口`createState()`用于创建和Stateful widget相关的状态`State`,它在Stateful widget生命周期可能会被多次调用。
当State变更时,可手动调用`setState()`方法通知Flutter framework状态发生改变,Flutter framework收到消息后,将重新调用`build`重建方法widget树了更新树木UI的目的。
class StatefulState extends State<StatefulApp> { int _i; ///当Widget第一次插入Widget树每一棵树来说,都会被调用State对象,Flutter framework回调只调用一次 @override void initState() { super.initState(); _i = 1; } @override Widget build(BuildContext context) { return MaterialApp( title: "Widget演示", theme: ThemeData(), home: Scaffold( appBar: AppBar(title: Text("Widget")), body: RaisedButton( onPressed: () { ///修改状态,setState会重新调用build更新ui setState(() { _i ; }); }, child: Text("Hello,Flutter! $_i"), ), )); } }
State生命周期
State类除了`build`此外,还有许多方法可以让我们重写,这些方法将在不同的状态下从Flutter因此,我们称之为这些方法**生命周期**方法。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { bool isShowChild; ///当Widget第一次插入到Widget树会被调用,对于每一个State对象,Flutter framework回调只调用一次 @override void initState() { super.initState(); isShowChild = true; debugPrint("parent initState..."); } /////初始化,在initState()立即调用 ///作为依赖InheritedWidget rebuild,该接口将被调用触发 @override void didChangeDependencies() { super.didChangeDependencies(); debugPrint("parent didChangeDependencies..."); } ////绘制界面,当setState触发时会再次调用 @override Widget build(BuildContext context) { debugPrint("parent build..."); return MaterialApp( home: Scaffold( body: Center( child: RaisedButton( onPressed: () { setState(() { isShowChild = !isShowChild; }); }, child: isShowChild ? Child() : Text("演示移除Child"), )), ), ); } //////状态变化时会调用这种方法,比如调用了setState @override void didUpdateWidget(MyApp oldWidget) { super.didUpdateWidget(oldWidget); debugPrint("parent didUpdateWidget..."); } ///当State当物体从树上移除时,此回调将被调用 @override void deactivate() { super.deactivate(); debugPrint('parent deactivate...'); } ///当State当物体从树上永久移除时,通常在此回调中释放资源 @override void dispose() { super.dispose(); debugPrint('parent dispose...'); } } class Child extends StatefulWidget { @override _ChildState createState() => _ChildState(); } class _ChildState extends State<Child> { @ovrride
Widget build(BuildContext context) {
debugPrint("child build......");
return Text('lifeCycle');
}
@override
void initState() {
super.initState();
debugPrint("child initState......");
}
///初始化时,在initState()之后立刻调用
///当依赖的InheritedWidget rebuild,会触发此接口被调用
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("child didChangeDependencies......");
}
///父widget状态改变的时候会调用该方法,比如父节点调用了setState
@override
void didUpdateWidget(Child oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint("child didUpdateWidget......");
}
///当State对象从树中被移除时,会调用此回调
@override
void deactivate() {
super.deactivate();
debugPrint('child deactivate......');
}
///当State对象从树中被永久移除时调用;通常在此回调中释放资源
@override
void dispose() {
super.dispose();
debugPrint('child dispose......');
}
}
执行的输出结果显示为:
- 运行到显示
```dart I/flutter (22218): parent initState...... I/flutter (22218): parent didChangeDependencies...... I/flutter (22218): parent build...... I/flutter (22218): child initState...... I/flutter (22218): child didChangeDependencies...... I/flutter (22218): child build...... ```
- 点击按钮会移除Child
```dart I/flutter (22218): parent build...... I/flutter (22218): child deactivate...... I/flutter (22218): child dispose...... ```
- 将MyApp的代码由` child: isShowChild ? Child() : Text("演示移除Child")`,改为` child: Child()`,点击按钮时
```dart I/flutter (22765): parent build...... I/flutter (22765): child didUpdateWidget...... I/flutter (22765): child build...... ```
从这些实验中能够得出State的生命周期
基础widget文本显示
Text
`Text`是展示单一格式的文本Widget(Android `TextView`)。
import 'package:flutter/material.dart';
///
/// main方法 调用runApp传递Widget,这个Widget成为widget树的根
void main() => runApp(TextApp());
///
/// 1、单一文本Text
///
//创建一个无状态的Widget
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//封装了应用程序实现Material Design所需要的一些widget
return MaterialApp(
title: "Text演示", //标题,显示在recent时候的标题
//主页面
//Scaffold : Material Design布局结构的基本实现。
home: Scaffold(
//ToolBar/ActionBar
appBar: AppBar(title: Text("Text")),
body: Text("Hello,Flutter"),
),
);
}
}
在使用`Text`显示文字时候,可能需要对文字设置各种不同的样式,类似Android的 `android:textColor/Size`等
在Flutter中也拥有类似的属性
Widget _TextBody() {
return Text(
"Hello,Flutter",
style: TextStyle(
//颜色
color: Colors.red,
//字号 默认14
fontSize: 18,
//粗细
fontWeight: FontWeight.w800,
//斜体
fontStyle: FontStyle.italic,
//underline:下划线,overline:上划线,lineThrough:删除线
decoration: TextDecoration.lineThrough,
decorationColor: Colors.black,
//solid:实线,double:双线,dotted:点虚线,dashed:横虚线,wavy:波浪线
decorationStyle: TextDecorationStyle.wavy),
);
}
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Text演示",
home: Scaffold(
appBar: AppBar(title: Text("Text")),
body: _TextBody(),
),
);
}
}
RichText
如果需要显示更为丰富样式的文本(比如一段文本中文字不同颜色),可以使用`RichText`或者`Text.rich`
Widget _RichTextBody() {
var textSpan = TextSpan(
text: "Hello",
style: TextStyle(color: Colors.red),
children: [
TextSpan(text: "Flu", style: TextStyle(color: Colors.blue)),
TextSpan(text: "uter", style: TextStyle(color: Colors.yellow)),
],
);
//Text.rich(textSpan);
return RichText(text: textSpan);
}
DefaultTextStyle
在widget树中,文本的样式默认是可以被继承的,因此,如果在widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式。相当于在Android中定义 Theme
Widget _DefaultStyle(){
DefaultTextStyle(
//设置文本默认样式
style: TextStyle(
color:Colors.red,
fontSize: 20.0,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Hello Flutter!"),
Text("Hello Flutter!"),
Text("Hello Flutter!",
style: TextStyle(
inherit: false, //不继承默认样式
color: Colors.grey
),
),
],
),
);
}
图片显示
"图文":有文字显示Widget,又怎么少的了图片呢。
FlutterLogo
这个Widget用于显示Flutter的logo......
Widget flutterLogo() {
return FlutterLogo(
//大小
size: 100,
//logo颜色 默认为 Colors.blue
colors: Colors.red,
//markOnly:只显示logo,horizontal:logo右边显示flutter文字,stacked:logo下面显示文字
style: FlutterLogoStyle.stacked,
//logo上文字颜色
textColor: Colors.blue,
);
}
Icon
主要用于显示内置图标的`Widget`
Widget icon() {
return Icon(
//使用预定义Material icons
// https://docs.flutter.io/flutter/material/Icons-class.html
Icons.add,
size: 100,
color: Colors.red);
}
Image
显示图片的`Widget`。图片常用的格式主要有bmp,jpg,png,gif,webp等,Android中并不是天生支持gif和webp动图,但是这一特性在flutter中被很好的支持了。
| 方式 | 解释 | | ------------- | ----------------------------------------------------------- | | Image() | 使用ImageProvider提供图片,如下方法本质上也是使用的这个方法 | | Image.asset | 加载资源图片 | | Image.file | 加载本地图片文件 | | Image.network | 加载网络图片 | | Image.memory | 加载内存图片 |
Iamge.asset
在工程目录下创建目录,如:assets,将图片放入此目录。打开项目根目录:pubspec.yaml
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: Image.asset("assets/banner.jpeg"),
),
);
Image.file
在sd卡中放入一张图片。然后利用**path_provider**库获取sd卡根目录(Dart库版本可以在:https://pub.dartlang.org/packages查询)。
**注意权限**
class ImageState extends State<ImageApp> {
Image image;
@override
void initState() {
super.initState();
getExternalStorageDirectory().then((path) {
setState(() {
image = Image.file(File("${path.path}${Platform.pathSeparator}banner.jpeg"));
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: image,
),
);
}
}
Image.network
直接给网络地址即可。
> Flutter 1.0,加载https时候经常出现证书错误。必须断开AS打开app
Image.memory
Future<List<int>> _imageByte() async {
String path = (await getExternalStorageDirectory()).path;
return await File("$path${Platform.pathSeparator}banner.jpeg").readAsBytes();
}
class ImageState extends State<ImageApp> {
Image image;
@override
void initState() {
super.initState();
_imageByte().then((bytes) {
setState(() {
image = Image.memory(bytes);
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: image,
),
);
}
}
fit属性相当于android中的scaletype,定义如下:
| fit | 说明 | 效果 | | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | BoxFit.fill | 填充,忽略原有的宽高比,填满为止 |  | | BoxFit.contain | 包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景 |  | | BoxFit.cover | 覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪 |  | | BoxFit.fitWidth | 横向图片填充 |  | | BoxFit.fitHeight | 纵向图片填充 |  | | BoxFit.none | 原始大小居中 |  | | BoxFit.scaleDown | 图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain |  |
CircleAvatar
主要用来显示用户的头像,任何图片都会被剪切为圆形。
CircleAvatar(
//图片提供者 ImageProvider
backgroundImage: AssetImage("assets/banner.jpeg"),
//半径,控制大小
radius: 50.0,
);
FadeInImage
当使用默认`Image` widget显示图片时,您可能会注意到它们在加载完成后会直接显示到屏幕上。这可能会让用户产生视觉突兀。如果最初显示一个占位符,然后在图像加载完显示时淡入,我们可以使用`FadeInImage`来达到这个目的!
image = FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: 'https://flutter.io/images/homepage/header-illustration.png',
);
按钮
Material widget库中提供了多种按钮Widget如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton的包装定制,所以他们大多数属性都和`RawMaterialButton`一样。所有Material 库中的按钮都有如下相同点:
1. 按下时都会有“水波动画”。 2. 有一个`onPressed`属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。
RaisedButton
"漂浮"按钮,它默认带有阴影和灰色背景
RaisedButton(
child: Text("normal"),
onPressed: () => {},
)
FlatButton
扁平按钮,默认背景透明并不带阴影
FlatButton(
child: Text("normal"),
onPressed: () => {},
)
OutlineButton
默认有一个边框,不带阴影且背景透明。
OutlineButton(
child: Text("normal"),
onPressed: () => {},
)
IconButton
可点击的Icon
IconButton(
icon: Icon(Icons.thumb_up),
onPressed: () => {},
)
按钮外观可以通过其属性来定义,不同按钮属性大同小异
const FlatButton({
...
@required this.onPressed, //按钮点击回调
this.textColor, //按钮文字颜色
this.disabledTextColor, //按钮禁用时的文字颜色
this.color, //按钮背景颜色
this.disabledColor,//按钮禁用时的背景颜色
this.highlightColor, //按钮按下时的背景颜色
this.splashColor, //点击时,水波动画中水波的颜色
this.colorBrightness,//按钮主题,默认是浅色主题
this.padding, //按钮的填充
this.shape, //外形
@required this.child, //按钮的内容
})
FlatButton(
onPressed: () => {},
child: Text("Raised"),
//蓝色
color: Colors.blue,
//水波
splashColor: Colors.yellow,
//深色主题,这样文字颜色会变成白色
colorBrightness: Brightness.dark,
//圆角按钮
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50)
),
)
而`RaisedButton`,默认配置有阴影,因此在配置`RaisedButton` 时,拥有一系列 elevation 属性的配置
const RaisedButton({
...
this.elevation = 2.0, //正常状态下的阴影
this.highlightElevation = 8.0,//按下时的阴影
this.disabledElevation = 0.0,// 禁用时的阴影
...
}
输入框
import 'package:flutter/material.dart';
void main() => runApp(Demo1());
class Demo1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Demo1",
home: Scaffold(
appBar: AppBar(
title: Text("登录"),
),
//线性布局,垂直方向
body: Column(
children: <Widget>[
TextField(
//自动获得焦点
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)),
),
TextField(
//隐藏正在编辑的文本
obscureText: true,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)),
),
],
),
),
);
}
}
这个效果非常的“系统”,我们可能大多数情况下需要将下划线更换为矩形边框,这时候可能就需要组合widget来完成:
//容器 设置一个控件的尺寸、背景、margin
Container(
margin: EdgeInsets.all(32),
child: TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person),
border: InputBorder.none //隐藏下划线
)),
//装饰
decoration: BoxDecoration(
// 边框浅灰色,宽度1像素
border: Border.all(color: Colors.red[200], width: 1.0),
//圆角
borderRadius: BorderRadius.circular(5.0),
),
)
焦点控制
FocusNode: 与Widget绑定,代表了这个Widget的焦点
FocusScope: 焦点控制范围
FocusScopeNode:控制焦点
class _TextFocusState extends State<TextFocusWidget> {
FocusNode focusNode1 = new FocusNode();
FocusNode focusNode2 = new FocusNode();
void _listener() {
debugPrint("用户名输入框焦点:${focusNode1.hasFocus}");
}
@override
void initState() {
super.initState();
//监听焦点状态改变事件
focusNode1.addListener(_listener);
}
@override
void dispose() {
super.dispose();
focusNode1.dispose();
focusNode2.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
autofocus: true,
//关联焦点
focusNode: focusNode1,
//设置键盘动作为: 下一步
textInputAction: TextInputAction.next,
//点击下一步执行回调
onEditingComplete: () {
//获得 context对应UI树的焦点范围 的焦点控制器
FocusScopeNode focusScopeNode = FocusScope.of(context);
//将焦点交给focusNode2
focusScopeNode.requestFocus(focusNode2);
},
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)),
),
TextField(
//隐藏正在编辑的文本
obscureText: true,
focusNode: focusNode2,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)),
),
custom(),
],
);
}
}
获取输入内容
获取输入内容有两种方式:
1. 定义两个变量,用于保存用户名和密码,然后在onChange触发时,各自保存一下输入内容。 2. 通过controller直接获取。
onChange获得输入内容:
TextField(
onChanged: (s) => debugPrint("ssss:$s"),
)
controller获取:
定义一个controller:
//定义一个controller
TextEditingController _unameController=new TextEditingController();
```
然后设置输入框controller:
```dart
TextField(
controller: _unameController, //设置controller
...
)
通过controller获取输入框内容
debugPrint(_unameController.text)
TextFormField
`TextFormField`比`TextField`多了一些属性,其中 ***validator***用于设置验证回调。在单独使用时与`TextField`没有太大的区别。当结合`From`,利用`From`可以对输入框进行分组,然后进行一些统一操作(验证)
class _TextFocusState extends State<TextFocusWidget> {
//全局key
GlobalKey<FormState> _key = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
//类似 id
key: _key,
child: Column(
children: <Widget>[
TextFormField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person)),
// 校验用户名
validator: (v) {
return v.trim().length > 0 ? null : "用户名不能为空";
}),
TextFormField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock)),
// 校验用户名
validator: (v) {
return v.trim().length > 0 ? null : "密码不能为空";
}),
RaisedButton(
onPressed: () {
//Form所有TextFormField成功 返回true
if (_key.currentState.validate()) {
}
},
child: Text("提交"),
)
],
));
}
}
第一个页面
入口函数
一个Flutter工程的入口函数与Dart命令行工程一样是`main`,不同的是在Flutter中执行`runApp(ArticleApp())` 就能够在手机屏幕上展示这个Widget。
import 'package:flutter/material.dart';
void main() => runApp(new ArticleApp());
ArticleApp
我们要实现的文章列表页面UI就在`ArticleApp`中定义:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text(
'文章',
style: const TextStyle(color: Colors.white),
),
),
body: new ArticlePage(),
),
);
}
}
`build`方法中返回的就是我们需要显示在屏幕上的widget。`MaterialApp`代表使用Material Design风格,这是一个封装了很多Android MD设计所必须要的组件的小部件。假设我们需要显示一个`Text`,而没有包裹在`MaterialApp`内:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//Center:摆放在中间
return Center(
child: Text("你好!"),
);
}
}
如果直接运行则会出现异常,因为Flutter不知道以什么顺序摆放文字
因此我们不得不给`Text`指名`textDirection`属性:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("你好!",
style: const TextStyle(color: Colors.white),
textDirection: TextDirection.ltr),
);
}
}
但是如果包含在`MaterialApp`当中我们就不需要指名类似`textDirection`这样的属性了,因为内部已经内置了一套风格,指明了这些必须属性。而`Scaffold`则实现了基本的 Material Design布局结构,在 Material 设计中定义的单个界面上的各种布局元素,在 Scaffold 中都支持。比如:AppBar、抽屉菜单、BottomNavigationBar等等。
ArticlePage
在我们的布局中指定了`Scaffold`的body(主体)为`ArticlePage`,这是一个我们自定义的组合Widget。
class ArticlePage extends StatefulWidget {
@override
_ArticlePageState createState() => _ArticlePageState();
}
class _ArticlePageState extends State<ArticlePage> {
///滑动控制器
ScrollController _controller = new ScrollController();
///控制小菊花的显示
bool _isLoading = true;
///请求到的文章数据
List articles = [];
///banner图
List banners = [];
///总文章数有多少
var listTotalSize = 0;
///分页加载,当前页码
var curPage = 0;
@override
void initState() {
super.initState();
_controller.addListener(() {
///获得 SrollController 监听控件可以滚动的最大范围
var maxScroll = _controller.position.maxScrollExtent;
///获得当前位置的像素值
var pixels = _controller.position.pixels;
///当前滑动位置到达底部,同时还有更多数据
if (maxScroll == pixels && articles.length < listTotalSize) {
///加载更多
_getArticlelist();
}
});
_pullToRefresh();
}
_getArticlelist([bool update = true]) async {
/// 请求成功是map,失败是null
var data = await Api.getArticleList(curPage);
if (data != null) {
var map = data['data'];
var datas = map['datas'];
///文章总数
listTotalSize = map["total"];
if (curPage == 0) {
articles.clear();
}
curPage++;
articles.addAll(datas);
///更新ui
if (update) {
setState(() {});
}
}
}
_getBanner([bool update = true]) async {
var data = await Api.getBanner();
if (data != null) {
banners.clear();
banners.addAll(data['data']);
if (update) {
setState(() {});
}
}
}
///下拉刷新
Future<void> _pullToRefresh() async {
curPage = 0;
Iterable<Future> futures = [_getArticlelist(), _getBanner()];
await Future.wait(futures);
_isLoading = false;
setState(() {});
return null;
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
///小菊花
Offstage(
offstage: !_isLoading, //是否隐藏
child: new Center(child: CircularProgressIndicator()),
),
///内容
Offstage(
offstage: _isLoading,
child: new RefreshIndicator(
child: ListView.builder(
itemCount: articles.length + 1,
itemBuilder: (context, i) => _buildItem(i),
controller: _controller,
),
onRefresh: _pullToRefresh),
)
],
);
}
Widget _buildItem(int i) {
if (i == 0) {
return new Container(
height: 180.0,
child: _bannerView(),
);
}
var itemData = articles[i - 1];
return new ArticleItem(itemData);
}
Widget _bannerView() {
var list = banners.map((item) {
return Image.network(item['imagePath'], fit: BoxFit.cover);
}).toList();
return list.isNotEmpty
? BannerView(
list,
intervalDuration: const Duration(seconds: 3),
)
: null;
}
}
这个Widget的代码比较多,它配置了我们见到的banner、与文章列表。代码中重写了State的生命周期方法`initState`与`build`。我们首先来观察`build`方法:
@override
Widget build(BuildContext context) {
//Stack:帧布局
return Stack(
children: <Widget>[
///正在加载
Offstage( //可以控制是否隐藏
offstage: !_isLoading, //是否隐藏
child: new Center(child: CircularProgressIndicator()),//圆形进度指示器(小菊花)
),
///内容
Offstage(
offstage: _isLoading,
child: new RefreshIndicator( //下拉刷新
child: ListView.builder(
itemCount: articles.length + 1, //列表视图的个数
itemBuilder: (context, i) => _buildItem(i),//类似adapter,item显示什么?返回widget
controller: _controller,//滑动控制器
),
onRefresh: _pullToRefresh),//刷新回调方法
)
],
);
}
这段代码中各个部分都给到了注释,`_buildItem`与`_pullToRefresh`方法分别用于条目视图的生成与新数据的获取。
_pullToRefresh
`_pullToRefresh`是传递给下拉刷新组件:`RefreshIndicator`的刷新回调方法参数,它需要返回一个`Future<void>`,同时我们初次进入页面也需要自动的去获取一次数据,所以我们还会在`initState`方法中主动的调用一次该方法。
Future<void> _pullToRefresh() async {
curPage = 0;
Iterable<Future> futures = [_getArticlelist(), _getBanner()];
await Future.wait(futures);
_isLoading = false;
setState(() {});
return null;
}
在这个方法中,我们需要重新请求文章列表与banner图,因此借助`Future.wait`组合两个任务,在两个任务都完成后,再利用`setState`更新UI完成重绘。
_buildItem
获取到数据之后,接下来我们需要对这些数据进行展示
Widget _buildItem(int i) {
if (i == 0) {
return new Container(
height: 180.0,
child: _bannerView(),
);
}
var itemData = articles[i - 1];
return new ArticleItem(itemData);
}
Widget _bannerView() {
///banners是请求到的banner信息组,其中imagePath代表了图片地址
///map意为映射,对banners中的数据进行遍历并返回Iterable<?>迭代器,
///?则是在map的参数:一个匿名方法中返回的类型
var list = banners.map((item) {
return Image.network(item['imagePath'], fit: BoxFit.cover);
}).toList();
///BannerView的条目不能为空
return list.isNotEmpty
? BannerView(
list,
///切换时间
intervalDuration: const Duration(seconds: 3),
)
: null;
}
`_buildItem`用于生成ListView当中的条目。注意在配置ListView时,我们给的`itemCount`为:`articles.length + 1`。articles就是请求到的文章信息数量,而**+1**则是为了显示banner。因此当`i=0`,显示第一个条目时候,我们返回了一个`BannerView`。这个`BannerView`其实是一个库(关于如何导入第三方库在最后)。而`ArticleItem`则又是我们自己定义的用于显示文章信息item的组合Widget。
ArticleItem
class ArticleItem extends StatelessWidget {
final itemData;
const ArticleItem(this.itemData);
@override
Widget build(BuildContext context) {
///时间与作者
Row author = new Row( //水平线性布局
children: <Widget>[
//expanded 最后摆我,相当于linearlayout的weight权重
new Expanded(
child: Text.rich(TextSpan(children: [
TextSpan(text: "作者: "),
TextSpan(
text: itemData['author'],
style: new TextStyle(color: Theme.of(context).primaryColor))
]))),
new Text(itemData['niceDate'])//时间
],
);
///标题
Text title = new Text(
itemData['title'],
style: new TextStyle(fontSize: 16.0, color: Colors.black),
textAlign: TextAlign.left,
);
///章节名
Text chapterName = new Text(itemData['chapterName'],
style: new TextStyle(color: Theme.of(context).primaryColor));
Column column = new Column( //垂直线性布局
crossAxisAlignment: CrossAxisAlignment.start, //子控件左对齐
children: <Widget>[
new Padding(
padding: EdgeInsets.all(10.0),
child: author,
),
new Padding(
padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0),
child: title,
),
new Padding(
padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 10.0),
child: chapterName,
),
],
);
return new Card(
///阴影效果
elevation: 4.0,
child: column,
);
}
}
Expanded
可以按比例“扩伸”Row、Column所占用的空间。
const Expanded({
int flex = 1,
@required Widget child,
})
flex为弹性系数,和Android中的`LinearLayout`的`weight`比重效果一致。
class _LayoutWidgetState extends State<LayoutWidget> {
@override
Widget build(BuildContext context) {
return Row(
//将Row 分成 2+3+1分,
children: <Widget>[
Expanded(flex:2,child: Container(child: Text('1'), color: Colors.red)),
Expanded(flex:3,child: Container(child: Text('1'), color: Colors.blue)),
Expanded(flex:1,child: Container(child: Text('1'), color: Colors.yellow)),
],
);
}
}
网络请求
一个app中,网络请求是最基本的功能,我们需要使用网络请求数据用于显示或者进行不同的逻辑处理。在我们的案例中,同样需要请求文章数据与banner数据。在Dart SDK中的io库其实提供了`HttpClient` 进行网络请求。大家都知道,Java中也提供了HttpConnection,但是我们更喜欢使用更加方便的OkHttp,所以一般开发中,我们可能使用一些更加方便的网络库,比如**http、dio**等等。进入https://pub.dartlang.org/ 输入库名就能够搜索到相关的库。这次我们使用dio来完成网络的请求:
class HttpManager {
Dio _dio;
static HttpManager _instance;
factory HttpManager.getInstance() {
if (null == _instance) {
_instance = new HttpManager._internal();
}
return _instance;
}
//以 _ 开头的函数、变量无法在库外使用
HttpManager._internal() {
///基础配置
BaseOptions options = new BaseOptions(
baseUrl: Api.baseUrl, //基础地址
connectTimeout: 5000, //连接服务器超时时间,单位是毫秒
receiveTimeout: 3000, //读取超时
);
_dio = new Dio(options);
}
request(url, {String method = "get"}) async {
try {
///默认使用get请求
Options option = new Options(method: method);
Response response = await _dio.request(url, options: option);
///一般来说,提供的是json字符串,response.data得到的就是这个json对应的map
return response.data;
} catch (e) {
return null;
}
}
}
导入库
在Flutter工程中存在一个`pubspec.yaml`文件。此文件类似`build.gradle`,在这个文件中进行我们整个工程的一些配置,其中就包括了库的导入。配置完成之后,点击右上角的`Packages get`就能自动下载依赖。
库的最新版本可以进入https://pub.dartlang.org/ 搜索。
添加文章详情——路由与导航
路由管理
路由(Route)在移动开发中通常指页面(Page),在Android中通常指一个Activity。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//导航到新路由
Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
命名路由
命名路由(Named Route)即给路由起一个名字,然后可以通过路由名字直接打开新的路由。这为路由管理带来了一种直观、简单的方式。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
//home: MainRoute(),
//注册路由表
routes: {
/// '/'是特殊地址,第一个页面
"/" :(context) => MainRoute(),
"new_page": (context) => SecondRoute(),
},
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.pushNamed(context, "new_page");
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
命名路由的最大优点是直观,我们可以通过语义化的字符串来管理路由。但其有一个明显的缺点:不能直接传递路由参数。假设SecondRoute,需要接受一个字符串参数`tip`,然后再在屏幕中心将`tip`的内容显示出来。因为命名路由需要提前注册到路由表中,所以就无法动态修改`tip`参数。
自定义路由切换动画
Material库中提供了MaterialPageRoute,它在Android上会上下滑动切换。如果想自定义路由切换动画,可以使用PageRouteBuilder。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///平移
return SlideTransition(
///Tween:在补间动画中,定义开始点结束点
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: SecondRoute(),
);
},
),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
同时我们也可以对动画进行组合
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///透明渐变与旋转
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.5, end: 1.0)
.animate(animation),
child: SecondRoute(),
),
);
},),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
注意点
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///Navigator.push内部其实就是 Navigator.of(context).push
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
这段代码运行会出现错误:!路由异常
问题关键点在于**Navigator operation requested with a context that does not include a Navigator.**(导航操作请求使用了不包含Navigator的上下文context)
`Navigator`实际上也是一个Widget,这个异常出现在`Navigator.of(context)`路由器的获取上,而这句代码会**从当前的context的父级一层层向上去查找一个`Navigator`**,我们当前传递的context就是MyApp,它的父级是root——UI根节点。`Navigator`这个widget的并不是由root创建的,因此在root下一级的上下文中无法获得`Navigator`。
> 在之前所有的路由案例中,我们的上下文是MainRoute,它的父级是MaterialApp。MaterialApp内部就会创建一个Navigator。 > > MaterialApp->\_MaterialAppState->WidgetsApp->\_WidgetsAppState > > 
所以问题就在于,`Navigator`需要通过MaterialApp或者它孩子的上下文。
解决一
按照此笔记最开始的正常路由演示案例来进行修改。
解决二
使用`Builder`
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
///
Builder(builder: (context){
return RaisedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
);
})
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
使用Builder嵌套,Builder的参数可以看成一个回调,接收自身的context并返回布局配置。现在路由是从Builder的父亲开始查找啦,自然能找到Navigator。

解决三
使用`navigatorKey`
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return new MaterialApp(
///指定路由器widget的key
navigatorKey: navigatorKey,
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///输出Navigator
debugPrint(navigatorKey.currentWidget.runtimeType.toString());
navigatorKey.currentState.push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}

在创建`Navigator`的时候,会给一个**key**,这个key可以看成一个Widget的id。这里的**_navigator**就是我们指定的**navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建`Navigator`了)。而通过这个key,就能够获得这个`Navigator`。直接获得了路由自然不需要再去查找了!
完善第一个页面
之前我们编写了第一个页面显示了banner与文章列表,一般的,在点击banner与文章条目时需要进入新界面显示文章详情,而wanandroid的文章详情是一个web页面,因此我们需要增加一个新界面用于展示web
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class WebViewPage extends StatefulWidget {
final data;
WebViewPage(this.data);
@override
_WebViewPageState createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
bool isLoad = true;
FlutterWebviewPlugin flutterWebViewPlugin;
@override
void initState() {
super.initState();
flutterWebViewPlugin = new FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((state) {
if (state.type == WebViewState.finishLoad) {
// 加载完成
setState(() {
isLoad = false;
});
} else if (state.type == WebViewState.startLoad) {
setState(() {
isLoad = true;
});
}
});
}
@override
void dispose() {
flutterWebViewPlugin.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
///WebView插件
return WebviewScaffold(
appBar: AppBar(
title: Text(widget.data['title']),
///appbar下边摆放一个进度条
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: const LinearProgressIndicator()),
///透明度
bottomOpacity: isLoad ? 1.0 : 0.0,
),
withLocalStorage: true, //缓存,数据存储
url: widget.data['url'],
withJavascript: true);
}
}
> flutter_webview_plugin是一个插件,需要引入:flutter_webview_plugin: ^0.3.0+2
首先我们加入banner的点击事件,在`page_article.dart`中修改`_bannerView`方法:
Widget _bannerView() {
var list = banners.map((item) {
///自动增加点击水波纹的widget
return InkWell(
child: Image.network(item['imagePath'], fit: BoxFit.cover), //fit 图片充满容器
///点击事件
onTap: () {
///跳转页面
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
return WebViewPage(item);
}));
},
);
}).toList();
return list.isNotEmpty
? BannerView(
list,
intervalDuration: const Duration(seconds: 3),
)
: null;
}
然后还有文章条目的点击,在`article_item.dart`文件中修改`build`方法最后return的内容
return Card(
///阴影效果
elevation: 4.0,
child: InkWell(
child: column,
onTap: () {
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
///因为webview获取的是map的url地址,而文章数据中文章详情的key是link
itemData['url'] = itemData['link'];
return WebViewPage(itemData);
}));
},
),
);
动画与打包
动画
Flutter中的动画系统基于`Animation`对象的,和之前的手势不同,它不是一个Widget,这是因为`Animation`对象本身和UI渲染没有任何关系。Animation是一个抽象类,就相当于一个定时器,它用于保存动画的插值和状态,并执行数值的变化。widget可以在`build`函数中读取`Animation`对象的当前值, 并且可以监听动画的状态改变。
AnimationController
AnimationController用于控制动画,它包含动画的启动`forward()`、停止`stop()` 、反向播放 `reverse()`等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000), //动画时间
lowerBound: 10.0, //生成数字的区间
upperBound: 20.0, //10.0 - 20.0
vsync: this //TickerProvider 动画驱动器提供者
);
Ticker
Ticker的作用是添加屏幕刷新回调,每次屏幕刷新都会调用`TickerCallback`。使用Ticker来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源。因为Flutter中屏幕刷新时会通知Ticker,锁屏后屏幕会停止刷新,所以Ticker就不会再触发。最简单的做法为将`SingleTickerProviderStateMixin`添加到State的定义中。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 100.0,
// 提供 vsync 最简单的方式,就是直接混入 SingleTickerProviderStateMixin
// 如果有多个AnimationController,则使用TickerProviderStateMixin。
vsync: this,
);
//状态修改监听
controller
..addStatusListener((AnimationStatus status) {
debugPrint("状态:$status");
})
..addListener(() {
setState(() => {});
});
debugPrint("controller.value:${controller.value}");
}
@override
Widget build(BuildContext context