12、Flutter - 项目实战 - 仿微信(六)聊天页面

Flutter - 项目实战 - 仿微信(六)聊天页面

 

接上篇:11、Flutter - 项目实战 - 仿微信(五)通讯录

 

详细代码参见Demo

Demo地址 -> wechat_demo

 

其他相关联文章

7、Flutter - 项目实战 - 仿微信(一)BottomNavigationBar 4个主页面显示

8、Flutter - 项目实战 - 仿微信(二)发现页面

9、Flutter - 项目实战 - 仿微信(三)我的页面

10、Flutter - 项目实战 - 仿微信(四)数据准备

11、Flutter - 项目实战 - 仿微信(五)通讯录

 

效果

 

创建一个chat 文件夹,用来存放我们的搜索页面和 chat_page页面

这里就会用到 10、数据准备中的聊天接口。通过网络请求获取到数据,然后显示处理出来。

这时候我们需求要导入http包,类似iOS中pod

 

首先介绍个网址  pub.dev  (https://pub.dev/packages)中搜索 http,点进去查看

在 AS(Android Studio) 中的 pubspec.yaml  中配置 http: ^0.12.1

当然了我们这里用另外一个封装好的http 库, dio,可以在pub.dev 中搜索一下看看。(代码中有http库请求的代码)

用dio 的话我们抽一个单独的文件写请求解析

创建一个文件http_manager

1、http_manager.dart

import 'package:dio/dio.dart';

class HttpManager {
  static final Dio dio = Dio();

  static Future request(String url,
      {String method = 'get',
      Map<String, dynamic> queryParameters,
      int timeOut}) {
//    1、创建配置,什么方法请求
    final options = Options(method: method, receiveTimeout: timeOut);
//  2、发网络请求
    return dio.request(
      url,
      queryParameters: queryParameters,
      options: options,
    );
  }
}

Future<Response> get(url,
    {Map<String, String> headers,
    Map<String, dynamic> queryParameters,
    int timeout}) {
  return HttpManager.request(url,
      queryParameters: queryParameters, method: 'get', timeOut: timeout);
}

外部调用的方法, get ,因为网络请求是耗时操作,所以用异步  async。 传入参数url,可选参数headers ,queryParameters(查询参数), timeout 超时时间

然后调用HttpManager 通过dio 发起网络请求

    return dio.request(
      url,
      queryParameters: queryParameters,
      options: options,
    );

pub.dev 有示例代码

import 'package:dio/dio.dart';

/// More examples see https://github.com/flutterchina/dio/tree/master/example
main() async {
  var dio = Dio();
  Response response = await dio.get('https://google.com');
  print(response.data);
}

 

2、chat_page.dart

chat_page zhogn 还是用到了search_bar 搜索显示,在下面介绍。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:wechat/const.dart';
import 'package:wechat/pages/chat/search_bar.dart';
import 'package:wechat/tools/http_manager.dart' as http;

class ChatPage extends StatefulWidget {
  @override
  _ChatPageState createState() => _ChatPageState();
}

//部件 保持状态
class _ChatPageState extends State<ChatPage>
    with AutomaticKeepAliveClientMixin {
  Widget _buildPopupMenuItem(String imageAss, String title) {
    return Row(
      children: <Widget>[
        Image(
          image: AssetImage(imageAss),
          width: 20,
        ),
        SizedBox(
          width: 20,
        ),
        Text(
          title,
          style: TextStyle(color: Colors.white),
        ),
      ],
    );
  }

//  bool _cancleConnect = false;
  Timer _timer;
  //  CancelToken _token = CancelToken();

  List<Chat> _datas = [];

  @override
  void initState() {
    super.initState();
    print('Chat的init来了!');

//    int _count = 0;
//    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
//      _count++;
//      print(object)
//      print('数据来了');
//      if (_count == 99) {
//        timer.cancel();
//      }
//    });

    getDatas().then((List<Chat> datas) {
      print('数据来了');
//          if (!_cancleConnect) {
      print('更新数据');
      setState(() {
        _datas = datas;
      });
//          }
    }).catchError((e) {
      print('错误$e');
    }).whenComplete(() {
      print('完毕');
//        })
//        .timeout(Duration(seconds: 6))
//        .catchError((timeout) {
//          _cancleConnect = true;
//          print('超时输出:$timeout');
    });
  }

  //  网络请求耗时,定义方法的时候定义成异步的
  Future<List<Chat>> getDatas() async {
    //不在是取消连接了!
//    _cancleConnect = false;
    final response = await http.get(
        'http://rap2.taobao.org:38080/app/mock/256914/api/chat/list',
        timeout: 100);
    if (response.statusCode == 200) {
//        JSON 转字典,字典转模型
//      //获取相应数据,并转成Map类型!
//      final responseBody = json.decode(response.body);
//      转模型数组 map中遍历的结果需要返回处理啊
//    http请求解析
//      List<Chat> chatList = responseBody['chat_list'].map<Chat>((item) {
      List<Chat> chatList = response.data['chat_list'].map<Chat>((item) {
        return Chat.fromJson(item);
      }).toList();
      return chatList;
    } else {
      throw Exception('statusCode:${response.statusCode}');
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        appBar: AppBar(
          elevation: 0.0,
          title: Text('微信'),
          backgroundColor: WeChatThemColor,
          actions: <Widget>[
            Container(
              margin: EdgeInsets.only(right: 10),
              child: PopupMenuButton(
                offset: Offset(0, 60.0),
                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/扫一扫.png', '扫一扫')),
                    PopupMenuItem(
                        child: _buildPopupMenuItem('images/收付款.png', '收付款')),
                  ];
                },
              ),
            ) //右上角按钮
          ],
        ),
        body: Container(
          child: _datas.length == 0
              ? Center(
                  child: Text('Loading...'),
                )
              : ListView.builder(
                  itemCount: _datas.length + 1,
                  itemBuilder: _buildCellForRow,
                ),
        ));
  }

  Widget _buildCellForRow(BuildContext context, int index) {
    if (index == 0) {
      return SearchCell(
        datas: _datas,
      );
    }
//    因为第一个位置放了搜索框,所以index要 -1,才能得到正确的数组数据下标
    index--;

    print('text name = ' + _datas[index].name);
    print('images url = ' + _datas[index].imageUrl);

    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: Container(
        width: 44,
        height: 44,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(6.0),
            image:
                DecorationImage(image: NetworkImage(_datas[index].imageUrl))),
      ),
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
//      取消我们的timer
    print('走了');
    if (_timer != null && _timer.isActive) {
      _timer.cancel();
    }

    super.dispose();
  }

  @override
  bool get wantKeepAlive => true;
}

