当前位置:   article > 正文

6、Flutterr聊天界面&网络请求

6、Flutterr聊天界面&网络请求

一、准备网络数据

1.1 数据准备工作

  1. 来到网络数据制造的网址,注册登录后,新建仓库,名为WeChat_flutter;
  2. 点击进入该仓库,删掉左侧的示例接口,新建接口.

3. 接着点击右上角‘编辑’按钮,新建响应内容,类型为Array,一次生成50条

4. 点击chat_list左侧添加按钮,新建chat_list中的数据内容,此时用到一个获取随机头像的网站.到该网站中,随机复制一个图片地址,假设为:https://randomuser.me/api/portraits/women/35.jpg.将数据填上,然后保存.

5.接下来,我们想让获取的图像是个随机值,那么参考Mock.js网站中的生成规则.

6.接着回到响应内容这里,通过设置初始值规则,生成随机的图片地址.

  • 接下来填充名称,消息的随机值.

  • 综上:服务器的数据准备工作完成,请求的链接地址

二、聊天界面导航条

  • 首先设置_RootPageState中进入App默认选中的NavigationBar的私有变量_currentIndex = 0,也就是默认选中微信界面.
  • 然后根据微信聊天界面的UI效果,我们先实现右上角的加号.

1. AppBar的actions就是我们需要添加操作的地方.

  • 按照这个思路继续,就需要自己实现一个弹出菜单的组件.然而这个时候,Flutter其实已经提供了一套成熟的控件以达到效果

2. PopupMenuButton组件

  • PopupMenuButton组件用来弹出一个菜单,必传参数为itemBuilder,用来实现它需要展示的内容.PopupMenuItem就是用来展示内容的类.PopupMenuButton有个onSelected属性,这个属性是个闭包,意思是选中某个PopupMenuItem的时候,会调用这个闭包.但是有个前提就是每个PopupMenuItem的value必须不为null的时候,才会执行onSelected闭包.
  • AppBar中具体内部实现为.
  1. AppBar(
  2. //去除导航条黑线
  3. elevation: 0.0,
  4. backgroundColor: WeChatThemeColor,
  5. //设置标题默认居中、否则双端默认方式不一致
  6. centerTitle: true,
  7. title: const Text("微信", style: TextStyle(color: Colors.black),),
  8. actions: [
  9. Container(
  10. margin: EdgeInsets.only(right: 10),
  11. child: PopupMenuButton(
  12. onSelected: (item){
  13. print(item);
  14. },
  15. onCanceled: (){
  16. print('onCanceled');
  17. },
  18. //PopupMenuButton的背景颜色
  19. color: Colors.black,
  20. offset: Offset(0,60),
  21. child: Image(image: AssetImage('images/圆加.png'),width: 25,height: 25,),
  22. itemBuilder: (BuildContext context){
  23. return <PopupMenuItem>[
  24. _buildMenuItem('images/发起群聊.png','发起群聊'),
  25. _buildMenuItem('images/添加朋友.png','添加朋友'),
  26. _buildMenuItem('images/扫一扫1.png','扫一扫'),
  27. _buildMenuItem('images/收付款.png','收付款'),
  28. ];
  29. },
  30. ),
  31. )
  32. ],
  33. ),

3. 其中_buildMenuItem是我们封装的一个创建组件的方法,内部实现为

  1. PopupMenuItem _buildMenuItem(String imageName,String title){
  2. return PopupMenuItem(
  3. value: {
  4. 'imageName' : imageName,
  5. 'title' : title,
  6. },
  7. child: Row(
  8. children: [
  9. Image(image: AssetImage(imageName), width: 25,),
  10. SizedBox(width: 10,),
  11. Text(title, style: TextStyle(color: Colors.white),),
  12. ],
  13. )
  14. );
  15. }

4.关于PopupMenuButton背景颜色的设置.可以直接在其内部设置背景颜色.也可以在ThemeData中设置app的cardColor.但是优先级没有直接设置PopupMenuButton的高.如果不设置黑色背景颜色,弹出的视图显示均为白色,看不到UI效果.

