-
上一节,我们完成了【我的】+【通讯录】页面开发
-
本节,我们开发【聊天】页面:
- 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邮箱注册
并登录
-
新建
image.pngFlutter入门
仓库:
-
点击
image.png进入仓库
,新建接口
,设置Url地址、名称、类型、状态码
:
-
编辑
image.png聊天接口
:
-
新增
chat_list
数组,50个
元素,每个元素包含imageUrl头像
、name名称
、message消息
。
image.png生成规则,可以参考👉 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接口内容
:
-
接口地址为
image.pnghttp://rap2api.taobao.org/app/mock/277621/api/chat/list
,每次访问,随机
生成50条
数据。
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();
},
),
);
}
}
-
查看
image.png打印结果
,可以看到数据
有null
的情况,状态
也看到了waitting
等待和done
完成两种:
-
查看
image.pngConnectionState
,是枚举
类型,包含4种情况
:
-
我们可以根据
不同数据状态
,展示
不同的视图信息
:
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气泡
-
使用
image.pngFlutter
提供的PopupMenuButton
实现导航栏按钮
和气泡弹框
:
-
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
仍然会重新请求数据
刷新页面
-
这是因为
image.pngmain.dart
中,我们每次切换tab
,都是返回的bodys[index]
,每次都重新生成
了页面
,所以无法保留状态
。
(如果需要保留
状态,页面
必须存在渲染树中
)
-
我们将
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】
多页视图
的使用(同时保活多个页面部件)。
下一节,完善聊天
的搜索框
和搜索页面
。
网友评论