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

本课时我会和大家一起来完善 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

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