上一节,我们完成了【我的】+【通讯录】页面开发
本节,我们开发【聊天】页面:
- Mock网络数据
- 网络请求
- 聊天页 (状态保留)
相关资源链接
- Mock数据地址:
http://rap2.taobao.org/account/login- 随机头像地址:
https://randomuser.me/photos- mockjs规则示例:
http://mockjs.com/examples.html- dart包的公共库:
https://pub.dev/
1. Mock网络数据
使用
阿里妈妈团队开发的RAP接口管理平台进行接口和数据的创建,模拟微信好友信息。-
点击进入 👉 Mock登录页,使用
邮箱注册并登录
image.png -
新建
Flutter入门仓库:
image.png -
点击
进入仓库,新建接口,设置Url地址、名称、类型、状态码:
image.png -
编辑
聊天接口:
image.png 新增
chat_list数组,50个元素,每个元素包含imageUrl头像、name名称、message消息。
生成规则,可以参考👉 mock.js实例
@natural: 随机生成数字
@name:随机生成英文名字
@cname:随机生成中文名字
@cparagraph:随机生成一段文字

头像链接从👉随机用户网站临时获取
image.png
- 拷贝地址
https://randomuser.me/api/portraits/women/85.jpg,将最后的85编号修改为随机值:
https://randomuser.me/api/portraits/women/@natural(20,70).jpg
image.png
-
获取
接口内容:
image.png -
接口地址为
http://rap2api.taobao.org/app/mock/277621/api/chat/list,每次访问,随机生成50条数据。
image.png
2. 网络请求
- 官方提供了
http的公共网络请求包,在👉dart公共库可以搜索到http及其使用方式


2.1 导入http网络请求包
- 拷贝
http版本,在项目的pubspec.yaml配置文件中,dependencies一栏新增http包引用

-
获取所有包:点击Pub get或终端手动输入flutter pub get,都可以获取到。

