GetX 实现类似微信转发搜索多选好友

search.gif

自定义searchBar 搜索本地数据库好友信息高亮显示匹配并多选发送

先看下文件分类吧

1620456343148.jpg

receive_share_intent.dart 请忽略

采用MVC实现代码分层

  • logic: controller层处理逻辑
  • view: UI控件
  • state: 数据层
  • widget:控件拆分

先看下view层

class ForwardMsgPage extends StatelessWidget {
  final Message forwardMsg;
  /// 注入controller 和 state
  final ForwardMsgLogic logic = Get.put(ForwardMsgLogic());
  final ForwardMsgState state = Get.find<ForwardMsgLogic>().state;

  ForwardMsgPage({Key key, this.forwardMsg}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return forwardBuildBg(context, backFunc: () {
      Get.delete<ForwardMsgLogic>();
      Navigator.pop(context);
    }, children: [
      ZYSearchWidget(
          hintText: '搜索',
          onSearch: logic.onSearch,
          onCancel: () => logic.onSearchCancle(),
          onClear: () => logic.onSearchClear(),
          onChanged: logic.onSearchValueChange
          // onEditingComplete: () =>logic.onSearchValueChange(''),
          ),

      buildUserAndGroupList(
        /// 滑动取消键盘
          gesturePanDownCallback: () {
            FocusScope.of(context).requestFocus(FocusNode());
          },
          children: [
            /// 聊天列表
            Obx(() => Visibility(
                visible: state.searchQuery.value.isEmpty,
                child: ForwardMsgRecentCon(
                  data: state.conList,
                  itemClick: (value) => showAlert(context,
                      determine: () =>
                          logic.sendMsg(forwardMsg, value, context)),
                ))),

            /// 联系人 群组列表
            Obx(() => Visibility(
                visible: state.searchQuery.value.isNotEmpty,
                child: ForwardMsgSearchResult(
                  state: state,
                  userItemClick: (userInfo) => logic.userItemClick(userInfo),
                  groupItemClick: (groupInfo) => logic.groupItemClick(groupInfo),
                  // itemClick: (value) => showAlert(context,
                  //     determine: () => logic.sendMsg(forwardMsg, value, context)),
                ))),
          ]),

      /// 联系人 群组列表
      Obx(() => Visibility(
          visible: state.showSend.value,
          child: ForwardChooseResult(
            state: state,
            closeClick: (item)=>logic.closeBtnClick(item),
            confirmSendClick:()=> logic.confirmSendClick(forwardMsg,completeHandler: (){
              Get.delete<ForwardMsgLogic>();
              Navigator.pop(context);
            }),
          )))
    ]);
  }
}

Obx 根据响应字段 searchQuery 和 showSend 隐藏显示
state类定义了可响应字段

typedef ForwardUserItemBuilder = Widget Function(UserInfo item);
typedef ForwardGroupItemBuilder = Widget Function(GroupInfoUserList item);
typedef ForwardChooseItemBuilder = Widget Function(dynamic item);
class ForwardMsgState {
  ///最近会话数据源
  List conList = [];
  /// 联系人列表
  List<SearchUserInfo> userList = [];
  /// 群列表
  List<SearchGroupInfo> groupList = [];
  /// 是否显示最近聊天
  // RxBool showRecentList = true.obs;
  /// 搜索内容
  RxString searchQuery = ''.obs;
  /// 选中的用户列表
  List selectUserList = [];
  /// 选中的群组列表
  List selectGroupList = [];
  /// 用户列表 + 群组列表
  List selectTotalList = [];
  /// 展示多选
  RxBool showSend = false.obs;
}

controller层

class ForwardMsgLogic extends GetxController {
  final state = ForwardMsgState();
  int forwardTotalCount = 0;

  @override
  void onReady() {
    // TODO: implement onReady
    super.onReady();

    ///防DDos - 每当用户停止输入1秒时调用
    debounce(state.searchQuery, (value) => loadDataFormDB(value));
    updateConversationList();
  }

