Flutter入门六:【聊天】+ 网络请求 + 状态保留

Flutter入门 学习大纲

  1. Mock网络数据
  2. 网络请求
  3. 聊天页 (状态保留)

相关资源链接

  1. Mock数据地址http://rap2.taobao.org/account/login
  2. 随机头像地址https://randomuser.me/photos
  3. mockjs规则示例http://mockjs.com/examples.html
  4. dart包的公共库https://pub.dev/

1. Mock网络数据

  • 使用阿里妈妈团队开发的RAP接口管理平台进行接口数据的创建,模拟微信好友信息

  • 点击进入 👉 Mock登录页,使用邮箱注册登录

    image.png

  • 新建Flutter入门仓库:

    image.png

  • 点击进入仓库新建接口,设置Url地址、名称、类型、状态码

    image.png

  • 编辑聊天接口

    image.png

  • 新增chat_list数组,50个元素,每个元素包含imageUrl头像name名称message消息

生成规则,可以参考👉 mock.js实例

@natural: 随机生成数字
@name:随机生成英文名字
@cname:随机生成中文名字
@cparagraph:随机生成一段文字

image.png

头像链接从👉随机用户网站临时获取

image.png

  • 拷贝地址https://randomuser.me/api/portraits/women/85.jpg,将最后的85编号修改为随机值
    https://randomuser.me/api/portraits/women/@natural(20,70).jpg
    image.png
  • 获取接口内容

    image.png

  • 接口地址为http://rap2api.taobao.org/app/mock/277621/api/chat/list,每次访问,随机生成50条数据。

    image.png

2. 网络请求

  • 官方提供了http的公共网络请求包,在👉dart公共库可以搜索到http及其使用方式
image.png
image.png

2.1 导入http网络请求包

  • 拷贝http版本,在项目的pubspec.yaml配置文件中,dependencies一栏新增http包引用
image.png
  • 获取所有包:点击Pub get终端手动输入flutter pub get,都可以获取到。
image.png

2.2 常规网络请求

  • 创建Chat 聊天模型,使用factory创建工厂化方法,将Map对象转换为Chat对象
class Chat {
  final String name;     // 名称
  final String message;  // 消息
  final String imageUrl; // 头像链接

  Chat({this.name, this.message, this.imageUrl});

  // 工厂方法,Map对象转Chat对象
  factory Chat.fromJson(Map json) {
    return Chat(name: json["name"],
                message: json["message"],
                imageUrl: json["imageUrl"]);
  }
}
  • 使用http请求获取数据
  // Future表示可能存在错误,记录正在执行的状态
  // async异步请求,配合await使用
  Future<List<Chat>> getDatas() async {
    // 发起请求并等待结果
    final response = await http.get(
        'http://rap2api.taobao.org/app/mock/277621/api/chat/list');

    // 状态码不为200,抛出错误
    if (response.statusCode != 200) {
      throw Exception('statusCode: ${response.statusCode}');
    }

    // response.body是json数据,json转map再转Model
    final responseBody = json.decode(response.body); // Map结构
    List<Chat> chatList = responseBody['chat_list'].map<Chat>((item) =>
        Chat.fromJson(item)).toList(); // 将列表元素都转换为Chat类型
    // print(chatList.map((item) => print(item.name)));
    return chatList;
  }
  1. 使用Future类,记录执行状态返回内容错误,供外部处理
  2. 网络请求使用异步任务,用async修饰,必须配合await等待网络结果
  3. 网络请求的错误情况,使用throw抛出Exception错误信息。
  4. 接口返回的数据json格式,使用json.decode转化成Map结构,再转换成对应Model类型。

2.3 数据的使用

  • 数据的使用,介绍两种方法:
  1. 常规方式:使用变量进行承接,通过setState()刷新部件
  2. 快捷方式:使用FutureBuilder部件,在future属性中设置异步请求函数,在builder中读取AsyncSnapshot 异步的数据结果并返回构建的部件

