Flutter —— 聊天界面

585 阅读5分钟

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

首先做的事右上角的按钮,右上角要弄一个menu,那么这里可以在AppBar的actions添加Flutter封装好的控件PopupMenuButton,用一个container包着调整位置,然后用offset调整自身的位置。

actions: [
         Container(child:
         PopupMenuButton(
           itemBuilder: (BuildContext context) {
             return <PopupMenuItem<String>>[
               PopupMenuItem(child: Text("发起群聊")),
               PopupMenuItem(child: Text("发起群聊")),
               PopupMenuItem(child: Text("发起群聊")),
             ];
           },
           child: Image(
             image: AssetImage('images/圆加.png'),
             width: 25,
           ),
           offset: Offset(0,60)
         ),
         margin: EdgeInsets.only(right: 10),),
         color: Color.fromRGBO(1, 1, 1, 0.65),),
        ],

创建一个_buildPopupMenuItem来快速创建PopupMenuItem.

  Widget _buildPopupMenuItem(String imgAsset, String title) {
    return Row(
      children: [
        Image(
          image: AssetImage(imgAsset),
          width: 20,
        ),
        SizedBox(width: 20,),
        Text(title,style: TextStyle(color:Colors.white),),
      ],
    );
  }

在PopupMenuButton中使用。

    child: PopupMenuButton(
                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', '/收付款')),
                  ];
                },

接下来要模拟网络请求。去rap2.taobao.org/创建一个仓库。

在这里插入图片描述

之后点击新建接口。

在这里插入图片描述

输入相应的内容

在这里插入图片描述

创建好之后点击编辑

在这里插入图片描述

随机生成响应内容图片,名字以及内容,这里使用randomuser.me/来生成随机头像。

在这里插入图片描述

这样网络数据就准备好了。 将Scaffold中的body改成下面的构造。

   body: Container(
        child: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Text('hello');
          },
          itemCount: 10,
        ),
      ),

接下来要添加库,一般Flutter使用的网络请求库有两个,一个是dio,一个是官方的http。这里先使用http,在pubspec添加http: ^0.13.4。

在这里插入图片描述

引用http之后调用一个新创建的方法getDatas,在getDatas里面使用http请求数据。这里getDatas使用async异步执行。异步不代表多线程,多线程可以异步,但是异步不代表多线程。一个线程可以在多个方法中来回切换来实现异步。

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

 @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getDatas();
  }
  void getDatas() async {
    Uri url = Uri.parse("http://rap2api.taobao.org/app/mock/293458/api/chat/list");
     final response = await http.get(url);
  }

这里模拟Json 转map的过程,先创建一个map,然后转为json,然后在转为map,打印出来看到两者区别就是有没有“ ”。

 //Json 转map
    final chat = {
      'name':'张三',
       'message':'吃了吗'
    };
    final chatJson = json.encode(chat);
    print(chatJson);  
    final newChat = json.decode(chatJson);
    print(newChat);
    print(newChat['name']);
    print(newChat['message']);

在这里插入图片描述

接下来还要map转模型。 这里先创建一个Chat Class,然后创建一个工厂构造方法来返回模型。

class Chat {
  final String? name;
  final String? message;
  final String? imageUrl;
  Chat({this.name,this.message,this.imageUrl});

  factory Chat.fromMap(Map map){
    return Chat(
      name:map['name'],
      message:map['message'],
      imageUrl:map['imageUrl'],
    );
  }

}

然后在newChat下面添加

   final chatModel = Chat.fromMap(newChat);
    print('name:${chatModel.name}, message:${chatModel.message}, ');

运行后发现转成功了,成功打印出来了。

在这里插入图片描述

接下来要从网络中拉取数据,然后将其转换为模型,然后放入模型数组里面。 getDatas方法一般是要有返回值的,而在Flutter中,如果一个异步方法有返回值,则需要使用Future.这里改写getDatas方法,然后将获取的数据转换为List返回出去。

Future<List<Chat>>  getDatas() async {
    final Uri url = Uri.parse(
        "http://rap2api.taobao.org/app/mock/293458/api/chat/list");
    final response = await http.get(url);

    if (response.statusCode == 200) {
      // 获取响应数据,转成map
     final responseBody =  json.decode(response.body);
     // map 作为List的遍历方法。
     final chatList = responseBody['chat_list'].map<Chat>((item) => Chat.fromMap(item)).toList();
     print(chatList);
     return chatList;
    } else {
      throw Exception('Error with statusCode ${response.statusCode}');
    }
  }

Future可以搭配then使用,这里就相当于回调。Future代表着是未来的数据,then相当于把里面的代码保存起来,等数据来的时候再调用。

 getDatas().then((value)  {
      
    });