  updateConversationList() async {
    List list = await RongIMClient.getConversationList(
        [RCConversationType.Private, RCConversationType.Group]);
    state.conList.addAll(list);
    update(['conList']);
  }

  void recentConItemClick(Conversation conversation) {}
  void onSearch(msg) {
    loadDataFormDB(msg);
  }

onReady方法获取最近聊天列表数据,get提供了防抖

搜索后获取本地数据库用户信息并匹配用户

void loadDataFormDB(query) async {
    print('query----rebuild');
    state.userList.clear();
    state.groupList.clear();
    List<userdb.UserInfo> userlist =
        await DbManager.instance.getUserInfoWithUserName(query);
    if (userlist != null && userlist.isNotEmpty) {
      List<SearchUserInfo> searchUserList = [];
      userlist.forEach((element) {
        SearchUserInfo searchUserInfo = SearchUserInfo.formUserInfo(element);
        // 根据已选择的是否包含 初始化可选状态
        state.selectUserList.forEach((element) {
          if (element.id == searchUserInfo.id) {
            searchUserInfo.checked.value = true;
          }
        });
        searchUserList.add(searchUserInfo);
      });
      state.userList.assignAll(searchUserList);
    }
    update(['userList']);

    List<GroupInfoUserList> grouplist =
        await DbManager.instance.getGroupInfoWithGroupName(query);
    if (grouplist != null && grouplist.isNotEmpty) {
      List<SearchGroupInfo> searchGroupList = [];
      grouplist.forEach((element) {
        SearchGroupInfo searchGroupInfo =
            SearchGroupInfo.formGroupInfo(element);
        // 根据已选择的是否包含 初始化可选状态
        state.selectGroupList.forEach((element) {
          if (element.id == searchGroupInfo.id) {
            searchGroupInfo.checked.value = true;
          }
        });
        searchGroupList.add(searchGroupInfo);
      });
      state.groupList.assignAll(searchGroupList);
    }
    update(['groupList']);
  }

手动调用 update(['userList']); 通过制定tag 做到指定刷新widget

///联系人搜索结果
  Widget _searchUserResult({ForwardUserItemBuilder itemBuilder}) {
    return GetBuilder<ForwardMsgLogic>(
        id: 'userList',
        builder: (controller) {
      return controller.state.userList.length > 0
          ? ListView.builder(
              padding: EdgeInsets.zero,
              shrinkWrap: true,
              physics: NeverScrollableScrollPhysics(),
              itemCount: controller.state.userList.length,
              itemBuilder: (context, index) => itemBuilder(controller.state.userList[index])
      )
          : Container();
    });
  }
  /// 搜索群组结果
  Widget _buildGroupList({ForwardGroupItemBuilder itemBuilder}){
    return GetBuilder<ForwardMsgLogic>(
        id: 'groupList',
        builder: (controller) {
      return controller.state.groupList.length > 0
          ? ListView.builder(
          // padding: EdgeInsets.zero,
          shrinkWrap: true,
          physics: NeverScrollableScrollPhysics(),
          itemCount: controller.state.groupList.length,
          itemBuilder: (context, index) => itemBuilder(controller.state.groupList[index]))
          : Container();
    });
}

通过 设置 GetBuilder的id对应update方法,GetBuilder 可以理解为statefulWidget

多选功能通过设置对象的checked为可观察 RxBool checked = false.obs;

class SearchUserInfo extends userdb.UserInfo {
  RxBool checked = false.obs;
  SearchUserInfo.formUserInfo(userdb.UserInfo userInfo) {
    this.companyName = userInfo.companyName;
    this.id = userInfo.id;
    this.name = userInfo.name;
  }
}

/// 用户列表可选框
  Widget _buildUserListCheckBox(SearchUserInfo userInfo){
    return Padding(
      padding: const EdgeInsets.only(right: 17),
      child: Obx(()=>Image.asset(
        userInfo.checked.value?'assets/images/contact_info_selected.png':'assets/images/contact_info_unselected.png',height: 24,width: 24,fit:BoxFit.fill ,)),
    );
  }

点击后更新对象属性的响应属性

state.userList.forEach((element) {
        if (element.id == item.id) {
          element.checked.value = false;
        }
      });

通过checked.value 设置 注意 .obs 属性对应 obx控件观察。

搜索高亮匹配 代码如下

Widget _splitUserNameRichText(String userName) {
    print(userName);
    List<TextSpan> spans = [];
    List<String> strs = userName?.split(state.searchQuery.value)??[];
    for (int i = 0; i < strs.length; i++) {
      if ((i % 2) == 1) {
        spans.add(TextSpan(
            text: state.searchQuery.value, style: _highlightUserNameStyle));
      }
      String val = strs[I];
      if (val != '' && val.length > 0) {
        spans.add(TextSpan(text: val, style: _normalUserNameStyle));
      }
    }
    return RichText(text: TextSpan(children: spans));
  }

界面布局防止套娃采用widget分组的方式便于定位代码

Widget build(BuildContext context) {
    print('ForwardMsgSearchResult---rebuild');
    return _buildBg(children: [
      /// 联系人
      _searchUserResult(itemBuilder: (item) {
        return _buildUserItemBg([
          /// 内容
          _buildItemContent(children: [
            /// 复选框
            _buildUserListCheckBox(item),
            /// 头像
            WidgetUtil.buildUserPortraitWithParm(item.portraitUrl,item.name),
            /// 用户名字
            _buildUserItemName(item),
            /// 公司名
            _buildUserCompanyName(item)
          ]),

          /// 下划线
          _buildBottomLine()
        ],item: item);
      }),

      /// 分隔符
      _buildSeparator(),

      /// 群组结果
      _buildSearchGroupBg(children: [
        /// 群组标题
        _buildGroupTitle(),
        /// 群组item
        _buildGroupList(itemBuilder: (item){
          return _buildGroupItemBg(
              groupInfo: item,
              children: [
            /// 群组内容
              _buildGroupContent(children: [
              /// 复选框
                _buildGroupListCheckBox(item),
              /// 群头像
                _buildGroupItemPortrait(item),
                _buildGroupNameAndUserName(children: [
                /// 群名称
                _buildGroupItemGroupName(item),
                /// 包含的群成员
                _buildGroupMember(item)
              ])
            ]),
            /// 下划线
            _buildBottomLine()
          ]);
        })

      ]),
    ]);
  }

难点在于三个数据源同步问题,底部展示多选列表和userList,groupList 同步

/// 联系人点击
  void userItemClick(SearchUserInfo item) {
    item.checked.value = !item.checked.value;
    if (item.checked.value) {
      bool exist = state.selectUserList.any((element) => element.id == item.id);
      if (!exist) {
        state.selectUserList.add(item);
        state.selectTotalList.add(item);
      }
    } else {
      state.selectUserList.removeWhere((element) => element.id == item.id);
      state.selectTotalList.removeWhere((element) => element.id == item.id);
    }
    print('leon----selectUserList---${state.selectUserList.length}');

    _showSend();
  }
  
  void _showSend() {
    update(['chooseSend', 'totalCount']);
    state.showSend.value = state.selectTotalList.isNotEmpty;
  }

观察showSend隐藏显示底部多选框 visible: state.showSend.value,

/// 多选列表
      Obx(() => Visibility(
          visible: state.showSend.value,
          child: ForwardChooseResult(
            state: state,
            closeClick: (item)=>logic.closeBtnClick(item),
            confirmSendClick:()=> logic.confirmSendClick(forwardMsg,completeHandler: (){
              Get.delete<ForwardMsgLogic>();
              Navigator.pop(context);
            }),
          )))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容