2.3.1 常规方式

  • 假设我们在initState中调用getDatas请求数据,对于Future类型的数据,可以有两种方法进行处理:
  • 方法一: 使用try catch处理正常请求结果error错误

  • 方法二: 使用Future提供的各种状态链式处理:

    • then:获取正确的内容
    • catchError:捕获异常
    • timeout:设置超时时间
    • whenComplete:捕获结束状态

    常识:
    接口请求超时仅代表超出客户端的timeout请求时限,并不代表取消了请求。依旧会收到服务端的返回结果,我们一般通过Bool值记录是否需要处理返回结果(超时或手动取消,不处理结果)

    • 错误示例(已超时仍接收了返回结果):
      image.png
  • 正确做法,下面代码中使用_cancelConnect进行状态记录,觉得是否接收返回结果

 @override
  void initState() {
    super.initState();

    // 方法一: try catch
    try { getDatas(); } catch(error) { print(error); }

    // 方法二: Future状态链式处理
    bool _cancelConnect = false;

    getDatas()
    // then 获取正确内容
    .then((value) {
      // 已取消,不接受数据
      if (_cancelConnect) return;
      /* 此处使用【变量】接受【value】值,配合【setState】重新【构建部件】即可 */
      value.forEach((e) => print(e.name)); 
    })
    // 捕获错误
    .catchError((error) => print("错误$error"))
    // 设置超时时间,捕获超时错误
    .timeout(Duration(milliseconds: 100)).catchError((timeout) {
      _cancelConnect = true;
      print("超时${timeout}");
    })
    // 结束
    .whenComplete(() => print("完毕"));
  }

2.3.2 快捷方式

  • 使用FutureBuilder异步部件,直接绑定异步数据部件
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';

import 'package:http/http.dart' as http; //导入http库,取别名为http

import 'chat.dart';

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

class _ChatPageState extends State<ChatPage> {

  // Future表示可能存在错误,记录正在执行的状态
  // async异步请求,配合await使用
  Future<List<Chat>> getDatas() async {
    // 发起请求并等待结果
    final response = await http
        .get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');

    // 状态码不为200,抛出错误
    if (response.statusCode != 200) {
      throw Exception('statusCode: ${response.statusCode}');
    }

    // response.body是json数据,json转map再转Model
    final responseBody = json.decode(response.body); // Map结构
    List<Chat> chatList = responseBody['chat_list']
        .map<Chat>((item) => Chat.fromJson(item))
        .toList(); // 将列表元素都转换为Chat类型
    // print(chatList.map((item) => print(item.name)));
    return chatList;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Wechat_themeColor,
        elevation: 0.0,
        centerTitle: true,
        title: Text("聊天"),
      ),
      // FutureBuilder 异步部件
      body: FutureBuilder(
        future: getDatas(),    // future: 异步请求
        builder: (BuildContext context, AsyncSnapshot snapshot) {  // builder: 获取异步数据并返回部件
          print(snapshot.data);            // 数据
          print(snapshot.connectionState); // 状态
          return Container();
        },
      ),
    );
  }
}
  • 查看打印结果,可以看到数据null的情况,状态也看到了waitting等待和done完成两种:

    image.png

  • 查看ConnectionState,是枚举类型,包含4种情况

    image.png

  • 我们可以根据不同数据状态展示不同的视图信息

body: FutureBuilder(
        future: getDatas(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.none:    // 无
              return Container();
            case ConnectionState.waiting:  // 加载中
              return Center(child:Text("加载中"));
            case ConnectionState.active:  // 连接中
              return Center(child:Text("连接中"));
            case ConnectionState.done:  // 已完成(根据数据展示页面)
              print("数据: ${snapshot.data}");
              return Center(child:Text("正常显示"));
            default:
              return Container();
          }
        },
      )
  • 以上,就是网络请求基本使用方式。下面进行聊天页开发

3. 聊天页

  1. 开发导航栏气泡弹框
  2. 根据网络数据,构建聊天Cell