class Chat {
  final String name;
  final String message;
  final String imageUrl;

  //直接赋值创建对象,必须创建新对象
  const Chat({this.name, this.message, this.imageUrl});

  //工厂构造函数
  //factory 可以创建已有的对象
  factory Chat.fromJson(Map json) {
    return Chat(
      name: json['name'],
      message: json['message'],
      imageUrl: json['imageUrl'],
    );
  }
}

//关于Map和Json
/*
*
//    final chat = {
//      'name': '张三',
//      'message': '吃了吗?',
//    };
    //Map转Json
//    final chatJson = json.encode(chat);
//    print(chatJson);

    //Json转Map
//    final newChat = json.decode(chatJson);
//    print(newChat['name']);
//    print(newChat['message']);
//    print(newChat is Map);
* */

由于Flutter 在切换页面的时候,不在屏幕上的会被释放。重新返回的时候又会重新请求和渲染,这并不是我们想要的效果,那么就需要保持页面状态。

需要混入 Mixins

使用with 来混入一个或者多个Mixin

StatefulWidget 保持状态的做法,3步

 

1、混入一个

with AutomaticKeepAliveClientMixin {
 

2、重写 get 方法

@override
  bool get wantKeepAlive => true;
 

3、bulid 调用 super 的bulid 方

@override
Widget build(BuildContext context) {
  super.build(context);
  return Scaffold(
 

NavigationBar 也会遇到同样的问题,如果切换了bar 其他的就会被释放掉。我们不想让释放就需要持有这些小部件

root_page  里面,就用了PageView 然后去持有这些小部件,保证不被释放。这样就全部放到了Widget树中

List<Widget> _pages = [
  ChatPage(),
  FriendsPage(),
  DiscoverPage(),
  MinePage(),
];

class _RootPageState extends State<RootPage> {
  int _currentIndex = 0;

  final PageController _controller = PageController(initialPage: 0);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        //页面滚动变化时调用
//        onPageChanged: (int index){
//          _currentIndex = index;
//          setState(() {});
//        },
        physics: NeverScrollableScrollPhysics(), //禁止页面滚动,tabbar页面
        controller: _controller,
        children: _pages,
      ),

2.1、PopupMenuButton

菜单弹出框

chilid 设置一下显示的图片

itemBuilder 设置一下弹出框要显示的item

_buildPopupMenuItem 

定义 item 的显示样式

 

2.2、listView

如果没有请求到数据,页面显示  loading...

itemCount :_data.length + 1,

加的这个1是,在ListView最顶端添加了搜索框,所以加1

 

2.3、_buildCellForRow

显示会话列表的Cell

BoxDecoration  装饰器

DecorationImage  占位图

 

在iOS我们的cell都要考虑到复用的问题,为了性能。

但是在 Flutter 中没有复用,因为一旦移动到屏幕之外,就被释放掉了,所以不需要考虑复用的问题(也没有提供复用的方式)。

 

3、search_bar.dart 

实际输入搜索内容的搜索框

import 'package:flutter/material.dart';
import 'package:wechat/const.dart';
import 'package:wechat/pages/chat/search_page.dart';

import 'chat_page.dart';

class SearchCell extends StatelessWidget {
  final List<Chat> datas;
  const SearchCell({this.datas});
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (d) {
        Navigator.of(context).push(MaterialPageRoute(builder: (context) {
          return SearchPage(
            datas: datas,
          );
        }));
      },
      child: Container(
        height: 44,
        color: WeChatThemColor,
        padding: EdgeInsets.only(left: 5, right: 5, bottom: 5),
        child: Stack(alignment: Alignment.center, children: <Widget>[
          Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(6.0),
            ),
          ), //白色底
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Image(
                image: AssetImage('images/放大镜.png'),
                width: 15,
                color: Colors.grey,
              ),
              Text(
                '搜索',
                style: TextStyle(fontSize: 15, color: Colors.grey),
              )
            ],
          )
        ]),
      ),
    );
  }
}

