Flutter项目(4)之仿微信聊天界面

1 创建网络仓库和假数据接口

  1. 创建仓库链接

  2. 创建接口:

    • 返回值是array,生成规则可以为50,表示一次返回50条数据;
    • 点击前面的+号,可以返回每条数据的具体值,比如image_url;网络头像链接,每个头像的结尾数字值不一样,代表不同的图片,可以使用生成随机数字的语法来生成不同的图片,根据mock.js语法,加入**@natural(10,70)**,代表生成10-70之间的随机数;
    • 需要返回user_name,即用户名,初始值可以输入 @cname,返回的是中文名称;
    • 还需要返回聊天的内容message,初始值可以写**@cparagraph**,返回的是中文的随机内容;
  3. mock.js语法链接;

  4. 返回接口内容示例:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sCDD82WX-1583233191829)(https://upload-images.jianshu.io/upload_images/6266734-348eda659132dc2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

2 导航栏的创建

!(img-h95FEqh4-1583233191830)(https://upload-images.jianshu.io/upload_images/6266734-5d6333b4d07fb546.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620)]

聊天界面导航栏右上角有一个+更多按钮,点击这个按钮的时候弹出一个弹窗:

appBar: AppBar(
  centerTitle: true,
  title: Text('微信'),
  actions: [
  Container(
    margin: EdgeInsets.only(right: 15),
    child: PopupMenuButton(
      offset: Offset(0, 60),
        child: Image(image: AssetImage('images/圆加.png'),width: 25,),
        itemBuilder: _PopMenuItemBuild,
    ),
  )
  ],
),

//itemBuilder单独写的方法,返回值是List >
List > _PopMenuItemBuild(BuildContext context){
    return >[
      _CreatPopMenuBuildItem('images/发起群聊.png', '发起群聊'),
      _CreatPopMenuBuildItem('images/添加朋友.png', '添加朋友'),
      _CreatPopMenuBuildItem('images/扫一扫1.png', '扫一扫'),
      _CreatPopMenuBuildItem('images/收付款.png', '收付款'),
    ];
}

//创建每一个item分出来的公共方法,传入一个imageName和一个title返回一个PopupMenuItem
PopupMenuItem _CreatPopMenuBuildItem(String imageName, String title){
  return PopupMenuItem(child: Row(
    children: [
      Image(image: AssetImage(imageName),width: 25,),
      SizedBox(width: 20),
      Text(title,style: TextStyle(color: Colors.white),),
    ],
  ),);
}

在flutter中,对于右上角的按钮,以及点击弹出的弹窗,系统提供了一个按钮:

/// See also:
///
///  * [PopupMenuItem], a popup menu entry for a single value.
///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
class PopupMenuButton extends StatefulWidget {
  /// Creates a button that shows a popup menu.
  ///
  /// The [itemBuilder] argument must not be null.
  const PopupMenuButton({
    Key key,
    @required this.itemBuilder,
    this.initialValue,
    this.onSelected,
    this.onCanceled,
    this.tooltip,
    this.elevation,
    this.padding = const EdgeInsets.all(8.0),
    this.child,
    this.icon,
    this.offset = Offset.zero,
    this.enabled = true,
    this.shape,
    this.color,
    this.captureInheritedThemes = true,
  }) : assert(itemBuilder != null),
  • 对于PopupMenuButton, itemBuilder是required,不能为空,返回的是一个List,点击的时候直接创建一个菜单view;
  • offset: Offset(0, 60),menuView相对于+按钮的偏移量,xy方向,默认是0,紧挨着+号按钮;

3 网络数据的请求

通过创建的网络假数据接口可以请求到网络数据,网络请求到的数据的可是是Json格式的,需要转换成Map,然后转换成对应的模型.用于ListView的创建;

  • 请求网络数据需要去官网导入支持http的package,导入http的步骤
  • 通过http.get方法直接请求链接url,使用asyncawait配合使用异步请求数据;
  • 数组的遍历直接使用.map((item){}),强转成List可以使用toList()方法;
  • 需要添加一个判断response.stateCode,如果不是请求成功的code,则抛出一个异常;
  • 添加一个bool值_cancelConnect来对超时,多次重复请求容错;
  • 网络请求之后使用then用来处理将来Future异步网络请求回来的数据,后面跟上catchError会回调失败的原因;
  • 加载完毕回调whenComplete(成功和失败都会调这个回调);
  • 网络请求超时回调timeout,后面跟上catchError是返回超时的错误原因
class _WeChatPageState extends State with AutomaticKeepAliveClientMixin {
  List _datas = [];
  bool _cancelConnect = false;

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    getDatas().then((List datas) {
      if(!_cancelConnect){
        setState(() {
          _datas = datas;
        });
      }
    }).catchError((error) {
      print(error);
    }).whenComplete(() {
      print("完毕");
    }).timeout(Duration(seconds: 10)).catchError(
        (timeOutError){
          _cancelConnect = true;
          print('${timeOutError}');
        }
    );
//    print('来了');
  }

  Future> getDatas() async {
    _cancelConnect = false;
    final response = await http
        .get('http://rap2.taobao.org:38080/app/mock/245766/api/chat/list');
    if (response.statusCode == 200) {
      var respones_Body = json.decode(response.body);

      List chat_list = respones_Body['chat_list']
          .map((item) => Chat.formJson(item))
          .toList();
      return chat_list;
    } else {
      throw Exception('stateCode=${response.statusCode}');
    }
    print(response.statusCode);
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: Text('微信'),
          actions: [
            Container(
              margin: EdgeInsets.only(right: 15),
              child: PopupMenuButton(
                offset: Offset(0, 60),
                child: Image(
                  image: AssetImage('images/圆加.png'),
                  width: 25,
                ),
                itemBuilder: _PopMenuItemBuild,
              ),
            )
          ],
        ),
        body: Container(
          child: _datas.length == 0
              ? Center(
                  child: Text("loading"),
                )
              : ListView.builder(
                  itemCount: _datas.length,
                  itemBuilder: (BuildContext context, int index) {
                    return ListTile(
                      leading: Container(
                        height: 45,
                        width: 45,
                        decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(8),
                            image: DecorationImage(
                                fit: BoxFit.fitHeight,
                                image: NetworkImage(_datas[index].imageUrl))),
                      ),
                      title: Text(_datas[index].name),
                      subtitle: Container(child: Text(_datas[index].message),height: 20,),
                    );
                  }),
        ));
  }
}