3.1 导航栏Popup气泡

  • 使用Flutter提供的PopupMenuButton实现导航栏按钮气泡弹框:

    image.png

  • chat_page页面代码:

import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';

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

class _ChatPageState extends State<ChatPage> {
  // 气泡视图
  List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
    return <PopupMenuItem<String>>[
      _buildItem("发起群聊", "发起群聊"),
      _buildItem("添加朋友", "添加朋友"),
      _buildItem("扫一扫1", "扫一扫"),
      _buildItem("收付款", "收付款")
    ];
  }

  // 气泡元素
  PopupMenuItem<String> _buildItem(String assetImage, String name) {
    return PopupMenuItem(
        child: Row(children: [
      Image(image: AssetImage("images/${assetImage}.png"), width: 20),
      SizedBox(width: 20),
      Text(name, style: TextStyle(color: Colors.white))
    ]));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Wechat_themeColor,
        elevation: 0.0,
        // 隐藏分割线
        centerTitle: true,
        // 安卓的导航栏标题未居中,可以设置居中
        title: Text("聊天"),
        actions: <Widget>[
          Container(
              margin: EdgeInsets.only(right: 20),
              child: PopupMenuButton(
                  // 偏移值 kToolbarHeight: 导航栏高度
                  offset: Offset(0, kToolbarHeight),
                  // 图标
                  child: Container(
                    margin: EdgeInsets.only(right: 10),
                    child: Image(image: AssetImage("images/圆加.png"), width: 20),
                  ),
                  // 气泡弹框
                  itemBuilder: _buildPopupMenuItem)),
        ],
      ),
      body: Center(child: Text("聊天页面")),
    );
  }
}

注意

  1. PopupMenuButtonchild设置按钮内容itemBuilder设置弹框内容,必须是内部元素PopupMenuEntry<T>List数组。(PopupMenuItem继承自PopupMenuEntry

  2. 气泡弹框背景色,需要更改主题背景色cardColor:
    main.dart中关于主题色的代码:

import 'package:flutter/material.dart';
import 'package:wechat_demo/pages/root_page.dart';

void main() => runApp(App());

class App extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Wechat Demo', // 安卓需要,后台切换app时展示的名称(iOS中名称与APP名称一致)
     debugShowCheckedModeBanner: false, // 隐藏debug角标
     home: RootPage(),
     theme: ThemeData(
       primaryColor: Colors.white, // 主题色
       highlightColor: Color.fromRGBO(0, 0, 0, 0), // 去除高亮色
       splashColor: Color.fromRGBO(0, 0, 0, 0), // 去除水波纹
       cardColor: Color.fromRGBO(1, 1, 1, 0.5) // 弹出卡片背景色
     ),
   );
 }
}

3.2 请求网络,构建聊天视图

  • 当前使用FutureBuildersnapshotwaiting等待时,页面展示loading,其余状态展示MessageCell
  • ListTile类似于iOS的默认UITableViewCell,有title主标题、subtitle子标题、leading首部部件(本案例放图片)

  • 圆角头像:

Container( width: 40, height: 40, 
           decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0), 
                                      image: DecorationImage(image: NetworkImage(item.imageUrl))))
  • 圆形头像:
CircleAvatar(backgroundImage: NetworkImage(item.imageUrl))
  • chat_page代码:
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';

import 'package:http/http.dart' as http; //导入http库,取别名为http

import 'chat.dart';

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

class _ChatPageState extends State<ChatPage> {
  // Future表示可能存在错误,记录正在执行的状态
  // async异步请求,配合await使用
  Future<List<Chat>> getDatas() async {
    // 发起请求并等待结果
    final response = await http
        .get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');

    // 状态码不为200,抛出错误
    if (response.statusCode != 200) {
      throw Exception('statusCode: ${response.statusCode}');
    }

    // response.body是json数据,json转map再转Model
    final responseBody = json.decode(response.body); // Map结构
    List<Chat> chatList = responseBody['chat_list']
        .map<Chat>((item) => Chat.fromJson(item))
        .toList(); // 将列表元素都转换为Chat类型
    // print(chatList.map((item) => print(item.name)));
    return chatList;
  }

