Flutter之三基础使用
一. 命令式UI和声明式UI
1. 命令式编程和声明式编程的区别
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
2. Flutter中命令式编程的应用
在Flutter中每个组件,会有个build函数,这里会返回一个能够完整描述UI的对象结构。每当数据改变时,就重新调用build函数,返回新的结构。
如何高效渲染,就是框架去做的事情了。通过这种方式,不管是UI的初始布局结构,还是后面的修改,都是build函数返回的对象结构去声明的,完整的声明式UI由此而来。
所以Flutter是构建新的widget实例,而不是改变旧的实例。
二. Flutter中的Widget
Flutter 中Widget 是一切的基础,一切的显示都是 Widget,利用响应式模式进行渲染。在 Flutter 中自定义组件就是一个类,这个类需要继承 StatelessWidget\StatefulWidget。
Widget 分为 有状态(StatefulWidget) 和 无状态(StatelessWidget) 两种,在 Flutter 中每个页面都是一帧,无状态就是保持在那一帧,而有状态的 Widget 当数据更新时,其实是创建了新的 Widget,
只是 State 实现了跨帧的数据同步保存。
1. 无状态StatelessWidget
(1.)定义
如果一个控件的UI页是静态的,也就是一旦这些UI页被成功渲染之后就不需要也不可能去改变他的状态,例如纯展示页面。就使用StatelessWidget。在需要实现一个StatelessWidget组件的时候,
声明一个class类extends继承StatelessWidget,必须要重写 build 方法,这个 build 方法会携带一个 BuildContext 参数。另外 build 方法返回一个 Widget 值,也就是我们自定义的无状态的布局。
这样就可以创建一个无状态的Widget。StatelessWidget 的 构造方法 和 build 方法之会创建一次,不会随着子节点StatefulWidget 控件的状态改变而重构布局。所以它适合放在布局嵌入比较深的布局节点,
又因为StatelessWidget是静态的,所以性能比较好,建议多使用。
注意: 如果无状态Widget里面有子Widget,并且子Widget是有状态的,则子Widget的内容是可以通过setState来更改的。无状态Widget影响的仅仅是自己是无状态的,不会影响他的父Widget和子Widget。
(2.)使用
flutter系统中提供了许多的已经定义好的StatelessWidget,例如StatelessWidget:StatelessWidget、 Icon、 IconButton、Text等。
1 | class CircleAvatar extends StatelessWidget {} |
Widget 和 Widget 之间通过 child: 进行嵌套。其中有的 Widget 只能有一个 child;有的 Widget 可以多个 child ,也就是children,比如` Column 布局。
StatelessWidget 是不能调用setState函数的。
例如:
1 | class HomePage extends StatelessWidget { |
2.有状态StatefulWidget
(1.)定义
StatefulWidget是可变状态的widget。StatefulWidget依赖的数据在Widget生命周期中可能会频繁的发生变化。当使用StatefulWidget依赖的数据发生变化时调用setState函数时,
会通知Flutter框架某个状态发生了变化,Flutter会重新运行build方法,应用程序变可以显示最新的状态,Widget只是视图的“配置信息”,是数据的映射。
(2.)使用
flutter系统中提供了许多的已经定义好的StatefulWidget,例如Checkbox, Radio, Slider, InkWell, Form, 和 TextField 都是有状态的widget,也是StatefulWidget的子类。
1 | class Checkbox extends StatefulWidget {} |
自定义 StatefullWidget :
1 | class HomePage extends StatefulWidget { |
(3.)State概念
每一个 StatefulWidget 类都会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态,保存的状态信息可以在 build 时被获取,
同时,在 widget 生命周期中可以被改变,改变发生时,可以调用其 setState() 方法通知 framework 发生改变,framework 会重新调用 build 方法重构 widget 树,最终完成更新 UI 的目的。
state 中包含两个常用属性:widget 和 context。widget 属性表示当前正在关联的 widget 实例,但关联关系可能会在 widget 重构时发生变化(framework 会动态设置 widget 属性为最新的widget 对象)。context 属性是 buildContext 类的实例,表示构建 widget 的上下文,每个 widget 都有一个自己的 context 对象,
它包含了查找、遍历当前 widget 树的方法。
(3.)State的生命周期
如下代码:
1 | class TestStateWidget extends StatefulWidget { |
State 中主要的声明周期有 :
- initState:初始化,理论上只有初始化一次。
- didChangeDependencies:在 initState 之后调用,此时可以获取其他 State 。
- dispose:销毁,只会调用一次。
3.Flutter自定义Widget
上文也讲到了在Flutter开发中,可以继承自StatelessWidget或者StatefulWidget来创建自己的Widget类;
- StatelessWidget: 没有状态改变的Widget,通常这种Widget仅仅是做一些展示工作而已;
- StatefulWidget: 需要保存状态,并且可能出现状态改变的Widget;
build方法什么情况下被执行呢:
- 当我们的StatelessWidget第一次被插入到Widget树中时(也就是第一次被创建时);
- 当我们的父Widget(parent widget)发生改变时,子Widget会被重新构建;
- 如果我们的Widget依赖InheritedWidget的一些数据,InheritedWidget数据发生改变时;
三. StatefulWidget的状态管理
1.widget的状态
我们知道在Flutter 内一切皆 Widge。runApp函数接受给定的Widget并使其成为widget树的根。
Widget描述了他们的视图在给定其当前配置和状态时应该看起来像什么。widget 的主要工作是通过实现 build 函数 来构建自身。
当widget调用build 函数时就会绘制出一帧静止的画面.当需要widget进行改变的时候就要不断地调用widget的build 函数进行绘制.
我们将widget对应的每一帧画面称作为一个状态.当widget的状态发生变化时,widget会重新构建UI,Flutter会对比前后变化的不同以确定底层渲染树从一个状态转换到下一个状态所需的最小更改.
注意:通常我们所说的StatefulWidget是有状态的组件,意思不是说StatefulWidget类本身是可变的,实际上StatefulWidget类本身也是不变的,StatefulWidget持有的state状态
是在该对应widget整个生命周期内一直存在的,
也是因为有了这个state状态,我们就可以通知Flutter框架某一个状态发生了变化,Flutter会重新运行build方法来重新绘制界面。这里的有状态指的是创建时需要指定一个 State ,
在需要更新 UI时调用 setState(VoidCallbackfn),并在 VoidCallback 中改变一些些变量数值等,组件会重新 build 以达到数显状态/UI的效果。
2.widget的状态改变的实现
来看一下Widget的源码:
1 |
|
@immutable实际上是一个注解 被@immutable注解标明的类或者子类都必须是不可变的也就是说定义到Widget中的数据一定是不可变的,需要使用final来修饰.
StatelessWidget是没有状态得因此它里面的数据通常是直接定义完后就不修改的。StatefulWidget需要有状态(可以理解成变量)的改变,既然我们在上面源码里面分析了Widget是不可变,
那么StatefulWidget如何来存储可变的状态呢?Flutter是靠将StatefulWidget设计成了两个类来实现状态的变化的:
- 一个类继承自StatefulWidget,作为Widget树的一部分;
- 一个类继承自State,用于记录StatefulWidget会变化的状态,并且根据状态的变化,构建出新的Widget;
这样设计的原因是因为在Flutter中,只要数据改变了Widget就需要重新构建(rebuild)
3.widget的状态管理的方式
状态管理就是一些能够引发界面状态改变的变量进行管理.这些变量需要在多个组件或者是路由界面中使用,所以就有了状态管理。
目前状态管理的方式有三种:
- Widget管理自己的状态。
- 父Widget管理子Widget状态。
- 混合管理(父Widget和子Widget都管理状态)。
如果某些状态只需要在自己的Widget中使用即可,Widget树中的其它部分并不需要访问这个状态.那么我们可以通过widget 管理自己的 state.比如选择框的选中状态.
但是如果某些状态需要在多个部分进行共享,那我们只有通过父 widget 管理子 widget 状态或者混合管理来实现了.比如用户的登录状态信息.
一般的原则是:如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理.
4. 跨widget跨页面状态管理
上面说到跨组件状态管理我们一般是通过父Widget管理子Widget状态来实现.例如A组件嵌套B,C两个兄弟组件,B组件有一个B1组件,C组件有一个C1组件.如果B1和C1组件需要共享一个状态,
那实现起来就非常麻烦,我们需要这个状态放到A组件中再通过B组件和C组件将整个状态传递到B1组件和C1组件组件中去.
通过这种方式我们可以发现B,C两个组件本身并不需要这个共享的状态,但是他们作为中转组件也必须要持有这个状态,如果随着层架的增加这种情况还会更加严重.我们如果在不同的Widget之间将状态传递来、传递去,
那么是无穷尽的,并且代码的耦合度会变得非常高,牵一发而动全身,无论是代码编写质量、后期维护、可扩展性都非常差。并且有的状态是需要跨页面共享的,例如登录状态.传递起来就更麻烦了.
这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
- 实现一个全局的事件总线EventBus,需要这个状态的组件可以通过订阅这个状态的通知,在收到通知后调用setState(…)方法重新build一下自身即可。
- 使用一些专门用于状态管理的包,如Provider、GetX、Redux等专门管理状态的工具包进行管理.(后续我会讲解这些包的使用)
四. Scaffold(脚手架页面)
在 Flutter 中,“脚手架页面(Scaffold Page)” 是指使用 Scaffold 组件构建的页面结构。
它是 Material Design 的核心布局容器,用于快速搭建常见的 App 页面框架,比如带有 AppBar、Drawer、BottomNavigationBar、FloatingActionButton 等组件的页面。
Scaffold(脚手架页面)详解
一、基本作用
Scaffold 是一个提供基础页面结构的容器组件,帮助开发者快速搭建应用的整体 UI 框架。
它内部包含了:
- 顶部导航栏(
AppBar) - 主体内容区(
body) - 底部导航栏(
BottomNavigationBar) - 悬浮按钮(
FloatingActionButton) - 抽屉菜单(
Drawer/EndDrawer) - 底部浮动区域(
bottomSheet) - SnackBar、BottomSheet 的显示逻辑等。
二、常用属性与功能说明
| 属性名 | 作用 | 示例 |
|---|---|---|
appBar |
顶部导航栏,通常放标题、操作按钮等 | AppBar(title: Text('主页')) |
body |
页面主体内容 | ListView()/Column()等 |
floatingActionButton |
悬浮操作按钮 | FloatingActionButton(onPressed: () {}) |
floatingActionButtonLocation |
悬浮按钮位置 | FloatingActionButtonLocation.centerDocked |
bottomNavigationBar |
底部导航栏 | BottomNavigationBar(items: [...]) |
bottomSheet |
固定在底部的视图 | Container(height: 50, color: Colors.blue) |
drawer |
左侧抽屉菜单 | Drawer(child: ListView(...)) |
endDrawer |
右侧抽屉菜单 | 同上 |
backgroundColor |
页面背景色 | Colors.white |
extendBodyBehindAppBar |
是否让 body 内容延伸到 AppBar 后面 | true/false |
三、使用示例
基础 Scaffold 页面
1 | import 'package:flutter/material.dart'; |
带底部导航和抽屉的页面
1 | import 'package:flutter/material.dart'; |
四、Scaffold 的特点总结
| 特点 | 说明 |
|---|---|
| 组件集成 | 内置 AppBar、Drawer、FAB、BottomNavigationBar 等 |
| 布局规范 | 遵循 Material Design 标准 |
| 智能管理 | 自动处理 Snackbar、BottomSheet 等的显示逻辑 |
| 使用方便 | 快速搭建标准页面结构 |
| 扩展灵活 | 可结合 SliverAppBar、NestedScrollView 等创建复杂页面 |
五、典型使用场景
| 场景 | 示例 |
|---|---|
| 普通页面 | AppBar + Body |
| 多页面导航 | Scaffold + BottomNavigationBar |
| 带抽屉菜单 | Scaffold + Drawer |
| 内容+悬浮按钮操作 | Scaffold + FloatingActionButton |
| 混合滚动页面 | Scaffold + NestedScrollView / SliverAppBar |
五. 常见的容器Widget
Flutter 布局容器可以按功能分为 线性布局、层叠布局、网格布局、弹性布局、约束布局、对齐与辅助布局 六大类。
线性布局(Row / Column / Flex)
常用控件
Row:水平排列子 WidgetColumn:垂直排列子 WidgetFlex:可指定方向(horizontal/vertical)Expanded:占据父控件剩余空间Flexible:弹性占据空间,可设fit为 tight 或 loose
特点
- 一维布局(水平或垂直)
- 支持主轴(MainAxisAlignment)和交叉轴(CrossAxisAlignment)对齐
- 可配合 Expanded/Flexible 实现响应式布局
使用场景
- 按行或列排列控件
- 导航栏、按钮组、表单行布局
- 剩余空间分配,如列表项宽度自适应
层叠布局(Stack / Positioned / IndexedStack)
常用控件
Stack:子 Widget 可重叠Positioned:在 Stack 中精确定位IndexedStack:只显示指定索引的子 Widget,但保留其他 Widget 状态
特点
- 二维布局(允许重叠)
- 可精确控制子控件位置
- IndexedStack 保持子 Widget 状态不被销毁
使用场景
- 悬浮按钮、徽章、叠加图片
- Tab 页面切换需要保留状态
- 弹窗、遮罩层布局
网格与流式布局(GridView / Wrap / Flow / Table)
常用控件
GridView:滚动网格布局GridTile:GridView 子项Wrap:自动换行排列Flow:自定义高性能流式布局Table:表格布局
特点
- 二维布局(网格或流式)
- Wrap 超出主轴自动换行
- Flow 可自定义布局规则,性能优于 Wrap
- Table 可指定列宽和对齐方式
使用场景
- 图片墙、标签云、卡片列表
- 表格数据展示
- 多按钮流式排列
弹性布局(Expanded / Flexible / Spacer)
常用控件
Expanded:占据剩余空间Flexible:弹性空间,可按比例占据Spacer:占据空白空间,辅助分隔
特点
- 用于剩余空间的动态分配
- 可以按比例占用空间,实现响应式布局
使用场景
- Row / Column 中控件自动拉伸
- 平分剩余空间
- 控件间隔自适应布局
约束布局(SizedBox / ConstrainedBox / AspectRatio / FractionallySizedBox)
常用控件
SizedBox:固定宽高,或作为间距ConstrainedBox:最小/最大宽高约束LimitedBox:仅在无限约束下生效FractionallySizedBox:按父容器比例设置宽高AspectRatio:固定宽高比IntrinsicWidth/Height:根据子 Widget 内在尺寸计算宽高
特点
- 精确控制宽高
- 可做响应式布局
- AspectRatio 保持宽高比例
使用场景
- 图片、视频播放器固定比例显示
- 控件尺寸受父布局限制
- 间距占位和自适应布局
对齐与辅助布局(Align / Center / Padding / Visibility / Clip)
常用控件
Padding:增加内边距Align/Center:对齐布局Baseline:按文字基线对齐Visibility/Offstage:控制显示/隐藏ClipRect/ClipRRect/ClipOval:裁剪布局
特点
- 对齐、间距、显示控制
- 可实现圆角、圆形、遮罩效果
使用场景
- 控件居中、对齐
- 控件间距
- 隐藏控件或实现裁剪效果
总结表格
| 布局类型 | 常用 Widget | 特点 | 使用场景 |
|---|---|---|---|
| 线性布局 | Row / Column / Flex / Expanded / Flexible | 一维布局,主轴/交叉轴对齐,可弹性分配空间 | 导航栏、表单行布局、按钮组 |
| 层叠布局 | Stack / Positioned / IndexedStack | 二维叠加,可精确定位 | 悬浮按钮、Tab页面、弹窗、遮罩 |
| 网格布局 | GridView / Wrap / Flow / Table | 二维排列或流式布局,支持换行 | 图片墙、标签云、表格、卡片列表 |
| 弹性布局 | Expanded / Flexible / Spacer | 剩余空间动态分配,按比例占用 | 自动拉伸、控件间隔、响应式布局 |
| 约束布局 | SizedBox / ConstrainedBox / AspectRatio / FractionallySizedBox | 控件尺寸约束,固定或比例 | 图片、视频、间距占位、自适应布局 |
| 对齐辅助 | Align / Center / Padding / Visibility / Clip | 对齐、间距、隐藏、裁剪 | 控件居中、间距、圆角裁剪、隐藏显示 |
💡 ‘总结思路‘:
- 一维布局 → Row/Column/Flex
- 二维叠加’ → Stack / Positioned
- 二维网格/流式 → GridView / Wrap / Flow / Table
- 弹性空间 → Expanded / Flexible / Spacer
- 尺寸约束 → SizedBox / ConstrainedBox / AspectRatio
- 对齐辅助 → Align / Padding / Visibility / Clip
六. 可滚动Widget
在 Flutter 中,滚动布局(Scrollable Widgets) 是构建可滑动页面、长列表、分页加载等界面的核心。
它们的特点是可以让内容在视口(屏幕可见区域)内 ‘滚动显示‘,以应对内容超出屏幕大小的情况。
下面我给你一个 ‘系统的总结‘,包括常见滚动类组件、它们的特性、使用方式和适用场景 👇
一、单一方向滚动布局
1. ListView
- ‘特性‘:
- 最常用的滚动控件。
- 默认垂直方向,可改为水平。
- 可自动回收(懒加载)、支持 itemBuilder。
- ‘使用方式‘:
1 | ListView.builder( |
- ‘典型场景‘:
- 列表展示、聊天记录、新闻流等。
2. GridView
- ‘特性‘:
- 网格滚动布局。
- 支持固定列数或固定宽度。
- ‘使用方式‘:
1 | GridView.count( |
- ‘典型场景‘:
- 图片墙、九宫格、商品展示页。
3. SingleChildScrollView
- ‘特性‘:
- 只能包含一个子 Widget。
- 内容超出时可以滚动。
- 常用于嵌套多种控件(不定高度)。
- ‘使用方式‘:
1 | SingleChildScrollView( |
- ‘典型场景‘:
- 表单页、静态内容较多的详情页。
4. PageView
- ‘特性‘:
- 按页滚动(水平或垂直翻页)。
- 支持
PageController。
- ‘使用方式‘:
1 | PageView( |
- ‘典型场景‘:
- 引导页、Banner、分页内容切换。
二、基于 CustomScrollView 的可组合滚动布局
5. CustomScrollView + Slivers 系列
- ‘特性‘:
- 高度可定制的滚动体系。
- 支持组合多种 “Sliver” 元素(懒加载部分)。
- 常见 Sliver:
SliverListSliverGridSliverAppBarSliverToBoxAdapter
- ‘使用方式‘:
1 | CustomScrollView( |
- ‘典型场景‘:
- 吸顶效果、可伸缩头部、复杂混合滚动页面。
三、支持嵌套或多方向滚动的布局
6. NestedScrollView
- ‘特性‘:
- 实现 头部+内容 的联动滚动(比如 AppBar + TabBar + 内容列表)。
- 支持内部嵌套的可滚动子视图。
- ‘使用方式‘:
1 | NestedScrollView( |
- ‘典型场景‘:
- 复杂吸顶页、滚动联动页面(如微博、知乎个人页)。
7. Scrollable(底层类)
- ‘特性‘:
- 所有滚动组件的基类。
- 通常不直接使用。
- ‘使用方式‘:
- 由上层如
ListView、GridView等封装使用。
- 由上层如
- ‘典型场景‘:
- 自定义滚动行为(高级开发中)。
8. ScrollView(抽象类)
- ‘特性‘:
- 滚动布局基类,
ListView、GridView、PageView等都继承自它。 - 一般不直接实例化。
- 滚动布局基类,
四、特殊滚动控件
9. ReorderableListView
- ‘特性‘:
- 支持 拖拽重新排序 的列表。
- ‘使用方式‘:
1 | ReorderableListView( |
- ‘典型场景‘:
- 可拖拽排序功能,如“我的频道”。
10. Scrollbar
- ‘特性‘:
- 给滚动内容添加可视化滚动条。
- ‘使用方式‘:
1 | Scrollbar( |
- ‘典型场景‘:
- 长列表滚动提示。
11. DraggableScrollableSheet
- ‘特性‘:
- 可拖拽的底部弹出面板。
- 可在最小/最大范围间滑动。
- ‘使用方式‘:
1 | DraggableScrollableSheet( |
- ‘典型场景‘:
- 弹出底部滑动面板(如地图详情面板)。
五、总结对比表
| 名称 | 滚动方向 | 子组件类型 | 特性 | 常见场景 |
|---|---|---|---|---|
| ListView | 垂直/水平 | 多个 | 懒加载,简单易用 | 列表、聊天页 |
| GridView | 垂直/水平 | 多个 | 网格布局 | 图片墙、商品页 |
| SingleChildScrollView | 垂直/水平 | 单个 | 嵌套静态内容 | 表单页、详情页 |
| PageView | 垂直/水平 | 多个 | 翻页切换 | 引导页、Banner |
| CustomScrollView | 任意 | Sliver | 组合滚动 | 吸顶、复杂滚动 |
| NestedScrollView | 垂直 | 多个 | 嵌套滚动 | Tab联动 |
| ReorderableListView | 垂直 | 多个 | 可拖拽排序 | 编辑频道 |
| Scrollbar | 任意 | 任意 | 滚动条可视化 | 长列表提示 |
| DraggableScrollableSheet | 垂直 | 一个 | 可拖动面板 | 地图详情、弹出层 |
附加建议
- 如果是’静态页面‘:推荐使用
SingleChildScrollView + Column - 如果是’长列表 / 动态数据‘:使用
ListView.builder - 如果是’复杂滚动 / 吸顶头部‘:使用
CustomScrollView + Sliver - 如果是’页面切换‘:使用
PageView - 如果是’联动滑动‘:使用
NestedScrollView
七. 常见的UI显示Widget
1. Text
- ‘特性‘:
- 显示文本内容。
- 可设置字体大小、颜色、粗细、行高、溢出方式等。
- ‘使用方式‘:
1 | Text( |
- ‘典型场景‘:
- 显示文字内容、标题、描述。
2. Icon / Image / Image.network / Image.asset
- Icon
- 显示图标,通常与
Icons或自定义 IconFont 配合。
- 显示图标,通常与
1 | Icon(Icons.favorite, color: Colors.red, size: 30) |
- Image
- 显示图片资源:
Image.asset本地资源Image.network网络资源Image.file本地文件
- 显示图片资源:
1 | Image.network('https://example.com/image.png', width: 100, height: 100) |
- ‘典型场景‘:
- 显示图标、图片、头像、装饰图。
3. Button 系列(ElevatedButton / TextButton / IconButton / OutlinedButton)
- ‘特性‘:
- 用于用户交互触发事件。
- 样式多样,可设置颜色、形状、阴影。
- ‘使用方式‘:
1 | ElevatedButton( |
- ‘典型场景‘:
- 提交操作、导航、交互按钮。
4. TextField / TextFormField
- ‘特性‘:
- 输入框 Widget。
- 支持验证、样式、光标、前缀/后缀图标。
- ‘使用方式‘:
1 | TextField( |
- ‘典型场景‘:
- 表单输入、搜索框、聊天输入。
5. Checkbox / Radio / Switch
- ‘特性‘:
- 选择类控件,单选、多选、开关。
- ‘使用方式‘:
1 | Checkbox(value: true, onChanged: (val) {}) |
- ‘典型场景‘:
- 设置开关、选项选择、表单。
6. ProgressIndicator(CircularProgressIndicator / LinearProgressIndicator)
- ‘特性‘:
- 进度显示控件。
- 支持旋转、线性显示、确定/不确定进度。
- ‘使用方式‘:
1 | CircularProgressIndicator() |
- ‘典型场景‘:
- 数据加载、进度展示。
7. Divider / VerticalDivider / Spacer
- Divider
- 水平分割线。
- VerticalDivider
- 垂直分割线。
- Spacer
- 弹性占位,非容器但影响布局。
1 | Divider(color: Colors.grey, thickness: 1) |
- ‘典型场景‘:
- 列表分割、弹性间隔。
8. Tooltip / Chip / Badge
- Tooltip
- 提示信息,当长按或悬停显示文本。
1 | Tooltip(message: 'Edit', child: Icon(Icons.edit)) |
- Chip
- 小型标签、可附加图标或删除按钮。
1 | Chip(label: Text('Flutter')) |
- Badge
- 消息角标、数量标记。
1 | Badge(label: Text('3')) |
- ‘典型场景‘:
- 提示、标签、角标显示。
9. RichText / TextSpan
- ‘特性‘:
- 支持文本多样式、部分文字点击。
- ‘使用方式‘:
1 | RichText( |
- ‘典型场景‘:
- 富文本显示、部分文字高亮或可点击。
10. GestureDetector / InkWell / InkResponse
- ‘特性‘:
- 用于 ‘捕获用户手势‘。
- InkWell 提供水波纹效果,GestureDetector 更灵活。
- ‘使用方式‘:
1 | GestureDetector( |
- ‘典型场景‘:
- 点击、长按、拖动等手势事件。
11. IconButton / FloatingActionButton
- IconButton
- 小图标按钮。
- FloatingActionButton
- 浮动操作按钮,一般放在页面右下角。
1 | FloatingActionButton(onPressed: () {}, child: Icon(Icons.add)) |
- ‘典型场景‘:
- 快速操作、工具栏按钮。
12. Other Display Widgets
- Card / ListTile
- 单独显示内容。
1 | ListTile( |
- CircleAvatar
- 圆形头像。
1 | CircleAvatar(backgroundImage: NetworkImage(url)) |
- Placeholder
- 占位控件,用于布局开发调试。
1 | Placeholder() |
总结
| Widget | 特性 | 使用场景 |
|---|---|---|
| Text | 文本显示 | 标题、描述、文字内容 |
| Icon / Image | 图标/图片显示 | 图标、图片、头像 |
| Button 系列 | 用户交互 | 提交操作、点击事件 |
| TextField | 文本输入 | 表单、搜索、聊天 |
| Checkbox / Radio / Switch | 选择控件 | 表单、设置选项 |
| ProgressIndicator | 进度显示 | 数据加载、进度展示 |
| Divider / Spacer | 分隔/弹性占位 | 列表、布局间隔 |
| Tooltip / Chip / Badge | 提示/标签/角标 | 小组件提示、标签显示 |
| RichText | 富文本 | 高亮、部分样式文本 |
| GestureDetector / InkWell | 手势捕获 | 点击、滑动、长按 |
| IconButton / FloatingActionButton | 图标操作按钮 | 页面快速操作 |
| ListTile / Card / CircleAvatar / Placeholder | 内容显示 | 列表项、头像、占位调试 |
八. widget事件
这里分两种情况,一种是widget 本身支持事件监测,另外一种是widget不支持事件检测.
1.widget 本身支持事件,
如果 widget 本身支持事件监测,直接传递给它一个函数,并在这个函数里实现响应方法。例如,RaisedButton、IconButton、OutlineButton、Checkbox、SnackBar、Switch等。
1 | class SampleApp extends StatelessWidget { |
2. widget 本身不支持事件,
如果 widget 本身不支持事件监测,则在外面包裹一个 GestureDetector(或者支持事件的widget例如: InkWell),并给它的属性传递一个onTap函数:
1 | class SampleApp extends StatelessWidget { |
3. widget 手势事件
| 函数 | 说明 |
|---|---|
| onTapDwon | 当按下屏幕时触发 |
| onTap | 当与屏幕短暂地触碰时触发,最常用 |
| onTapUp | 当用户停止触碰屏幕时触发 |
| onTapCancel | 当用户触摸屏幕,但没有完成Tap事件时触发 |
| onDoubleTap | 快速双击屏幕时触发 |
| onLongPress | 当长按屏幕时触发(与屏幕接触事件必须超过500ms) |
| onPanUpdate | 当在屏幕上移动时触发 |
| onVerticalDragDown | 当手指触碰屏幕且准备往屏幕垂直方向移动时触发 |
| onVerticalDragStart | 当手指触碰屏幕且开始往屏幕垂直方向移动时触发 |
| onVerticalDragUpdate | 当手指触碰屏幕且开始往屏幕垂直方向移动并发生位移时触发 |
| onVerticalDragEnd | 当用户完成垂直方向触摸屏幕时触发 |
| onVerticalDragCancel | 当用户中断了onVerticalDragDown时触发 |
| onHorizontalDragDown | 当手指触摸屏幕且准备往屏幕水平方向移动时触发 |
| onHorizontalDragStart | 当手指触摸屏幕且开始往屏幕水平方向移动时触发 |
| onHorizontalDragUpdate | 当手指触摸屏幕且开始往屏幕水平方向移动并发生位移时触发 |
| onHorizontalDragEnd | 当用户完成水平方向触摸屏幕时触发 |
| onHorizontalDragCancel | 当用户中断了onHorizontalDragDown时触发 |
| onPanDown | 当用户触摸屏幕时触发 |
| onPanStart | 当用户触摸屏幕并开始移动时触发 |
| onPanUpdate | 当用户触摸屏幕并产生移动时触发 |
| onPanEnd | 当用户完成触摸屏幕时触发 |
| onScaleStart | 当用户触摸屏幕并开始缩放时触发 |
| onScaleUpdate | 当用户触摸屏幕并产生缩放时触发 |
| onScaleEnd | 当用户完成缩放时触发 |
九. 路由和导航
Flutter 中万物皆 Widget,页面自然也是一个 Widget。只不过是一个全屏的 Widget。在flutter中两种页面跳转方式:
- 无名路由跳转(一种动态构建路由的方式)。
- 命名路由跳转(一种提前命名路由的方式)。
1.无名路由跳转
直接使用使用 Navigator 跳转页面,在 Flutter 中,使用 Navigator 来进行页面跳转。一个简单的跳转页面的例子:
1 | Navigator.push( |
或者
1 | Navigator.of(context).push( |
在A页面中关闭A页面返回到上一个页面:
1 | Navigator.pop(context); |
或者:
1 | Navigator.of(context).pop(); |
2.命名路由跳转
命名路由跳转需要先注册路由表,放在MaterialApp的 initialRoute 和 routes 中.命名路由路由存在的意义在于可以让我们更方便的导航到想要到达的页面,便于管理和维护。
要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。路由表的注册方式很简单,找到MaterialApp,添加routes属性,
1 | void main() => runApp(MyApp());//单行函数调用写法 |
要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法:
Future pushNamed(BuildContext context, String routeName,{Object arguments})
Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,读者可以自行查看API文档。通过刚刚注册的页面名称来跳转一个页面:
1 | Navigator.pushNamed(context, 'first_page');// one_page表示页面别名 |
3.界面之间传递参数
传递的方式有两种:
- 通过构造方法中传递数据。
- 在Route中传递数据给下一个页面。
(1. )通过构造方法中传递数据
需要在接收数据的页面事先定义好构造方法,构造方法中定义要接收的参数。例如:我们在SecondPage中定义一个构造方法,构造方法中可以定义我们要接收的数据:
1 | class SecondPage extends StatelessWidget { |
Frist页面跳转SecondPage页面时给传递数据:
1 | Navigator.push( |
(2.)将参数传递给指定路由
构造方法参数参数的缺点不是太灵活。Flutter提供了把传递的参数放到Navigator中,然后传递给指定的路由,在接收的页面提取出需要的参数即可,这种方式更加灵活一些。
- 首先要先定义好要传递的数据
例如:
我们先定义一个实体类:1
2
3
4
5class People {
String name;
int age;
People(this.name, this.age);
} - 传递参数
将参数数据传递给SecondPage,可以有如下四种传参方式,效果都一样
第一种:第二种:1
2
3
4
5Navigator.pushNamed(
context,
second_page,
arguments: People("张三", 30),//要传递的数据
);第三种:1
Navigator.of(context).pushNamed(second_page, arguments: People("张三", 30));
第四种1
2
3
4
5
6
7
8Navigator.push(context,
MaterialPageRoute(
builder: (context) => SecondPage(),
settings: RouteSettings(
arguments: People("张三", 30),
),
),
);1
2
3
4
5
6
7
8Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPage(),
settings: RouteSettings(
arguments: People("张三", 30),
)
),
); - 接收参数
在SecondPage接收数据时,数据要通过 ModalRoute.of 方法。此方法返回带有参数的当前路由。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class SecondPage extends StatelessWidget {
Widget build(BuildContext context) {
/*获取传递过来的参数*/
People _people = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text("SecondPage"),
),
body: Center(
child: Text("name:${_people.name},age:${_people.age}"),
),
);
}
}
4.返回参数
路由打开页面后可以通过await 关键字等待路由返回参数.
1 | class FirstPage extends StatelessWidget { |
通过Navigator.pop返回数据
1 | class SecondPage extends StatelessWidget { |
5. 路由拦截
我们开发App的时候,有的界面无需要登录就可以查看例如新闻,视频等功能,但是有的界面如我的,收藏列表等页面需要登录才能查看.为了实现上诉功能通常的做法是
在打开每一个路由页前判断用户登录状态,但是每次打开路由前我们都需要去判断一下将会非常麻烦,可以用Flutter提供路由拦截来解决这种问题.MaterialApp有一个onGenerateRoute属性,
当调用Navigator.pushNamed(…)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中生成路由组件;
如果路由表中没有注册,才会调用onGenerateRoute来生成路由。要实现制页面权限的功能就非常容易:在onGenerateRoute中进行统一的权限控制判断,如:
1 | MaterialApp( |
onGenerateRoute只会对命名路由生效。
5.路由使用总结
建议最好统一使用命名路由的管理方式好处有:
- 语义化更明确。
- 代码更好维护;如果使用匿名路由,则必须在调用Navigator.push的地方创建新路由页,这样不仅需要import新路由页的dart文件,而且这样的代码将会非常分散。
- 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑。
十. 资源管理
Flutter APP安装包中会包含代码和 assets(资源)两部分。Assets是会打包到程序安装包中的,可在运行时进行访问。常见类型的assets包括:
- 图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)
- 字体
- Json文件
- 静态数据(视频,声音)
1.加载图片
类似于Android原生开发,Flutter也可以为当前设备加载适合其分辨率的图像。
| dpi范围 | 密度 |
|---|---|
| 0dpi ~ 120dpi | ldpi |
| 120dpi ~ 160dpi | mdpi |
| 160dpi ~ 240dpi | hdpi |
| 240dpi ~ 320dpi | xhdpi |
| 320dpi ~ 480dpi | xxhdpi |
| 480dpi ~ 640dpi | xxxhdpi |
(1.)声明分辨率相关的图片 assets
pubspec.yaml中asset添加不同设备像素比例的图片。
1 | …/my_icon.png |
在设备像素比率为1.8的设备上,…/2.0x/my_icon.png 将被选择。对于2.7的设备像素比率,…/3.0x/my_icon.png将被选择。
如果没有在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。
也就是说,如果是…/my_icon.png是72px乘72px,如果是…/3.0x/my_icon.png应该是216px乘216px;
pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。
2. 特定平台 assets
上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用,如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets。
(1.)设置APP启动图标
Android
在Flutter项目的根目录中,导航到…/android/app/src/main/res目录,里面包含了各种资源文件夹(如mipmap-hdpi已包含占位符图像“ic_launcher.png”)。
只需按照Android开发人员指南 (opens new window)中的说明, 将其替换为所需的资源,并遵守每种屏幕密度(dpi)的建议图标大小标准。
注意: 如果您重命名.png文件,则还必须在您AndroidManifest.xml的
标签的android:icon属性中更新名称。
iOS
在Flutter项目的根目录中,导航到…/ios/Runner。该目录中Assets.xcassets/AppIcon.appiconset已经包含占位符图片, 只需将它们替换为适当大小的图片,保留原始文件名称。
(2.)启动页
Android
要将启动屏幕(splash screen)添加到您的Flutter应用程序, 请导航至…/android/app/src/main。在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面
(你也可以直接换一张图片)。
iOS
要将图片添加到启动屏幕(splash screen)的中心,请导航至…/ios/Runner。在Assets.xcassets/LaunchImage.imageset, 拖入图片,
并命名为LaunchImage.png、LaunchImage@2x.png、LaunchImage@3x.png。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。
您也可以通过打开Xcode完全自定义storyboard。在Project Navigator中导航到Runner/Runner然后通过打开Assets.xcassets拖入图片,或者通过在LaunchScreen.storyboard中使用Interface Builder进行自定义。
十一.包管理与第三方库引入
1. YAML包管理
Flutter使用配置文件pubspec.yaml(位于项目根目录)来管理第三方依赖包。
pubspec.yaml:
1 | name: flutter_in_action |
下面,我们逐一解释一下各个字段的意义:
- name:应用或包名称。
- description: 应用或包的描述、简介。
- version:应用或包的版本号。
- dependencies:应用或包依赖的其它包或插件。
- dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
- flutter:flutter相关的配置选项。
如果我们的Flutter应用本身依赖某个包,我们需要将所依赖的包添加到dependencies 下.需要注意dependencies和dev_dependencies的区别,前者的依赖包将作为APP的源码的一部分参与编译,生成最终的安装包。
而后者的依赖包只是作为开发阶段的一些工具包,主要是用于帮助我们提高开发、测试效率,比如flutter的自动化测试包等。
2.依赖方式
有三种依赖方式:
1. 依赖Pub仓库
Pub是Google官方的Dart Packages仓库,类似于node中的npm仓库,android中的jcenter。我们可以在Pub上面查找我们需要的包和插件,也可以向Pub发布我们的包和插件。
我们也可以在控制台,定位到当前工程目录,然后手动运行flutter packages get 命令来下载依赖包。
3. 依赖本地包
如果我们正在本地开发一个包,包名为pkgOne,我们可以通过下面方式依赖:
1 | dependencies: |
路径可以是相对的,也可以是绝对的。
3依赖git仓库
依赖Git:你也可以依赖存储在Git仓库中的包。如果软件包位于仓库的根目录中,请使用以下语法
1 | dependencies: |
上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:
1 | dependencies: |
十二.Json解析
由于Flutter禁用运行时反射,所以在Flutter中是没有GSON,Jackson这类解析JSON的库。
- 方案一:手写实体类
- 方案二:json_ serializable库生成实体类
- 方案三:json-to-dart插件自动生成实体类
1. 手动序列化JSON
手动解析通常应用在一些基本简单的场合,即数据结构不是很复杂的场景,手动解析JSON是指使用Flutter提供的dart:convert中内置的JSON解码器。
它能够将原始JSON字符串传递给json.decode() 方法,该方法可以根据JSON字符串具体内容将其转为List或Map,然后在返回的Map<String, dynamic>或者List中查找所需的值。
它不需要依赖任何第三方库,对于小项目来说很方便。
例如:
1 | Map<String, dynamic> person = JSON.decode(jsonStr); |
JSON.decode()返回一个Map<String, dynamic>,这意味着我们直到运行时才知道值的类型。失去了静态类型语言特性,代码非常容易出错。非常不推荐。
可以通过引入Model在模型类中序列化JSON来解决上述问题.
1 | class Person { |
使用时
1 | Map personMap = JSON.decode(jsonStr); |
通过Model调用代码可以具有类型安全、自动补全字段以及编译时异常等静态类型语言特性。如果拼写或者类型错误就不会通过编译,而不是在运行时崩溃。
不过在实际项目中JSON对象很少会这么简单,各种List和Map嵌套的JSON也是很常见的。如果每一个属性都通过手写来实现无疑是非常麻烦的。因此我们一般是不会手写的.
2.通过使用 json_serializable生成JsonModel
json_serializable是Google提供的一个自动化的源代码生成器,可以为我们生成JSON序列化模板。这种方案易维护,由于序列化数据代码不再需要手动编写或者维护,可以将序列化 JSON 数据在运行时的异常风险降到最低;
(1.)引入依赖
需要用到以下三个依赖包,通过代码自动生成的方式,生成模型。
- json_annotation
- json_serializable
- build_runner
在pubspec.yaml中添加依赖并执行flutter pub get:1
2
3
4
5
6dependencies:
json_annotation: ^x.x.x
dev_dependencies:
build_runner: ^1.0.0
json_serializable: x.x.x
(2.)生成模型类
例如Json有如下Json文件
1 | { |
编写生成模型类:
1 | import 'package:json_annotation/json_annotation.dart'; |
- 初次创建 Person.dart 的时候,需要加入 part ‘Person.g.dart’;
- 在需要转换的实体 dart 类 前加入 @JsonSerializable(nullable: false) 注解,标识需要 json序列化处理
- fromJson()、toJson() 方法的写法是固定模式,按模板修改即可
- Person.g.dart 和 文件名 需要保持一致,否则执行以下命令无效
因为实体类的生成代码还不存在,所以上代码会提示一-些错误是正常现象
当然如果我们不想手动编写这个生成类,也可以通过一些工具进行.json2dart 就是这样的一个工具.
工具使用很简单直接粘贴生成对应的类名称,此时我们将生成的代码copy出来创建一个文件在自己的工程中,或者直接下载文件放入工程中即可.
(3.)执行命令根据模型类,生成模型类代码.
一次性生成:
1 | flutter packages pub run build_runner build |
持续生成:
1 | flutter packages pub run build_runner watch。 |
(4.)使用
1 | Map person = JSON.decode(json); |
2.通过在线或者开发工具json-to-dart插件生成
Json2Dart在线插件:
JSON to Dart
JSON To Model
quicktype
或者在Android Studio里面装一个Dart2Json插件.
| 方案 | 特点 | 适合场景 |
|---|---|---|
| 手写实体类 | 耗时 | 小型项目且json不复杂 |
| json_serializable | 需要定义字段、易维护 | 中大型项目 |
| json-to-dart插件 | 快速、易操作 | 任何类型的项目 |
十三.网络请求
flutter网络请求三种方式:
- flutter自带的HttpClient
- 第三方库http
- 第三方库Dio
1. 原生方式(不建议使用)
1.1 get 请求
1 | void getNetData() async { |
2.库http(不建议使用)
(1.)get 请求
代码如下:
1 | void getNet() async { |
代码量比原生的简洁很多,然而还可以更简洁
1 | void getNet() { |
(2.)post 请求
1 | void postNet() async { |
3. 库 dio(推荐使用)
官方提供的HttpClient和http都可以正常的发送网络请求,但是对于现代的应用程序开发来说,通常要求的东西会更多:比如拦截器、取消请求、文件上传/下载、超时设置等等;可以使用一个在Flutter中非常流行的三方库:dio;
dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等…
pubspec.yaml 添加依赖:
1 | dependencies: |
(1.)get 请求
1 | void getNet() async { |
(2.)post 请求
1 | void postNet() async { |
十四.弹窗Dialog
Flutter中的操作提示主要有 SnackBar、BottomSheet、Dialog
Flutter中也提供了很多Dialog 弹窗,如:AboutDialog、AlertDialog、SimpleDialog、CupertinoAlertDialog、CupertinoFullscreenDialogTransition、BottomSheet。
对话框本质上是属于一个路由的页面Route,由Navigator进行管理,所以控制对话框的显示和隐藏,也是调用Navigator.of(context)的push和pop方法。
在Flutter中,对话框会有两种风格,调用showDialog()方法展示的是material风格的对话框,调用showCupertinoDialog()方法展示的是ios风格的对话框。
而这两个方法其实都会去调用showGeneralDialog()方法,可以从源码中看到最后是利用Navigator.of(context, rootNavigator: true).push()一个页面。
基本要传的参数:context上下文,builder用于创建显示的widget,barrierDismissible可以控制点击对话框以外的区域是否隐藏对话框。
showDialog()方法返回的是一个Future对象,可以通过这个future对象来获取对话框所传递的数据。 比如我们想知道想知道用户是点击了对话框的确认按钮还是取消按钮,那就在退出对话框的时候,
利用Navigator.of(context).pop("一些数据");
在 MaterialDesign下,
Dialog主要有 3 种:
- SimpleDialog
- AlertDialog
- BottomSheet
1. SimpleDialog
1 | void showMySimpleDialog(BuildContext context) { |
2. AlertDialog
1 | void showMyMaterialDialog(BuildContext context) { |
3. BottomSheet
1 | showModalBottomSheet( |
4. 自定义弹窗
定义组件类来继承Dialog,添加build方法,return 自定义内容
1 | class MyCustomLoadingDialog extends StatelessWidget { |