美文网首页
实践 Flutter 交友功能-网络缓存

实践 Flutter 交友功能-网络缓存

作者: 孽_a9d9 | 来源:发表于2020-10-19 11:09 被阅读0次

    本课时我会和大家一起来完善 App 的其他功能,其中包括:我的好友、我的消息、系统设置和搜索功能。按照我们之前课时所学的技术点,我们可以通过绘制组件树+布局来实现,在实现过程中也会介绍一些新的知识点,接下来我们就分别看下这几个功能的实现过程。

    我的好友

    我们首先看下要实现的效果图,如图 1 所示。

    图 1 我的好友效果图

    根据图 1 的效果图,我们绘制出组件树+布局,如图 2 所示。

    图 2 组件树

    图 2 很清晰地分析出了界面所转化的组件树,由于这里都不涉及动态组件,因此将 Text 和 Image 作为一个 card 组件即可。代码实现逻辑和我们之前介绍的推荐页面和关注页面基本一样,接下来我们看下“我的消息”的实现。

    我的消息

    我们先来看下“我的消息”要实现的界面效果,如图 3 所示。

    图 3 我的消息界面效果

    根据图 3 的效果图,我们绘制出组件树+布局,如图 4 所示。

    图 4 我的消息组件树+布局

    图 4 就非常清晰地描述了我们整个 UI 构造:

    图 4 中的 Row-Expanded-1 和 Row-Expanded-5 代表的是使用 flex 布局,左右屏幕占比 1 比 5;

    图 4 中的 first_line 代表的是图 3 右侧的用户昵称和时间一行;

    图 4 中的 spaceBetween 是 Row 的 mainAxisAlignment 属性,代表的是两端对齐,具体这部分代码如下。

    复制代码

    /// 获取右侧的首行

    Widget _getFirstLine() {

      return  Row(

        mainAxisAlignment: MainAxisAlignment.spaceBetween,

        children: <Widget>[

          Text(

            userMessage.userInfo.nickName,

            style: TextStyles.commonStyle(0.8, Colors.black),

          ),

          _getMessageTimeSection(userMessage.messageTime),

        ],

      );

    }

    由于这里也没有涉及组件的复用和动态组件,因此这里也建议将整个组件内容设计为一个组件叫作 message_card。为了代码维护性,可以使用类函数来封装小组件,为后续重构抽象为通用组件做准备,例如这里我们将 first_line 设计为一个类函数,如上代码中的 _getFirstLine 函数。

    系统设置

    接下来我们来看下“系统设置”这部分界面效果,如图 5 所示。

    图 5 系统设置的效果

    看到图 5 的效果后,其实组件设计可能不是关键。这里涉及两个新的知识点:

    在 Flutter 上怎么处理表单数据;

    怎么保存系统设置的数据。

    这里具体的组件树+布局就不绘制了,我们可以将实现过程分为四部分:第三方库引入、通用文件存储、model 应用和组件应用。

    第三方库

    这里我们需要使用到 Flutter 本地存储功能,Flutter 本地存储功能包含三种:shared_preferences、path_provider 文件存储以及 sqflite。这里只介绍 path_provider 文件存储的实现,其他两个大家参照官网的介绍尝试即可。使用该 path_provider 库需要在 pubspec.yaml 中增加库引入,然后更新本地库。

    通用文件存储

    接下来我们基于 path_provider 实现一个通用的文件内容存储,代码在 github 源码中的 util/tools/local_storage.dart 中。这里我们主要需要实现两个方法,一个是文件储存内容,一个文件读取内容。

    文件存储

    我们先来看下文件存储的逻辑,如下代码:

    复制代码

    /// 将数据保存到文件中

    static Future<void> save(String content, String filePath) async {

      final directory = await getApplicationDocumentsDirectory();

      File file = new File('${directory.path}/$filePath');

      file.writeAsString(content);

    }

    因为是异步获取文件存储路径,因此 save 方法也需要作为异步逻辑,由于无须等待处理结果,因此返回 void。上面代码中使用了 path_provider 的 getApplicationDocumentsDirectory 的方法获取文件存储目录,使用 dart:io 获取具体文件的操作句柄,最后将内容写入文件,接下来我们看下读取的过程。

    文件读取

    读取的过程和写的代码相似,首先是找到文件并获取文件操作句柄,然后再使用文件句柄读取文件具体内容,代码如下:

    复制代码

    /// 获取文件数据内容

    static Future<String> get(String filePath) async {

      try {

        final directory = await getApplicationDocumentsDirectory();

        File file = new File('${directory.path}/$filePath');

        bool exist = await file.exists();

        if(!exist){ // 判断是否存在文件

          return '';

        }

        return file.readAsString();

      } catch(e) {

        return '';

      }

    }

    上面代码增加了一个异常处理,避免读取失败返回错误数据,因此如果这里判断异常,则返回空字符串。在 catch 逻辑中是需要增加上报来监控告警的,后续我们再来介绍这部分内容。

    model 应用

    因为系统配置是一个全局状态,需要在多个页面使用,所以我们需要将系统数据保存在 model 中,因此我们在 model 创建

    system_config_model.dart 文件。在实现逻辑中,需要先调用 LocalStorage 来获取初始配置,代码如下:

    复制代码

    /// 构造函数

    SystemConfigModel.init(){

      LocalStorage.get('tyfapp.system.config').then((configStr){

        Map<String, dynamic> configInfo = {};

        if(configStr == null || configStr == '') { // 判断合法性

          configInfo = {

            "accessMessage" : true,

            "tipsDetail" : true,

            "soundReminder" : true,

            "vibrationReminder" : true

          };

        } else {

          try { // 尝试 json 解析,解析失败直接返回

            configInfo =

            json.decode(configStr) as Map<String, dynamic>;

          } catch(e){

            return;

          }

        }

        systemConfig = StructSystemConfig.fromJson(configInfo);

      });

    }

    上面代码 init 为构造函数,其中第 3 行是异步读取文件,获取到文件后存储在共享状态变量 systemConfig 中。为了异常考虑,如果没有获取到文件内容,则将共享状态变量默认设置打开状态,也就是 true 值。有了初始化部分,再修改 main.dart 增加一个新的状态共享,部分如下代码:

    复制代码

    // 初始化共享状态对象

    LikeNumModel likeNumModel = LikeNumModel();

    NewMessageModel newMessageNum = NewMessageModel(newMessageNum: 0);

    // 异步数据处理

    ApiUserInfoMessage.getUnReadMessageNum(newMessageNum);

    // 异步获取系统配置

    SystemConfigModel systemConfigModel = SystemConfigModel.init();

    return MultiProvider(

      providers: [

        ChangeNotifierProvider(create: (context) => likeNumModel),

        ChangeNotifierProvider(

            create: (context) => UserInfoModel(myUserInfo: myUserInfo)),

        ChangeNotifierProvider(create: (context) => newMessageNum),

        ChangeNotifierProvider(create: (context) => systemConfigModel),

      ],

      child: child,

    );

    上面代码的第 7 行就是增加了系统变量的初始化,第 15 行就是增加到状态共享中。接下来我们完善下 system_config_model.dart 代码,为其增加 get 和 save 方法,代码如下:

    复制代码

    /// 转化为StructSystemConfig结构

    StructSystemConfig get() {

      return systemConfig;

    }

    /// 转化为StructSystemConfig结构

    bool getSwitchItem(String switchItem) {

      if(systemConfig == null) {

        return false;

      }

      Map<String,dynamic> systemConfigJson =

      StructSystemConfig.toJson(systemConfig);

      try{

        return systemConfigJson[switchItem] as bool;

      }catch(e){

        return false;

      }

    }

    代码的第 2 到第 18 行中的两个方法 get 和 getSwitchItem ,其作用都是获取系统配置,前者是获取所有配置,后者是获取具体的某个配置。我们继续看下配置保存的两个方法,代码如下。

    复制代码

    /// 保存单个数据

    void saveOne(String key, bool value) {

      Map<String,dynamic> systemConfigJson =

        StructSystemConfig.toJson(systemConfig);

      if(systemConfigJson[key] == value) {

        return;

      }

      systemConfigJson[key] = value;

      systemConfig = StructSystemConfig.fromJson(systemConfigJson);

      print(systemConfigJson);

      LocalStorage.save(json.encode(systemConfigJson), 'tyfapp.system.config');

      notifyListeners();

    }

    /// 整体数据保存

    void save(StructSystemConfig newSystemConfig) {

      if(

      systemConfig.accessMessage == newSystemConfig.accessMessage &&

          systemConfig.tipsDetail == newSystemConfig.tipsDetail &&

          systemConfig.soundReminder == newSystemConfig.soundReminder &&

          systemConfig.vibrationReminder == newSystemConfig.vibrationReminder

      ) {

        return;

      }

      systemConfig = newSystemConfig;

      LocalStorage.save(

          json.encode(StructSystemConfig.toJson(systemConfig)),

          'tyfapp.system.config'

      );

      notifyListeners(

    代码 save 和 saveOne,分别对应保存整个系统配置数据和保存单个系统配置数据。在两者实现逻辑中,首先都做了前期数据校验判断,避免不必要的 build 操作。在 save 代码逻辑中,需要将数据存储到本地,通过调用 LocaStorage.save 来实现。

    组件应用

    组件应用部分较为简单,我们先看下 pages/system_page/index.dart 的逻辑,如下:

    复制代码

    import 'package:flutter/material.dart';

    import 'package:provider/provider.dart';

    import 'package:two_you_friend/model/system_config_model.dart';

    import 'package:two_you_friend/widgets/system_page/switch_card.dart';

    import 'package:two_you_friend/util/struct/system_config.dart';

    /// 首页

    class SystemConfigPageIndex extends StatelessWidget {

      /// 构造函数

      const SystemConfigPageIndex();

      @override

      Widget build(BuildContext context) {

        final systemConfigModel = Provider.of<SystemConfigModel>(context);

        StructSystemConfig systemConfig = systemConfigModel.get();

        return Container(

          padding: EdgeInsets.all(8),

          child: Column(

            children: <Widget>[

              SystemPageSwitchCard(itemDesc: '新消息提醒', switchItem: 'accessMessage'),

              SystemPageSwitchCard(itemDesc: '通知显示详情', switchItem: 'tipsDetail'),

              SystemPageSwitchCard(itemDesc: '声音', switchItem: 'soundReminder'),

              SystemPageSwitchCard(itemDesc: '振动', switchItem: 'vibrationReminder')

            ],

          ),

        );

      }

    }

    主要逻辑在 build 中,build 中使用了 widgets/system_page/switch_card.dart ,我们看下这个子组件的实现,代码如下:

    复制代码

    import 'package:flutter/material.dart';

    import 'package:provider/provider.dart';

    import 'package:two_you_friend/model/system_config_model.dart';

    import 'package:two_you_friend/styles/text_syles.dart';

    /// 单个系统配置

    ///

    /// [title]为帖子详情内容

    class SystemPageSwitchCard extends StatelessWidget {

      /// 传入的帖子标题

      final String switchItem;

      /// 消息提醒文字

      final String itemDesc;

      /// 构造函数

      const SystemPageSwitchCard(

          {Key key, this.itemDesc, this.switchItem}

          ) : super(key: key);

      @override

      Widget build(BuildContext context) {

        // 获取操作句柄

        final systemConfigModel = Provider.of<SystemConfigModel>(context);

        return Row(

          mainAxisAlignment: MainAxisAlignment.spaceBetween,

          children: <Widget>[

            Text(

              itemDesc,

              style: TextStyles.commonStyle(1, Colors.black),

            ),

            Switch( // 选择

                value: systemConfigModel.getSwitchItem(switchItem),

                activeTrackColor: Colors.lightBlueAccent,

                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,

                onChanged: (newValue) { // 触发状态变化

                  systemConfigModel.saveOne(

                      switchItem,

                      !systemConfigModel.getSwitchItem(switchItem)

                  );

                }

            ),

          ],

        );

      }

    }

    代码第 34 行使用了 Switch 这个组件,该组件的 value 是通过 systemConfigModel 状态共享类获取,在点击切换时触发状态改变,并调用 systemConfigModel 中的 saveOne 触发依赖组件状态变化。

    以上就实现了系统设置的功能,相对其他组件的实现,这部分逻辑较为复杂,涉及了 Flutter 的本地存储 以及 Provider 的应用技术点。

    搜索

    最后我们来看下搜索功能,前面我们已经实现了一个基本的搜索功能,但是其中的接口部分没有补齐,我们先来看下实际的效果,如图 6 所示。

    图 6 搜索提示和搜索结果效果

    组件树+布局

    根据图 6 的页面效果,我们来绘制组件树+布局,搜索提示就是一个列表,这里就不绘制了,搜索结果稍微复杂一些,主要看下这部分,绘制结果如图 7 所示。

    图 7 搜索结果页面组件树+布局设计

    这个组件树的设计,包含了我们布局设计思想中的 8 个过程,竖横、高宽、上下和左右,具体细节就不再赘述。接下来我们看下这两部分逻辑的实现:搜索提示和搜索结果。

    搜索提示

    搜索提示较为简单,主要逻辑是从服务端拉取搜索提示接口,并返回一个 ListView 列表结果。具体代码如下:

    复制代码

    /// 获取 suggest list组件列表

    Future<Widget> _getSuggestList() async{

      List<String> suggests = await ApiSearchIndex.suggest(query);

      if(suggests == null || suggests.length < 1){ // 异常处理

        return Container();

      }

      // 保留前 5 个搜索

      int subLen = suggests.length > 5 ? 5 : suggests.length;

      List<String> subSuggests = suggests.sublist(0, subLen);

      return ListView.builder(

          scrollDirection: Axis.vertical,

          shrinkWrap: true,

          itemCount: subSuggests.length,

          itemBuilder: (context,index){

            return  ListTile(

                title: RichText(

                    text: TextSpan(

                      // 获取搜索框内输入的字符串,设置它的颜色并加粗

                        text: subSuggests[index],

                        style: TextStyles.commonStyle()

                    )

                ),

                onTap: () {

                  query = subSuggests[index];

                  showResults(context);

                },

            );

          }

      );

    }

    代码中,首先使用 query 关键词获取用户输入,通过 ApiSearchIndex.suggest 方法获取服务端搜索提示结果,接下来做一些数据校验,最后根据搜索提示 build 出相应的组件。其中的第 26 行至第 28 行代码的作用是,通过点击搜索提示触发搜索行为,第 27 行替换搜索提示内容,第 28 行执行搜索并获取搜索结果。

    搜索结果

    根据图 7 的绘制结果,我们了解到这里需要设计两个组件,组件一是展示搜索到的用户列表内容,组件二是展示搜索到的帖子列表内容。我们这里就使用两个组件函数来实现,主要看下用户部分(帖子部分逻辑相似)。

    复制代码

    /// 获取用户搜索结果组件

    Widget _getUserListWidget(List<StructUserInfo> userList) {

      if(userList == null || userList.length < 1){

        return Container();

      }

      int subLen = userList.length > 5 ? 5 : userList.length;

      List<StructUserInfo> subUserList = userList.sublist(0, subLen);

      return ListView.builder(

          scrollDirection: Axis.vertical,

          shrinkWrap: true,

          itemCount: subUserList.length + 1,

          itemBuilder: (context,index) {

            if(index == 0){

              return Row(

                children: <Widget>[

                  Padding(padding: EdgeInsets.only(left: 10)),

                  Text(

                    '用户',

                    style: TextStyles.commonStyle(0.9),

                  ),

                ],

              );

            }

            return UserPageCard(userInfo: subUserList[index - 1]);

          });

    }

    以上组件代码的实现与我们之前所学习的知识点,没有太大的差异性。核心知识点是应用 ListView.builder 组件,来显示 seaction_name (也就是上面的 Text 组件)和搜索结果中的用户列表(上面的 UserPageCard 组件)。

    以上就完成了搜索部分的逻辑,具体代码查看 github 中的 pages/search_page/custom_delegate.dart 文件。

    总结

    本课时带领大家实践开发了四个核心页面(我的好友、我的消息、系统设置和搜索)。学完本课时你需要进一步掌握组件树+布局的设计思想,同时掌握 Flutter 本地存储的技术点,进一步巩固 Flutter 编码风格。学完本课时之后,我建议你自行去实现“我的消息”中的私信功能和评论相关的部分(后续会在 github 上提供源码)。

    本课时之前,我们对 App 的安全并没有关注太多,可以说完全放任。下一课时我们将通过工具化的方式来上报异常,保证我们 App 的质量,提前发现并解决问题。

    转自:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=251#/detail/pc?id=3535

    相关文章

      网友评论

          本文标题:实践 Flutter 交友功能-网络缓存

          本文链接:https://www.haomeiwen.com/subject/pmxtmktx.html