  // 气泡视图
  List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
    return <PopupMenuItem<String>>[
      _buildItem("发起群聊", "发起群聊"),
      _buildItem("添加朋友", "添加朋友"),
      _buildItem("扫一扫1", "扫一扫"),
      _buildItem("收付款", "收付款")
    ];
  }

  // 气泡元素
  PopupMenuItem<String> _buildItem(String assetImage, String name) {
    return PopupMenuItem(
        child: Row(children: [
      Image(image: AssetImage("images/${assetImage}.png"), width: 20),
      SizedBox(width: 20),
      Text(name, style: TextStyle(color: Colors.white))
    ]));
  }

  // 消息Cell
  Widget _buildMessageCell(Chat item) {
    return Container(child: Column(children: [
      ListTile(
        title: Text(item.name),
        subtitle: Container(height: 20, child: Text(item.message, overflow: TextOverflow.ellipsis)),
        leading: Container( 
            width: 40,  height: 40,
            decoration: BoxDecoration( // 圆角
                borderRadius: BorderRadius.circular(12.0),
                image: DecorationImage(image: NetworkImage(item.imageUrl)))),
      ),
      Container(margin: EdgeInsets.only(left: 74), height: 1, color: Wechat_themeColor)
    ]));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Wechat_themeColor,
        elevation: 0.0,// 隐藏分割线
        centerTitle: true, // 安卓导航栏居中
        title: Text("聊天"),
        actions: <Widget>[
          Container(
              margin: EdgeInsets.only(right: 20),
              child: PopupMenuButton(
                  offset: Offset(0, kToolbarHeight),// 偏移值 kToolbarHeight: 导航栏高度
                  child: Container( // 图标
                    margin: EdgeInsets.only(right: 10),
                    child: Image(image: AssetImage("images/圆加.png"), width: 20),
                  ),
                  itemBuilder: _buildPopupMenuItem)),// 气泡弹框
        ],
      ),
      body: FutureBuilder(
        future: getDatas(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: Text("Loading..."));  // loading
          }
          return ListView(   // 正常展示
              children: snapshot.data
                  .map<Widget>((item) => _buildMessageCell(item))
                  .toList());
        },
      ),
    );
  }
}
  • 展示样式:


    image.png

使用FutureBuilder部件构建,每次进入页面都会触发网络请求刷新页面,适用于简单页面
当前模块,我们需要保留页面状态,不适合使用FutureBuilder,可使用变量(List<Chat>)记录返回值,将数据存储内存中,保留页面状态

  1. 使用上面介绍常规方式,在initState中调用getDatas,用变量_datas记录返回值,通过setState重新构建部件
    getDatas中使用_cancelConnect记录了请求是否取消,默认false每次请求都会重置falsetimeout超时会设置为truethen数据返回时,只有_cancelConnecttrue才会更新数据setState刷新页面。

  2. 通过_datas数据构建body。通过_datas.length区分loading正常数据部件的展示

  3. 使用Mixins(混入)保活当前页面

Mixins(混入):

  • 类似iOS中的Category分类,用来给类增加功能, 使用【with混入一个或多个mixin(实现多继承的关系)

  • 执行方法:

【第一步】:state类使用with继承AutomaticKeepAliveClientMixin。例如:

class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage> { ... }

【第二步】:重写wantKeepAlive计算属性。例如:

 @override
 bool get wantKeepAlive => true; 

【第三步】:执行父类build。例如:

  Widget build(BuildContext context) {
   super.build(context); 
   ... 
  }
  • 修改后的chat_page.dart代码如下:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:wechat_demo/const.dart';
import 'package:http/http.dart' as http; //导入http库,取别名为http
import 'chat.dart';

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