2.2 常规网络请求
- 创建
Chat 聊天模型,使用factory创建工厂化方法,将Map对象转换为Chat对象:
class Chat {
final String name; // 名称
final String message; // 消息
final String imageUrl; // 头像链接
Chat({this.name, this.message, this.imageUrl});
// 工厂方法,Map对象转Chat对象
factory Chat.fromJson(Map json) {
return Chat(name: json["name"],
message: json["message"],
imageUrl: json["imageUrl"]);
}
}
- 使用
http请求获取数据:
// Future表示可能存在错误,记录正在执行的状态
// async异步请求,配合await使用
Future<List<Chat>> getDatas() async {
// 发起请求并等待结果
final response = await http.get(
'http://rap2api.taobao.org/app/mock/277621/api/chat/list');
// 状态码不为200,抛出错误
if (response.statusCode != 200) {
throw Exception('statusCode: ${response.statusCode}');
}
// response.body是json数据,json转map再转Model
final responseBody = json.decode(response.body); // Map结构
List<Chat> chatList = responseBody['chat_list'].map<Chat>((item) =>
Chat.fromJson(item)).toList(); // 将列表元素都转换为Chat类型
// print(chatList.map((item) => print(item.name)));
return chatList;
}
- 使用
Future类,记录执行状态,返回内容和错误,供外部处理。网络请求使用异步任务,用async修饰,必须配合await等待网络结果。网络请求的错误情况,使用throw抛出Exception错误信息。接口返回的数据是json格式,使用json.decode转化成Map结构,再转换成对应Model类型。
2.3 数据的使用
- 数据的使用,介绍两种方法:
常规方式:使用变量进行承接,通过setState()刷新部件快捷方式:使用FutureBuilder部件,在future属性中设置异步请求函数,在builder中读取AsyncSnapshot 异步的数据结果并返回构建的部件
2.3.1 常规方式
- 假设我们在
initState中调用getDatas请求数据,对于Future类型的数据,可以有两种方法进行处理:
方法一: 使用
try catch处理正常请求结果和error错误方法二: 使用
Future提供的各种状态,链式处理:
then:获取正确的内容catchError:捕获异常timeout:设置超时时间whenComplete:捕获结束状态常识:
接口请求超时,仅代表超出客户端的timeout请求时限,并不代表取消了请求。依旧会收到服务端的返回结果,我们一般通过Bool值记录是否需要处理返回结果(超时或手动取消,不处理结果)
- 错误示例(
已超时但仍接收了返回结果):
image.png正确做法,下面代码中使用
_cancelConnect进行状态记录,觉得是否接收返回结果
@override
void initState() {
super.initState();
// 方法一: try catch
try { getDatas(); } catch(error) { print(error); }
// 方法二: Future状态链式处理
bool _cancelConnect = false;
getDatas()
// then 获取正确内容
.then((value) {
// 已取消,不接受数据
if (_cancelConnect) return;
/* 此处使用【变量】接受【value】值,配合【setState】重新【构建部件】即可 */
value.forEach((e) => print(e.name));
})
// 捕获错误
.catchError((error) => print("错误$error"))
// 设置超时时间,捕获超时错误
.timeout(Duration(milliseconds: 100)).catchError((timeout) {
_cancelConnect = true;
print("超时${timeout}");
})
// 结束
.whenComplete(() => print("完毕"));
}
2.3.2 快捷方式
- 使用
FutureBuilder异步部件,直接绑定异步数据与部件:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';
import 'package:http/http.dart' as http; //导入http库,取别名为http
import 'chat.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
// Future表示可能存在错误,记录正在执行的状态
// async异步请求,配合await使用
Future<List<Chat>> getDatas() async {
// 发起请求并等待结果
final response = await http
.get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');
// 状态码不为200,抛出错误
if (response.statusCode != 200) {
throw Exception('statusCode: ${response.statusCode}');
}
// response.body是json数据,json转map再转Model
final responseBody = json.decode(response.body); // Map结构
List<Chat> chatList = responseBody['chat_list']
.map<Chat>((item) => Chat.fromJson(item))
.toList(); // 将列表元素都转换为Chat类型
// print(chatList.map((item) => print(item.name)));
return chatList;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Wechat_themeColor,
elevation: 0.0,
centerTitle: true,
title: Text("聊天"),
),
// FutureBuilder 异步部件
body: FutureBuilder(
future: getDatas(), // future: 异步请求
builder: (BuildContext context, AsyncSnapshot snapshot) { // builder: 获取异步数据并返回部件
print(snapshot.data); // 数据
print(snapshot.connectionState); // 状态
return Container();
},
),
);
}
}
-
查看
打印结果,可以看到数据有null的情况,状态也看到了waitting等待和done完成两种:
image.png -
查看
ConnectionState,是枚举类型,包含4种情况:
image.png 我们可以根据
不同数据状态,展示不同的视图信息:
body: FutureBuilder(
future: getDatas(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: // 无
return Container();
case ConnectionState.waiting: // 加载中
return Center(child:Text("加载中"));
case ConnectionState.active: // 连接中
return Center(child:Text("连接中"));
case ConnectionState.done: // 已完成(根据数据展示页面)
print("数据: ${snapshot.data}");
return Center(child:Text("正常显示"));
default:
return Container();
}
},
)
- 以上,就是
网络请求的基本使用方式。下面进行聊天页的开发
3. 聊天页
- 开发
导航栏和气泡弹框 - 根据
网络数据,构建聊天Cell
3.1 导航栏Popup气泡
-
使用
Flutter提供的PopupMenuButton实现导航栏按钮和气泡弹框:
image.png chat_page页面代码:
import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
// 气泡视图
List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
return <PopupMenuItem<String>>[
_buildItem("发起群聊", "发起群聊"),
_buildItem("添加朋友", "添加朋友"),
_buildItem("扫一扫1", "扫一扫"),
_buildItem("收付款", "收付款")
];
}
// 气泡元素
PopupMenuItem<String> _buildItem(String assetImage, String name) {
return PopupMenuItem(
child: Row(children: [
Image(image: AssetImage("images/${assetImage}.png"), width: 20),
SizedBox(width: 20),
Text(name, style: TextStyle(color: Colors.white))
]));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Wechat_themeColor,
elevation: 0.0,
// 隐藏分割线
centerTitle: true,
// 安卓的导航栏标题未居中,可以设置居中
title: Text("聊天"),
actions: <Widget>[
Container(
margin: EdgeInsets.only(right: 20),
child: PopupMenuButton(
// 偏移值 kToolbarHeight: 导航栏高度
offset: Offset(0, kToolbarHeight),
// 图标
child: Container(
margin: EdgeInsets.only(right: 10),
child: Image(image: AssetImage("images/圆加.png"), width: 20),
),
// 气泡弹框
itemBuilder: _buildPopupMenuItem)),
],
),
body: Center(child: Text("聊天页面")),
);
}
}
注意
PopupMenuButton的child设置按钮内容,itemBuilder设置弹框内容,必须是内部元素为PopupMenuEntry<T>的List数组。(PopupMenuItem继承自PopupMenuEntry)
气泡弹框的背景色,需要更改主题背景色的cardColor:
main.dart中关于主题色的代码:import 'package:flutter/material.dart'; import 'package:wechat_demo/pages/root_page.dart'; void main() => runApp(App()); class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Wechat Demo', // 安卓需要,后台切换app时展示的名称(iOS中名称与APP名称一致) debugShowCheckedModeBanner: false, // 隐藏debug角标 home: RootPage(), theme: ThemeData( primaryColor: Colors.white, // 主题色 highlightColor: Color.fromRGBO(0, 0, 0, 0), // 去除高亮色 splashColor: Color.fromRGBO(0, 0, 0, 0), // 去除水波纹 cardColor: Color.fromRGBO(1, 1, 1, 0.5) // 弹出卡片背景色 ), ); } }
3.2 请求网络,构建聊天视图
- 当前使用
FutureBuilder,snapshot为waiting等待时,页面展示loading,其余状态展示MessageCell
ListTile类似于iOS的默认UITableViewCell,有title主标题、subtitle子标题、leading首部部件(本案例放图片)圆角头像:
Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0), image: DecorationImage(image: NetworkImage(item.imageUrl))))
- 圆形头像:
CircleAvatar(backgroundImage: NetworkImage(item.imageUrl))
-
chat_page代码:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';
import 'package:http/http.dart' as http; //导入http库,取别名为http
import 'chat.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
// Future表示可能存在错误,记录正在执行的状态
// async异步请求,配合await使用
Future<List<Chat>> getDatas() async {
// 发起请求并等待结果
final response = await http
.get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');
// 状态码不为200,抛出错误
if (response.statusCode != 200) {
throw Exception('statusCode: ${response.statusCode}');
}
// response.body是json数据,json转map再转Model
final responseBody = json.decode(response.body); // Map结构
List<Chat> chatList = responseBody['chat_list']
.map<Chat>((item) => Chat.fromJson(item))
.toList(); // 将列表元素都转换为Chat类型
// print(chatList.map((item) => print(item.name)));
return chatList;
}
// 气泡视图
List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
return <PopupMenuItem<String>>[
_buildItem("发起群聊", "发起群聊"),
_buildItem("添加朋友", "添加朋友"),
_buildItem("扫一扫1", "扫一扫"),
_buildItem("收付款", "收付款")
];
}
// 气泡元素
PopupMenuItem<String> _buildItem(String assetImage, String name) {
return PopupMenuItem(
child: Row(children: [
Image(image: AssetImage("images/${assetImage}.png"), width: 20),
SizedBox(width: 20),
Text(name, style: TextStyle(color: Colors.white))
]));
}
// 消息Cell
Widget _buildMessageCell(Chat item) {
return Container(child: Column(children: [
ListTile(
title: Text(item.name),
subtitle: Container(height: 20, child: Text(item.message, overflow: TextOverflow.ellipsis)),
leading: Container(
width: 40, height: 40,
decoration: BoxDecoration( // 圆角
borderRadius: BorderRadius.circular(12.0),
image: DecorationImage(image: NetworkImage(item.imageUrl)))),
),
Container(margin: EdgeInsets.only(left: 74), height: 1, color: Wechat_themeColor)
]));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Wechat_themeColor,
elevation: 0.0,// 隐藏分割线
centerTitle: true, // 安卓导航栏居中
title: Text("聊天"),
actions: <Widget>[
Container(
margin: EdgeInsets.only(right: 20),
child: PopupMenuButton(
offset: Offset(0, kToolbarHeight),// 偏移值 kToolbarHeight: 导航栏高度
child: Container( // 图标
margin: EdgeInsets.only(right: 10),
child: Image(image: AssetImage("images/圆加.png"), width: 20),
),
itemBuilder: _buildPopupMenuItem)),// 气泡弹框
],
),
body: FutureBuilder(
future: getDatas(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: Text("Loading...")); // loading
}
return ListView( // 正常展示
children: snapshot.data
.map<Widget>((item) => _buildMessageCell(item))
.toList());
},
),
);
}
}
-
展示样式:
image.png
使用FutureBuilder部件构建,每次进入页面都会触发网络请求,刷新页面,适用于简单页面。
当前模块,我们需要保留页面状态,不适合使用FutureBuilder,可使用变量(List<Chat>)记录返回值,将数据存储在内存中,保留页面状态
使用
上面介绍的常规方式,在initState中调用getDatas,用变量_datas记录返回值,通过setState重新构建部件。
getDatas中使用_cancelConnect记录了请求是否取消,默认false,每次请求都会重置为false,timeout超时会设置为true,then数据返回时,只有_cancelConnect为true才会更新数据,setState刷新页面。通过
_datas数据构建body。通过_datas.length区分loading和正常数据部件的展示使用
Mixins(混入)保活当前页面
Mixins(混入):
类似
iOS中的Category分类,用来给类增加功能, 使用【with】混入一个或多个mixin(实现多继承的关系)执行方法:
【第一步】:
state类使用with继承AutomaticKeepAliveClientMixin。例如:class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage> { ... }【第二步】:
重写wantKeepAlive计算属性。例如:@override bool get wantKeepAlive => true;【第三步】:执行
父类build。例如:Widget build(BuildContext context) { super.build(context); ... }
- 修改后的
chat_page.dart代码如下:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';
import 'package:http/http.dart' as http; //导入http库,取别名为http
import 'chat.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage>
with AutomaticKeepAliveClientMixin<ChatPage> { // 第一步,混入(多继承)
@override
bool get wantKeepAlive => true; // 第二步,重写wantKeepAlive计算属性
// 存储模型数据
List<Chat> _datas = [];
// 记录连接状态
bool _cancelConnect = false;
@override
initState() {
super.initState();
getDatas()
.then((value) {
if (_cancelConnect) return; // 已取消连接,不更新数据
_datas = value;
setState(() {});
})
.catchError((e) => print(e)) // 错误
.whenComplete(() => print("完毕")) //完毕
.timeout(Duration(seconds: 6)).catchError((timeout) {
_cancelConnect = true; // 超时取消连接
print("超时$timeout");
});
}
// Future 记录结果, async异步请求,配合await使用
Future<List<Chat>> getDatas() async {
// 每次调用请求,设为false(保证每次主动请求都可执行)
_cancelConnect = false;
// 发起请求并等待结果
final response = await http
.get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');
// 状态码不为200,抛出错误
if (response.statusCode != 200) {
throw Exception('statusCode: ${response.statusCode}');
}
// response.body是json数据,json转map再转Model
final responseBody = json.decode(response.body); // Map结构
List<Chat> chatList = responseBody['chat_list']
.map<Chat>((item) => Chat.fromJson(item))
.toList(); // 将列表元素都转换为Chat类型
return chatList;
}
// 气泡视图
List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
return <PopupMenuItem<String>>[
_buildItem("发起群聊", "发起群聊"),
_buildItem("添加朋友", "添加朋友"),
_buildItem("扫一扫1", "扫一扫"),
_buildItem("收付款", "收付款")
];
}
// 气泡元素
PopupMenuItem<String> _buildItem(String assetImage, String name) {
return PopupMenuItem(
child: Row(children: [
Image(image: AssetImage("images/${assetImage}.png"), width: 20),
SizedBox(width: 20),
Text(name, style: TextStyle(color: Colors.white))
]));
}
// 构建Body
Widget _buildBody() {
if (_datas.length == 0) return Center(child: Text("Loading..."));
return ListView.builder(
itemCount: _datas.length, itemBuilder: _buildCellOfRow);
}
// 构建Cell
Widget _buildCellOfRow(BuildContext context, int index) {
final item = _datas[index];
return Container(
child: Column(children: [
ListTile(
title: Text(item.name),
subtitle: Container(
height: 20,
child: Text(item.message, overflow: TextOverflow.ellipsis)),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
image: DecorationImage(image: NetworkImage(item.imageUrl)))),
),
Container(
margin: EdgeInsets.only(left: 74),
height: 1,
color: Wechat_themeColor)
]));
}
@override
Widget build(BuildContext context) {
super.build(context); // 第三步,执行父类build
return Scaffold(
appBar: AppBar(
backgroundColor: Wechat_themeColor,
elevation: 0.0,
centerTitle: true,
title: Text("聊天"),
actions: <Widget>[
Container(
margin: EdgeInsets.only(right: 20),
child: PopupMenuButton(
offset: Offset(0, kToolbarHeight), // 偏移值 kToolbarHeight: 导航栏高度
child: Container(// 图标
margin: EdgeInsets.only(right: 10),
child:
Image(image: AssetImage("images/圆加.png"), width: 20),
),
itemBuilder: _buildPopupMenuItem)),// 气泡弹框
],
),
body: _buildBody()); // 构建Body
}
}
app运行后,发现切换tab仍然会重新请求数据刷新页面
-
这是因为
main.dart中,我们每次切换tab,都是返回的bodys[index],每次都重新生成了页面,所以无法保留状态。
(如果需要保留状态,页面必须存在渲染树中)
image.png 我们将
main.dart的body改为PageView部件,使用_pageController(PageController类型)记录当前PageView,设置children为四个tab页面,将physics设置为NeverScrollableScrollPhysics()进制左右滑动。点击
Tab时,我们通过_pageController执行jumpToPage切换到指定tab。
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:wechat_demo/pages/chat/chat_page.dart';
import 'package:wechat_demo/pages/discover/discover_page.dart';
import 'package:wechat_demo/pages/friends/friends_page.dart';
import 'package:wechat_demo/pages/mine/mine_page.dart';
class RootPage extends StatefulWidget {
@override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
PageController _pageController = PageController(); // 记录当前PageView控制器
// 点击Tabbar
Widget onTap(int index) {
_currentIndex = index;
setState(() { });
_pageController.jumpToPage(index); // 调到指定tab页面
}
// 每个栏目的主页面
final List<Widget> bodys = [ChatPage(), FriendsPage(), DiscoverPage(), MinePage()];
// 每个栏目的底部Item
final List<BottomNavigationBarItem> items = [
BottomNavigationBarItem(
icon: Image(image: AssetImage('images/tabbar_chat.png'), width: 20),
activeIcon:
Image(image: AssetImage('images/tabbar_chat_hl.png'), width: 20),
label: "聊天"),
BottomNavigationBarItem(
icon: Image(image: AssetImage('images/tabbar_friends.png'), width: 20),
activeIcon:
Image(image: AssetImage('images/tabbar_friends_hl.png'), width: 20),
label: "通讯录"),
BottomNavigationBarItem(
icon: Image(image: AssetImage('images/tabbar_discover.png'), width: 20),
activeIcon: Image(
image: AssetImage('images/tabbar_discover_hl.png'), width: 20),
label: "朋友圈"),
BottomNavigationBarItem(
icon: Image(image: AssetImage('images/tabbar_mine.png'), width: 20),
activeIcon:
Image(image: AssetImage('images/tabbar_mine_hl.png'), width: 20),
label: "我的")
];
// 当前选中Index
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue,
body: PageView(
controller: _pageController, // 记录当前PageView,便于点击tab时,控制跳转
physics: NeverScrollableScrollPhysics(), // 禁止左右滑动页面 默认AlwaysScrollableScrollPhysics可滚动
children: bodys,
// 左右滚动页面
// onPageChanged: (index) {
// _currentIndex = index;
// setState(() { });
// },
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
// 固定大小,避免白色背景
fixedColor: Colors.green,
// 固定颜色
currentIndex: _currentIndex,
// 选择的默认值
items: items,
onTap: onTap,
// 点击回调
selectedFontSize: 12, // 选择字体大小设置为12(因为默认大小是12,这样可以去掉变大动画)
// selectedLabelStyle: ,
),
);
}
}
- 至此,
切换tab时,页面保持在原先滚动的位置,保持了原有状态。
总结
本节,我们掌握了:
- 【接口Mock】借助
RAP接口管理平台Mock接口数据;- 【网络请求】
http网络请求库的使用,通过Future获取多状态结果以及try catch和链式处理的方法,async和await的组合使用,使用_cancelConnect隔离数据和重复请求。
2.1 使用变量,内存存储异步数据,setState重新构建部件。
2.2FutureBuilder,每次都会异步请求数据并重构部件。- 【气泡弹框】气泡按钮弹框
PopupMenuButton的使用- 【状态保留】
state类通过with继承AutomaticKeepAliveClientMixin,重写wantKeepAlive,执行父类build。- 【PageView】
多页视图的使用(同时保活多个页面部件)。
下一节,完善聊天的搜索框和搜索页面。