3.1 Chat模型类中的实现代码:

里面有用到一个factory工厂方法来实现jsonMap转模型,在外部可以直接调用这个工厂方法传入有一个Map返回一个Chat模型.

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

  Chat({this.name, this.message,this.imageUrl});
  factory Chat.formJson(Map json) {
    return Chat(
      name: json['user_name'],
      message: json['message'],
      imageUrl: json['image_url'],
    );
  }
}

3.2 Dart中Json和Map的互转

首先需要导入头文件;
示例代码如下:

  • 将Map转换为Json使用json.encode();
  • 将Json转换为Map使用json.decode();
import 'dart:convert';

_MapAndJson() {
  var TestMap = {
    'name': 'Jack',
    'message': 'today is nice',
  };

  //map转json使用encode
  var mapJson = json.encode(TestMap);
  print(mapJson);

  //json转map使用decode
  var jsonMap = json.decode(mapJson);
  print(jsonMap);
}

4 保持部件的状态MixIn和PageViewController

为了不每次进这个页面都刷新页面的状态,需要保持这个页面的状态,这时候需要用到dart中的混合MixIn,类似于iOS中的分类,就是可以用其他class的方法,需要添加如下操作:

  • 使用MixIn,加上with AutomaticKeepAliveClientMixin
  • 需要重写wantKeepAlive方法
  • 需要在Widget build方法中加入super.build(context)
class _WeChatPageState extends State with AutomaticKeepAliveClientMixin {
 
  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
  