class _ChatPageState extends State<ChatPage>
with AutomaticKeepAliveClientMixin<ChatPage> {  // 第一步,混入(多继承)

  @override
  bool get wantKeepAlive => true; // 第二步,重写wantKeepAlive计算属性
  // 存储模型数据
  List<Chat> _datas = [];
  // 记录连接状态
  bool _cancelConnect = false;

  @override
  initState() {
    super.initState();

    getDatas()
        .then((value) {
          if (_cancelConnect) return; // 已取消连接,不更新数据
          _datas = value;
          setState(() {});
        })
        .catchError((e) => print(e)) // 错误
        .whenComplete(() => print("完毕")) //完毕
        .timeout(Duration(seconds: 6)).catchError((timeout) {
          _cancelConnect = true; // 超时取消连接
          print("超时$timeout");
        });
  }

  // Future 记录结果, async异步请求,配合await使用
  Future<List<Chat>> getDatas() async {
    // 每次调用请求,设为false(保证每次主动请求都可执行)
    _cancelConnect = false;
    // 发起请求并等待结果
    final response = await http
        .get('http://rap2api.taobao.org/app/mock/277621/api/chat/list');
    // 状态码不为200,抛出错误
    if (response.statusCode != 200) {
      throw Exception('statusCode: ${response.statusCode}');
    }

    // response.body是json数据,json转map再转Model
    final responseBody = json.decode(response.body); // Map结构
    List<Chat> chatList = responseBody['chat_list']
        .map<Chat>((item) => Chat.fromJson(item))
        .toList(); // 将列表元素都转换为Chat类型
    return chatList;
  }

  // 气泡视图
  List<PopupMenuItem<String>> _buildPopupMenuItem(BuildContext context) {
    return <PopupMenuItem<String>>[
      _buildItem("发起群聊", "发起群聊"),
      _buildItem("添加朋友", "添加朋友"),
      _buildItem("扫一扫1", "扫一扫"),
      _buildItem("收付款", "收付款")
    ];
  }

  // 气泡元素
  PopupMenuItem<String> _buildItem(String assetImage, String name) {
    return PopupMenuItem(
        child: Row(children: [
      Image(image: AssetImage("images/${assetImage}.png"), width: 20),
      SizedBox(width: 20),
      Text(name, style: TextStyle(color: Colors.white))
    ]));
  }

  // 构建Body
  Widget _buildBody() {
    if (_datas.length == 0) return Center(child: Text("Loading..."));
    return ListView.builder(
        itemCount: _datas.length, itemBuilder: _buildCellOfRow);
  }

  // 构建Cell
  Widget _buildCellOfRow(BuildContext context, int index) {
    final item = _datas[index];
    return Container(
        child: Column(children: [
      ListTile(
        title: Text(item.name),
        subtitle: Container(
            height: 20,
            child: Text(item.message, overflow: TextOverflow.ellipsis)),
        leading: Container(
            width: 40,
            height: 40,
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(12.0),
                image: DecorationImage(image: NetworkImage(item.imageUrl)))),
      ),
      Container(
          margin: EdgeInsets.only(left: 74),
          height: 1,
          color: Wechat_themeColor)
    ]));
  }

  @override
  Widget build(BuildContext context) {
    super.build(context); // 第三步,执行父类build
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Wechat_themeColor,
          elevation: 0.0,
          centerTitle: true, 
          title: Text("聊天"),
          actions: <Widget>[
            Container(
                margin: EdgeInsets.only(right: 20),
                child: PopupMenuButton(
                    offset: Offset(0, kToolbarHeight), // 偏移值 kToolbarHeight: 导航栏高度
                    child: Container(// 图标
                      margin: EdgeInsets.only(right: 10),
                      child:
                          Image(image: AssetImage("images/圆加.png"), width: 20),
                    ),
                    itemBuilder: _buildPopupMenuItem)),// 气泡弹框
          ],
        ),
        body: _buildBody()); // 构建Body
  }
}