三、请求网络数据

  1. 通过Dart packages 这个网站可以搜索flutter使用的包packages.我们使用http这个包来请求我们的网络数据.这个包是flutter官方提供的.实际项目开发的时候可能并不会使用http这个包,大部分是使用dio来请求网络数据.这里只介绍官方的http包如何使用.
  2. 导入http包.在名称后可以点击复制包名.

3.在项目的pubspec.yaml中粘贴复制的包名.

4. 粘贴完之后需要Pub get获取一下,获取包对应的代码.

  • 也可以在终端输入flutter packages get 来获取.

5.在chat_page.dart中导入http包并取别名

import 'package:http/http.dart' as http;

6.在渲染状态组件的时候发起网络请求,也就是在initState中发起网络请求.getData后采用async表示异步执行.async需要搭配await使用,await后面跟着的是耗时的代码,所以会异步执行调用.

  1. class _ChatPageState extends State<ChatPage> {
  2. .....
  3. @override
  4. void initState() {
  5. super.initState();
  6. getDatas();
  7. }
  8. getDatas() async {
  9. var response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'),);
  10. print(response.statusCode);//200
  11. print(response.body);//这里就是我们自定义的网络数据了
  12. }
  13. .....
  14. }
  • 点击其他界面再次回到聊天界面会发现initState方法重新走了一遍,调用了网络请求,这是因为我么还没有保存住状态.后面将讲述如何保存Widget的状态.

7.处理返回数据

  • 首先介绍一下,在flutter中如何将请求返回的JSON数据转为Map,在我们iOS开发中是转为字典,而flutter中没有字典这个类型,对应的类型是Map.以及如何将Map转为JSON.在iOS中我们会使用一个NSJSONSerialization的类用来处理JSON数据.同样的,在flutter中也会有一个专门的类JsonCode来处理.

JSON和Map互相转换

  • 首先需要导入dart中的convert组件.
  • 然后我们写点测试用例,熟悉它的使用方式.
  1. void initState() {
  2. super.initState();
  3. getDatas();
  4. final chat = {
  5. 'name': '张三',
  6. 'message': '在干嘛?',
  7. };
  8. //Map转JSON
  9. final jsonChat = json.encode(chat);
  10. print(jsonChat);
  11. //JSON转Map
  12. final mapChat = json.decode(jsonChat);
  13. print(mapChat);
  14. print(mapChat is Map);
  15. }
  • 返回结果如下:
  1. flutter: {"name":"张三","message":"在干嘛?"}
  2. flutter: {name: 张三, message: 在干嘛?}
  3. flutter: true
  4. flutter: 200
  • 其中的json就是JsonCodec的实例. 'is'是用来判断是不是某个类型.

8. 新建聊天模型

  • 因为网络出来的数据可能为空,那么就需要用?来修饰定义的属性;
  1. class Chat {
  2. final String? name;
  3. final String? message;
  4. final String? imageUrl;
  5. Chat(this.name,this.message,this.imageUrl);
  6. //工厂方法,用来初始化对象.
  7. factory Chat.fromJson(Map json){
  8. return Chat(json['name'],json['message'],json['imageUrl']);
  9. }
  10. }
  • factory 关键字用来标记当前是工厂方法,是设计模式的一种,用来初始化对象.除了默认的构造方法,还可以使用这个工厂方法来实例化一个Chat对象.模型建立好了之后就可以处理响应的数据.
    • 如下: 模型数据成功转换.
  1. //将json转为Chat模型
  2. final chatModule = Chat.fromJson(mapChat);
  3. print('name:${chatModule.name} message:${chatModule.message}');// name:张三 message:在干嘛?

9.处理响应的数据

  • 首先我们会获取到通过网络接口获取的列表数据,但是不能保证网络请求一定会发送成功.所以要处理一些错误情况.在flutter中引入Future.表示接下来请求的数据,可能有值也可能没有值,一般与网络请求配合使用.
  • 因此返回值我们可以设定为.