class SearchBar extends StatefulWidget {
  final ValueChanged<String> onChanged;
  const SearchBar({this.onChanged});
  @override
  _SearchBarState createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  final TextEditingController _controller = TextEditingController();
  bool _showClear = false;
  _onChange(String text) {
    if (text.length > 0) {
      widget.onChanged(text);
      setState(() {
        _showClear = true;
      });
    } else {
      widget.onChanged('');
      _showClear = false;
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 84,
      color: WeChatThemColor,
      child: Column(
        children: <Widget>[
          SizedBox(
            height: 40,
          ),
          Container(
            height: 44,
//            color: Colors.red,
            child: Row(
              children: <Widget>[
                Container(
                  width: ScreenWidth(context) - 40,
                  height: 34,
                  margin: EdgeInsets.only(left: 5),
                  padding: EdgeInsets.only(left: 5, right: 5),
                  decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(6.0)),
                  child: Row(
                    children: <Widget>[
                      Image(
                        image: AssetImage('images/放大镜b.png'),
                        width: 20,
                        color: Colors.grey,
                      ),
                      Expanded(
                          flex: 1,
                          child: TextField(
                            controller: _controller,
                            onChanged: _onChange,
                            autofocus: true,
                            cursorColor: Colors.green,
                            style: TextStyle(
                              fontSize: 18.0,
                              color: Colors.black,
                            ),
                            decoration: InputDecoration(
                                contentPadding:
                                    EdgeInsets.only(left: 5, bottom: 10),
                                border: InputBorder.none,
                                hintText: '搜索'),
                          )),
                      _showClear
                          ? GestureDetector(
                              onTap: () {
                                setState(() {
                                  _controller.clear();
                                  _onChange('');
                                });
                              },
                              child: Icon(
                                Icons.cancel,
                                size: 20,
                                color: Colors.grey,
                              ),
                            )
                          : Container(), //取消按钮
                    ],
                  ),
                ), //左边的圆角背景
                GestureDetector(
                  onTap: () {
                    Navigator.pop(context);
                  },
                  child: Text('取消'),
                )
              ],
            ),
          ) //下面的搜索条
        ],
      ),
    );
  }
}

 