很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面。当然,通过 StatefulWidget 完全可以实现上述这些功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,因此Flutter专门提供了FutureBuilder 组件来快速实现这种功能。FutureBuilder会依赖一个Future,通常是一个异步耗时任务,它会根据所依赖的Future的状态来动态构建自身。这个时候,getDatas返回的数据就在snapshot里面。

child:FutureBuilder(
          future: getDatas(),
          builder:(BuildContext context, AsyncSnapshot snapshot) {
            return Container(
                          print('Data:  ${snapshot.data}');
            );
          } ,
        ),

运行后发现这里没有数据的时候也运行了一次,这是因为这里不能卡住UI,没有数据的时候渲染占位的东西,有数据的时候重新渲染更新页面。这里就是异步渲染。

在这里插入图片描述

这里也可以使用snapshot.的connectionState来返回不同的界面。

  if(snapshot.connectionState == ConnectionState.waiting) {
              return Center(child: Text( "Loading"),);
            }
             return ListView(
              children: snapshot.data.map<Widget>((Chat item){
                return ListTile(
                  title: Text(item.name ?? ""),
                  subtitle: Container(
                    height: 20,
                    child: Text(item.message ?? ""),
                  ),
                  leading: CircleAvatar(
                    backgroundImage: NetworkImage(item.imageUrl ?? ""),
                  ),
                );
              }
            ).toList(),
            );

FutureBuilder比较适合在数据不多的情况下使用,在数据很多的情况下,用FutureBuilder并不好,因为我们需要保存数据。 声明一个空数组,然后在initState的时候使用then在请求数据后进行保存。

// 模型数据
List<Chat> _datas = [];
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getDatas().then((List<Chat> datas)  {
      setState(() {
        _datas = datas;
      });
    });
  }

然后在使用ListView进行渲染。

body: Container(
          child: Container(
              child: _datas.length == 0
                  ? Center(
                      child: Text('Loading'),
                    )
                  : ListView.builder(
                      itemBuilder: (BuildContext context, int index) {
                        return ListTile(
                          title: Text(_datas[index].name ?? ""),
                          subtitle: Container(
                            alignment: Alignment.bottomCenter,
                            padding: EdgeInsets.only(right: 10),
                            height: 25,
                            child: Text(
                              _datas[index].message ?? "",
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                          leading: ClipRRect(
                            //剪裁为圆角矩形
                            borderRadius: BorderRadius.circular(5.0),
                            child: Image(
                                image:
                                    NetworkImage(_datas[index].imageUrl ?? "")),
                          ),
                        );
                      },
                      itemCount: _datas.length,
                    ))),

这里单纯的使用_datas的长度判断并不是很好,那么可以使用catchError,whenComplete,timeout来进行处理,例如timeout不进行加载_datas。

  bool _cancelConnect = false;

class _ChatPageState extends State<ChatPage> {
  bool _cancelConnect = false;
// 模型数据
  List<Chat> _datas = [];
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getDatas().then((List<Chat> datas) {
      if (!_cancelConnect){
        setState(() {
          _datas = datas;
        });
      }
    }).catchError((e) {
      print(e);
    }).whenComplete(()  {

    }).timeout(Duration(milliseconds: 100)).catchError((timeoutError){
      print('超时了!$timeoutError');
      _cancelConnect = true;
    });
  }

现在每次进来依然会调用initState进行请求数据,Flutter中如果想要保留数据,那么就需要混入AutomaticKeepAliveClientMixin类。

class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin 

然后重写wantKeepAlive为true。

  bool get wantKeepAlive =>  true;

然后需要在build方法里面调用 super.build(context)。

  Widget build(BuildContext context) {
    super.build(context);
    .......
    }

现在这个类可以保住状态了,但是在root页面里面只是显示某一个页面,也就是切出去的时候Widget树就没有chatpage这个对象了,这个时候chatpage就会被移除掉。也就是说,如果想要保存下来,那么就要将页面保存在Widget树里面,只是显不显示的问题。那么就到HomePage修改。这里声明一个PageController,然后Body使用PageView,然后在bottomNavigationBar的onTap中使用jumpToPage切换页面。

在这里插入图片描述

使用PageView之后可以左右拖拽切换页面的,这个时候如果想要切换下面的bottomNavigationBar的选中,那么就可以使用onPageChanged。

 onPageChanged:(int index){
          setState(() {
            _currentIndex = index;
          });
        } ,

否则就将physics设为NeverScrollableScrollPhysics()。

	physics: NeverScrollableScrollPhysics(),