app运行后,发现切换tab仍然会重新请求数据刷新页面

  • 这是因为main.dart中,我们每次切换tab,都是返回的bodys[index],每次都重新生成页面,所以无法保留状态
    (如果需要保留状态,页面必须存在渲染树中)

    image.png

  • 我们将main.dartbody改为PageView部件,使用_pageController(PageController类型)记录当前PageView,设置children为四个tab页面,将physics设置为NeverScrollableScrollPhysics()进制左右滑动。

  • 点击Tab时,我们通过_pageController执行jumpToPage切换到指定tab

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:wechat_demo/pages/chat/chat_page.dart';
import 'package:wechat_demo/pages/discover/discover_page.dart';
import 'package:wechat_demo/pages/friends/friends_page.dart';
import 'package:wechat_demo/pages/mine/mine_page.dart';

class RootPage extends StatefulWidget {
  @override
  _RootPageState createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {

  PageController _pageController = PageController(); // 记录当前PageView控制器

  // 点击Tabbar
  Widget onTap(int index) {
    _currentIndex = index;
    setState(() { });
    _pageController.jumpToPage(index); // 调到指定tab页面
  }

  // 每个栏目的主页面
  final List<Widget> bodys = [ChatPage(), FriendsPage(), DiscoverPage(), MinePage()];

  // 每个栏目的底部Item
  final List<BottomNavigationBarItem> items = [
    BottomNavigationBarItem(
        icon: Image(image: AssetImage('images/tabbar_chat.png'), width: 20),
        activeIcon:
            Image(image: AssetImage('images/tabbar_chat_hl.png'), width: 20),
        label: "聊天"),
    BottomNavigationBarItem(
        icon: Image(image: AssetImage('images/tabbar_friends.png'), width: 20),
        activeIcon:
            Image(image: AssetImage('images/tabbar_friends_hl.png'), width: 20),
        label: "通讯录"),
    BottomNavigationBarItem(
        icon: Image(image: AssetImage('images/tabbar_discover.png'), width: 20),
        activeIcon: Image(
            image: AssetImage('images/tabbar_discover_hl.png'), width: 20),
        label: "朋友圈"),
    BottomNavigationBarItem(
        icon: Image(image: AssetImage('images/tabbar_mine.png'), width: 20),
        activeIcon:
            Image(image: AssetImage('images/tabbar_mine_hl.png'), width: 20),
        label: "我的")
  ];

  // 当前选中Index
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blue,
      body: PageView(
        controller: _pageController, // 记录当前PageView,便于点击tab时,控制跳转
        physics: NeverScrollableScrollPhysics(), // 禁止左右滑动页面 默认AlwaysScrollableScrollPhysics可滚动
        children: bodys,
        // 左右滚动页面
        // onPageChanged: (index) {
        //   _currentIndex = index;
        //   setState(() { });
        // },
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        // 固定大小,避免白色背景
        fixedColor: Colors.green,
        // 固定颜色
        currentIndex: _currentIndex,
        // 选择的默认值
        items: items,
        onTap: onTap,
        // 点击回调
        selectedFontSize: 12, // 选择字体大小设置为12(因为默认大小是12,这样可以去掉变大动画)
        // selectedLabelStyle: ,
      ),
    );
  }
}
  • 至此,切换tab时,页面保持原先滚动位置,保持了原有状态

总结
本节,我们掌握了:

  1. 【接口Mock】借助RAP接口管理平台Mock接口数据;
  2. 【网络请求】http网络请求库的使用,通过Future获取多状态结果以及try catch链式处理的方法,asyncawait的组合使用,使用_cancelConnect隔离数据和重复请求
    2.1 使用变量内存存储异步数据setState重新构建部件
    2.2 FutureBuilder每次都会异步请求数据重构部件
  3. 【气泡弹框】气泡按钮弹框PopupMenuButton的使用
  4. 【状态保留】 state类通过with继承AutomaticKeepAliveClientMixin,重写wantKeepAlive,执行父类build
  5. 【PageView】多页视图的使用(同时保活多个页面部件)。

下一节,完善聊天搜索框搜索页面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容