3.1、SearchCell

这个是chat_page 中 cell 第一个显示的,就是一个样式,然后点击进行跳转到search_page。并且将chat_page,中的数据传过去

    onTapDown: (d) {
        Navigator.of(context).push(MaterialPageRoute(builder: (context) {
          return SearchPage(
            datas: datas,
          );
        }));
      },

3.2、_onChange

输入框显示内容的调用

根据输入框内是否有内容显示小叉

 

3.3、searchBar 是导航栏上显示的搜索

在4、search_page 中调用

      body: Column(
        children: <Widget>[
          SearchBar(
            onChanged: (text) {
              _searchData(text);
            },
          ),

 

4、search_page.dart

import 'dart:core';

import 'package:flutter/material.dart';
import 'package:wechat/pages/chat/search_bar.dart';

import 'chat_page.dart';

class SearchPage extends StatefulWidget {
  final List<Chat> datas;
  const SearchPage({this.datas});
  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  List<Chat> _models = [];
  String _searchStr = '';
  TextStyle _normalStyle = TextStyle(
    fontSize: 16,
    color: Colors.black,
  );
  TextStyle _highlightStyle = TextStyle(
    fontSize: 16,
    color: Colors.green,
  );

  Widget _title(String name) {
    List<TextSpan> spans = [];
    List<String> strs = name.split(_searchStr);
    for (int i = 0; i < strs.length; i++) {
      String str = strs[i]; //拿出字符串
      if (str == '' && i < strs.length - 1) {
        spans.add(TextSpan(text: _searchStr, style: _highlightStyle));
      } else {
        spans.add(TextSpan(text: str, style: _normalStyle));
        if (i < strs.length - 1) {
          spans.add(TextSpan(text: _searchStr, style: _highlightStyle));
        }
      }
    }
    return RichText(
      text: TextSpan(children: spans),
    );
  }

  void _searchData(String text) {
    //每次进来都是重新搜索
    _models.clear();
    _searchStr = text;
    if (text.length > 0) {
      for (int i = 0; i < widget.datas.length; i++) {
        String name = widget.datas[i].name;
        if (name.contains(text)) {
          _models.add(widget.datas[i]);
        }
      }
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          SearchBar(
            onChanged: (text) {
              _searchData(text);
            },
          ),
          Expanded(
              flex: 1,
              child: MediaQuery.removePadding(
                context: context,
                removeTop: true,
                child: ListView.builder(
                    itemCount: _models.length, itemBuilder: _buildCellForRow),
              ))
        ],
      ),
    );
  }

  Widget _buildCellForRow(BuildContext context, int index) {
    return ListTile(
      title: _title(_models[index].name),
      subtitle: Container(
        alignment: Alignment.bottomCenter,
        padding: EdgeInsets.only(right: 10),
        height: 25,
        child: Text(
          _models[index].message,
          overflow: TextOverflow.ellipsis,
        ),
      ),
      leading: Container(
        width: 44,
        height: 44,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(6.0),
            image:
                DecorationImage(image: NetworkImage(_models[index].imageUrl))),
      ), //聊天cell
    );
  }
}

通过输入框的输入内容和 传过来的数据 datas,进过计算得出搜索结果

      body: Column(
        children: <Widget>[
          SearchBar(
            onChanged: (text) {
              _searchData(text);
            },
          ),

 遍历数据,将name 包含搜索内容的结果天交到_models 里面。切记需要先清空,_models.clear();

void _searchData(String text) {
    //每次进来都是重新搜索
    _models.clear();
    _searchStr = text;
    if (text.length > 0) {
      for (int i = 0; i < widget.datas.length; i++) {
        String name = widget.datas[i].name;
        if (name.contains(text)) {
          _models.add(widget.datas[i]);
        }
      }
    }
    setState(() {});
  }

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值