Future<List<Chat>?> getDatas() async {}
  • 对于异常情况的处理,可以通过throw Exception的形式.
  1. Future<List<Chat>?> getDatas() async {
  2. final response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'));
  3. print(response.statusCode);
  4. if (response.statusCode == 200) {
  5. } else {
  6. throw Exception('statusCode: ${response.statusCode}');
  7. }
  8. }
  • 接下来我们处理返回的body中的数据
    • 获取响应数据,并且转换为Map类型
  1. //获取响应数据,并且转换成Map类型
  2. final responseBody = json.decode(response.body);
  3. //转换模型数组
  4. responseBody['chat_list'].map(
  5. (item) {
  6. print(item);
  7. return item;
  8. }
  9. );
  • 这样可以看到item的遍历数据.
  1. flutter: {imageUrl: https://randomuser.mflutter: {imageUrl: https://randomuser.me/api/portraits/women/12.jpg, name: 黎超, message: 音和委起度明条部过们放省。们区以号还九保把王之候包与先件能议清。江知天能能五开比点别增石次米五平。极养提立手专把示低率号容眼组是石。离维照联子象派三热始受构参元离还。相电构次色影件力计面进东把。}
  2. flutter: {imageUrl: https://randomuser.me/api/portraits/women/23.jpg, name: 傅秀兰, message: 但保写太满果此力少合反压色生太个图。制社并更个构北不张需国些清不。没八你或况铁员三时划志有改题头感。值年改你要变程新但八传织。进化林号中不按亲天张原美多。}e/api/portraits/women/37.jpg, name: 李丽, message: 可组品且发铁直报表状传素安小全。器音天石别数业局装共习清。加然处进派变装你农速约部族利音次层。毛得理状主质所局等工型即天研走机段。}
    • 接下来将其返回结果直接遍历为模型返回为List
  1. final responseBody = json.decode(response.body);
  2. //转换模型数组
  3. List<Chat>chatList = responseBody['chat_list'].map<Chat>(
  4. (item) {
  5. return Chat.fromJson(item);
  6. }
  7. ).toList();
  8. print(chatList);
  9. return chatList;
  • 此时我们可以看到输出均为实例对象
[Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat',...] 
  • 上述可以直接采用箭头函数.这样我们就实现了响应数据的模型转换.
  1. List<Chat>chatList = responseBody['chat_list'].map<Chat>((item) => Chat.fromJson(item)).toList();
  2. return chatList;

10.处理网络请求的结果

  • 接下来我们要处理返回Future类型的异步网络请求结果.
    • 可以采用try...catch
    • 也可以采用then结合的形式
  • 这里我们使用then的方式处理结果, 输出其中的value.
  1. loadData(){
  2. getDatas().then((value) {
  3. print(value);
  4. });
  5. }
    • 这里可以看到value的结果就是我们的chatList数据.
  • 其实我们还可以采用一种更为简单的方式处理网络请求.在下一章讲解.

四、利用FutureBuilder渲染微信界面

  • 在flutter中渲染网络数据专门有个控件叫做FutureBuilder.当无数据时展示默认界面,有数据时继续渲染网络请求下来的数据

  • 此时可以看到返回的结果中 先是null、然后再是连续几次的数据.

  1. 此时我们可以通过snapshot的异步连接状态来查看.
  • 也就是snapshot.connectionState
    • 当处于waiting状态时,data会返回null
    • 当处于done的状态时,data会返回正常解析结果.
  1. flutter: data: null
  2. flutter: state:ConnectionState.waiting
  3. flutter: data: [Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', ...]
  4. flutter: state:ConnectionState.done

    2. 所以这个时候我们可以借助于ConnectionState状态来确定当前要渲染的界面.

    • 当waiting状态时,展示一个Loading...
    • 当done状态时,渲染界面.
  • 因此我们的FutureBuilder的渲染实现部分为
  1. FutureBuilder(
  2. future: getDatas(),
  3. builder: (BuildContext context, AsyncSnapshot snapshot){
  4. //无数据时渲染默认界面,有数据时显示网络数据
  5. print('state:${snapshot.connectionState}');
  6. if (snapshot.connectionState == ConnectionState.waiting) {
  7. return Center(child: Text('Loading...'),);
  8. } else {
  9. return ListView(
  10. children: snapshot.data.map<Widget>((item){
  11. return ListTile(
  12. //右侧 标题
  13. title: Text(item.name),
  14. //右侧 子标题
  15. subtitle: Container(
  16. height: 20,width: 20,
  17. //TextOverflow.ellipsis 展示不下的时候省略号
  18. child: Text(item.message,overflow: TextOverflow.ellipsis,),
  19. ),
  20. //左侧:圆型头像
  21. leading: CircleAvatar(
  22. backgroundImage: NetworkImage(item.imageUrl),
  23. ),
  24. );
  25. }).toList(),
  26. );
  27. }
  28. },
  29. )
  • 展示的效果图如下:

  • 但是这样的渲染方式不是最好的.因为每次进入微信界面就需要发送网络请求.每次都要重新渲染.
    • 那么这种FutureBilder直接渲染布局的方式只能应用于数据相对简单的界面.
  • 也可以将请求下来的数据放入一个缓存模型数组中,当builder的时候再从模型数组中拿取使用.

五、网络请求的处理

  1. 这里我们将网络请求的数据进行处理.首先来到 _ChatPageState中,创建一个缓存数组
List<Chat> _datas = [];

      2. 在loadData时处理返回的数据.

  • 通过then将正常返回的数据赋值给缓存,然后在setState方法中实现这个赋值操作,就会引起界面的渲染.
  • 将错误的返回结果通过日志输出.
  • 会有类型不匹配的错误: 将datas设置为可空类型,然后在赋值的时候对空的情况进行空处理就行.
  1. loadData(){
  2. //当数据正常返回的时候
  3. getDatas().then((List<Chat>? datas) {
  4. setState(() {
  5. _datas = datas ?? [];
  6. });
  7. }).catchError((err){
  8. print(err);
  9. });
  10. }

3. 这个时候通过缓存的数据来渲染界面

  • Scaffold中body的FutureBuilder替换回Container.通过三目运算符判断当前缓存数组中是否有值,如果没有,就设置Loading.如果有值就渲染界面
  1. Container(
  2. child: _datas.length == 0 ?
  3. Center(child: Text('Loading...'),)
  4. : ListView.builder(
  5. itemCount: _datas.length,
  6. itemBuilder: (BuildContext context, int index) {
  7. return ListTile(
  8. title: Text(_datas[index].name ?? ""),
  9. subtitle: Container(height: 20,width: 20,child: Text(_datas[index].message ?? ""),),
  10. leading: CircleAvatar(
  11. backgroundImage: NetworkImage(_datas[index].imageUrl ?? ""),
  12. ),
  13. );
  14. }),
  15. ),
  • 点击其他NavigationBar界面,再点回微信界面,会发现界面的渲染过程.

4. 关于Future请求结果的处理完善

  • Future请求结果的处理方面,除了catchError之外,还有whenComplete、timeout
  1. loadData(){
  2. //当数据正常返回的时候
  3. getDatas().then((List<Chat>? datas) {
  4. setState(() {
  5. _datas = datas ?? [];
  6. });
  7. }).catchError((err){
  8. print(err);
  9. }).whenComplete(() {
  10. //数据处理完毕
  11. print("完毕");
  12. }).timeout(Duration(milliseconds: 10)).catchError((timeout){
  13. print("加载超时 ${timeout}");//flutter: 加载超时 TimeoutException after 0:00:00.010000: Future not completed
  14. });
  15. }
  • 在这里,超时并不意味着请求结束,当请求结果返回之后,仍然调用了请求完毕.
  • 在这里,我们我们需要处理: 一旦请求发出,除非异常,否则超时后其他的数据都不应该返回去调用我们的setState渲染界面.

5. 请求超时的异常处理/多次重复刷新

  • 设置一个私有bool变量_cancelConnect, 如果当前的标记不为true,那么再去setState渲染界面.避免了数据污染.

  • 这个时候我们发现界面和通讯录界面都存在一种现象:当我们点击其他界面,再回到当前界面时,数据会重新加载.这个现象说明了我们需要进行状态的保存以避免重复刷新.接下来将对这个现象进行处理.

六、保存Widget的状态

关于状态的保存,这个时候我们引入另一个概念: 混入(Mixins),用来给类增加功能,是多继承模式下的一种代码复用.使用with关键字来实现混入一个或多个类.

  1. 因为flutter渲染效率非常高,当控件不在界面上展示的话就会被销毁,再次展示时会重新渲染.
  2. 如果我们的状态需要保存的话,就需要混入一个类:AutomaticKeepAliveClientMixin,也就是对ChatPage的延展.重写wantKeepAlive属性并且在build渲染方法中调用父类的渲染方法.
  1. //AutomaticKeepAliveClientMixin让当前界面保存状态
  2. class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage>{
  3. .....
  4. //保留setState状态
  5. @override
  6. bool get wantKeepAlive => true;
  7. @override
  8. Widget build(BuildContext context) {
  9. //6.3 重写父类渲染方法
  10. super.build(context);
  11. ...
  12. }
  13. ...
  14. }

3. 同理我们去保存通讯录界面的状态.

4. 这个时候我们去检测设置的效果,发现貌似并没有设置成功.还是会重新渲染对应的界面.是否是设置失效了呢?

  • 其实不是.这个时候就需要考虑我们的根视图设置的问题了.因为之前预埋了坑点.

5. 回到rootpage.dart中,我们发现在设置当前显示body的时候,采用的是从_pages数组中获取对应的界面.在iOS中,这样设置并没有什么问题.

  • 但是在flutter中,这样设置,每次根据_currentIndex获取的视图显示对象并不是数组中设置的对象.
    • 因为在flutter中,在数组中创建的对象对于flutter来说只是一堆数据,通过build方法渲染到界面上.
    • 在build中有一个小部件树,这个小部件树是从MyApp开始build渲染,然后是它的home属性包含的RootPage对象,再通过RootPage中的设置的Container,去包含的ChatPage、FriendsPage等
MyApp => RootPage => Container => ChatPage/FriendsPage/DiscoverPage/MinePage
  • 一旦当前_currentIndex改变之后,那么设置的body就从_pages中获取到对应的界面对象.然而就是这个操作导致.当前Container包含的Page从一个切换到另一个,之前的界面就不在渲染树中.也就是被销毁了.

6. 回到我们在微信界面/通讯录界面混入AutomaticKeepAliveClientMixin这个类的目的:

    • 让小部件树之外指定的界面不要被销毁.也就是我们想要保留根视图下四个界面状态.当其中一个渲染在小部件树中的时候,另外三个不要被销毁.
  • 为了解决这个问题,我们引入flutter中另外一个控件PageController.

7. PageController设置rootpage根视图界面.

  • 首先创建一个私有变量PageController _controller, 并将初始界面page设置为第一个界面.
  1. final PageController _controller = PageController(
  2. //初始显示的界面索引
  3. initialPage: 0,
  4. );
  • 其次将Container中Scaffold的body设置为PageView
    • onPageChanged就是我们需要设置index改变的回调的地方
    • NeverScrollableScrollPhysics可以去除默认的左右滚动的效果.
  1. PageView(
  2. onPageChanged: (int index ){
  3. _currentIndex = index;
  4. setState(() {
  5. });
  6. },
  7. //如果不设置这个属性,那么根视图事件可以左右滚动切换.
  8. physics: NeverScrollableScrollPhysics(),
  9. controller: _controller,
  10. children: [
  11. ChatPage(),
  12. FriendsPage(),
  13. DiscoverPage(),
  14. MinePage()
  15. ],
  16. ),
  • 综上:做完这些操作,我们再去点击任意界面,之前的状态就会被保留下来.
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/257461
推荐阅读
相关标签
  

闽ICP备14008679号