Flutter 布局聊天列表页及网络数据处理
下拉菜单栏实现
Widget _buildPopupMenuItem(String imgAss, String title) {
return Row(
children: [
Image(image: AssetImage(imgAss), width: 20,),
SizedBox(width: 20,),
Text(title, style: TextStyle(color: Colors.white),),
],
);
}
Container(
margin: EdgeInsets.only(right: 10),
child: PopupMenuButton(
offset: Offset(0, 50.0),
color: Color.fromRGBO(1, 1, 1, 0.65),
child: Image(image: AssetImage('images/圆加.png'), width: 25,),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem(child: _buildPopupMenuItem('images/发起群聊.png', '发起群聊')),
PopupMenuItem(child: _buildPopupMenuItem('images/添加朋友.png', '添加朋友')),
PopupMenuItem(child: _buildPopupMenuItem('images/扫一扫1.png', '扫一扫')),
PopupMenuItem(child: _buildPopupMenuItem('images/收付款.png', '收付款')),
];
},
),
)
如图所示,实现这种菜单栏我们可以使用 Flutter
提供的部件 PopupMenuButton
来实现,itemBuilder
属性是一个 PopupMenuItem
类型的数组,这里我们抽取了 _buildPopupMenuItem
方法来创建 item
,最后用 PopupMenuItem
部件包装 _buildPopupMenuItem
方法的返回值。
json 转模型
class ChatModel {
final String? name;
final String? message;
final String? imageUrl;
ChatModel({this.name, this.message, this.imageUrl});
//工厂构造方法
factory ChatModel.fromMap(Map map) {
return ChatModel(
name: map['name'],
message: map['message'],
imageUrl: map['imageUrl'],
);
}
}
final chatMap = {
'name' : 'ChenXi',
'message' : 'Hello!',
};
//Map 转 json
final chatJson = json.encode(chatMap);
print(chatJson);
// json 转 Map
final newChatMap = json.decode(chatJson);
print(chatJson);
final chatModel = ChatModel.fromMap(newChatMap as Map);
print(chatModel);
这里我们简单定义了一个 map
对象,代码示例中给出里 json
与 Map
的相互转换,及 Map
转模型。我们定义了一个 ChatModel
的模型,添加了 fromMap
方法,由外部传入一个 Map
类型的对象。开源的也有一些转模型的框架,这里我们先自己实现。
Future 使用
void initState() {
super.initState();
//获取网络数据
_getDatas().then((value) {
print('$value');
});
}
Future<List<ChatModel>> _getDatas() async {
//url 链接
final url = Uri.parse('http://rap2api.taobao.org/app/mock/294394/api/chat/list');
//发送请求
final response = await http.get(url);
if (response.statusCode == 200) {
//获取响应数据,并且把 json 转成 Map
final bodyMap = json.decode(response.body);
// 取出 bodyMap 中的 chat_list 数组,通过 map 方法进行遍历并转为模型,通过 toList 返回一个模型数组
final chatList = (bodyMap['chat_list'] as List).map((item) => ChatModel.fromMap(item)).toList();
return chatList;
} else {
throw Exception('statusCode:${response.statusCode}');
}
这里 Future
代表未来的数据,我们对返回的数据通过 Future
包装成 Future<List<ChatModel>>
,Future
有一个 then
方法,then
有一个外部传入一个闭包属性,当数据请求完成会调用闭包,这里我们可以拿到 value
的值,也就是模型数组。
FutureBuilder 异步渲染
body: Container(
child: FutureBuilder(
future: _getDatas(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//正在加载
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Text('正在加载!'),
);
}
//加载完成
return ListView(
children: snapshot.data.map<Widget>((ChatModel item) {
return ListTile(
title: Text(item.name as String),
subtitle: Container(
alignment: Alignment.bottomCenter,
height: 25,
child: Text(item.message as String, overflow: TextOverflow.ellipsis,),
),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(image: NetworkImage(item.imageUrl as String)),
),
),
);
}).toList(),
);
},
)
)
这里我们通过 FutureBuilder
部件实现网络数据的加载,FutureBuilder
部件支持异步渲染,future
属性是 Future
类型的数据。在每次进入微信页面的时候 builder
方法最少会被调用两次,没有数据的时候会调用一次,数据来了之后又会调用一次。ConnectionState.waiting
代表数据正在加载,在这里我们可以做一些空页面展示的处理。ConnectionState.done
代表数据加载完成,snapshot.data
就是 _getDatas
方法返回的列表数据,这里可以进行相关逻辑的处理, 这里我们展示聊天列表数据。ListView
中我们用 ListTile
部件来作为 cell
,ListTile
包含主标题 title
、副标题 subtitle
、头像 leading
等属性,用起来很方便。
网络请求数据处理
//模型数组
List<ChatModel> _datas = [];
void initState() {
super.initState();
//获取网络数据
_getDatas().then((value) {
if (!_cancelConnect) {
setState(() {
_datas = value;
});
}
}).catchError((e) {
_cancelConnect = true;
//获取数据失败
print(e);
}).whenComplete(() {
print('数据请求结束');
}).timeout(Duration(seconds: 5)).catchError((timeout) {
_cancelConnect = true;
print('请求超时 ! $timeout');
});
}
这里我们创建了一个外部成员变量 _datas
,用来保存网络请求的数据,网络请求成功之后调用 setState
方法,定义了一个属性 _cancelConnect
标识网络请求是否取消。catchError
代表请求失败,whenComplete
代表请求结束,timeout
可以设置超时时间,这里我们可以用来做一些 loading
页面的展示及错误页面的展示。
Container(
child: _datas.length == 0 ? Center(child: Text('Loading...')) :
ListView.builder(itemCount: _datas.length ,itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(_datas[index].name as String),
subtitle: Container(
alignment: Alignment.bottomCenter,
height: 25,
child: Text(_datas[index].message as String, overflow: TextOverflow.ellipsis,),
),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(image: NetworkImage(_datas[index].imageUrl as String)),
),
),
);
}),
)
对于列表的展示我们这里换回了 ListView
,使用 FutureBuilder
的话,当数据很多的话需要进行数据的保存,会专门放入一个数组,例如数据的分页加载等,这时候使用 FutureBuilder
就不太好,但是数据量不大的话用 FutureBuilder
就会很方便。
页面保持状态
class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage>
Widget build(BuildContext context) {
super.build(context);
}
class _RootPageState extends State<RootPage> {
int _currentIndex = 0;
List <Widget>_pages = [ChatPage(), FriendsPage(), DiscoverPage(), MinePage()];
final PageController _controller = PageController();
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
body: PageView(
//禁止页面拖拽
physics: NeverScrollableScrollPhysics(),
onPageChanged: (int index) {
setState(() {
_currentIndex = index;
});
},
controller: _controller,
children: _pages,
),
当我们切换底部 tabBar
的时候,每次进入页面都会重新加载,这里我们采用 AutomaticKeepAliveClientMixin
来保持状态,让页面只会被加载一次,以聊天页面为例,_ChatPageState
后面加上 with AutomaticKeepAliveClientMixin<ChatPage>
,并在 build
方法中调用 super.build(context)
。在 RootPage
中,用 _pages
数组来保存底部子页面,body
使用 PageView
部件,controller
赋值为我们定义的 _controller
,children
赋值为 _pages
。
Dart 中的异步编程
通过这个案例我们可以看到 任务 3
会被 for
循环给阻塞,那么我们怎么能把循环给放到异步而不影响其他任务继续执行呢?
这里我们可以通过 Future
进行包装,把耗时任务放到 Future
中,而不会阻塞 任务 3
的执行。
如果 任务 4
需要依赖异步耗时任务完成后再执行的话,可以使用 async
加 await
结合的方式。
当我们需要在耗时任务之后需要执行 任务 4
,并且耗时操作不阻塞 任务 4
的执行,这里我们可以定义一个变量 future
,调用 then
方法,then
中的闭包会在耗时操作完成之后执行。