   @override
  Widget build(BuildContext context) {
    super.build(context);

4.1 PageViewController

但是在page.dart中需改了以后,发现点击还是会去跟新,原因是在rootpage中,body返回的是每个创建的page,其他的page会被销毁,会重新渲染,所以保持部件的状态没有效果,所以在rootPage中需要更改控制body的方式,使用PageViewController专门来控制tabbarpage的切换.

class PageController extends ScrollController {
  /// Creates a page controller.
  ///
  /// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null.
  PageController({
    this.initialPage = 0,
    this.keepPage = true,
    this.viewportFraction = 1.0,
  }) : assert(initialPage != null),
       assert(keepPage != null),
       assert(viewportFraction != null),
       assert(viewportFraction > 0.0);

  /// The page to show when first creating the [PageView].
  final int initialPage;

  /// Save the current [page] with [PageStorage] and restore it if
  /// this controller's scrollable is recreated.
  ///
  /// If this property is set to false, the current [page] is never saved
  /// and [initialPage] is always used to initialize the scroll offset.
  /// If true (the default), the initial page is used the first time the
  /// controller's scrollable is created, since there's isn't a page to
  /// restore yet. Subsequently the saved page is restored and
  /// [initialPage] is ignored.
  ///
  /// See also:
  ///
  ///  * [PageStorageKey], which should be used when more than one
  ///    scrollable appears in the same route, to distinguish the [PageStorage]
  ///    locations used to save scroll offsets.
  final bool keepPage;


PageViewController的使用需要先创建一个实例对象,initialPage=0是选中的第0个page;然后在root_Page中body传入PageView,PageView的controller传入之前创建好的PageViewController对象;

class _RootPageState extends State {
  int _cuttentIndex = 0;
  final PageController _pageController = PageController(
    initialPage: 0,
  );

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Scaffold(
        bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          fixedColor: Colors.greenAccent,
          currentIndex: _cuttentIndex,
          selectedFontSize: 12.0,
          items: [
            BottomNavigationBarItem(
                icon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_chat.png')),
                activeIcon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_chat_hl.png')),
                title: Text('微信')),
            BottomNavigationBarItem(
                icon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_friends.png')),
                activeIcon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_friends_hl.png')),
                title: Text('通讯录')),
            BottomNavigationBarItem(
                icon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_discover.png')),
                activeIcon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_discover_hl.png')),
                title: Text('发现')),
            BottomNavigationBarItem(
                icon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_mine.png')),
                activeIcon: Image(
                    height: 20,
                    width: 20,
                    image: AssetImage('images/tabbar_mine_hl.png')),
                title: Text('我的')),
          ],
          onTap: (int index) {
            _cuttentIndex = index;
            setState(() {});
            _pageController.jumpToPage(index);
          },
        ),
        body: PageView(
          controller: _pageController,
          onPageChanged: (int index){//tabbar页面滚动回调,更改index刷新tabbar选中的item
            _cuttentIndex = index;
            setState(() {});
          },
//        physics: NeverScrollableScrollPhysics(),//禁止tabbar页面滚动
          children: [
            WeChatPage(),
            FriendsPage(),
            FindPage(),
            MinePage()
          ],
        ),
      ),
    );
  }
}


和PageViewController配合使用的有一个PageView,

  • controller:_pageController,使用创建好的PageViewController来控制tabbar的四个控制器
  • onPageChanged回调,选中某个page时的回调,需要更改_currentIndex(当前选中的page),用于刷新tabbar上选中的item;
  • physics: NeverScrollableScrollPhysics(),禁止tabbar手势左右滑动切换页面
  • children:传入子page.

5 总结

效果:这次的界面搭建出来的实现效果比较粗糙,主要是为了学习网络请求的逻辑流程,Json和模型的转换,理解弄清楚了这些界面的搭建细节可以修改.
Flutter项目(4)之仿微信聊天界面_第1张图片

网络请求是学会Flutter很关键的一点,熟练的运用网络请求和处理数据,那么项目困难已经解决了的一大半.
原文链接

你可能感兴趣的:(Flutter项目(4)之仿微信聊天界面)