三方库开源地址:https://atomgit.com/nutpi/flutter_ohos_text_span_field

写在前面

上一篇文章介绍了怎么用 FlutterTextSpanField 实现@用户功能,但光能@还不够,我们还得把@的用户ID拿出来发给后端。不然后端怎么知道要给谁发通知呢?

这就涉及到一个概念——隐藏域。简单说就是用户看到的是昵称,但我们实际存储和传输的是ID。

说实话,这个功能我一开始也没太重视,觉得不就是拿个ID嘛,能有多难?结果真正做的时候发现,要从一堆 TextSpan 里筛选出自定义的@块,还要保证顺序不乱,还挺麻烦的。

好在这个库提供了一个 getWidgets() 方法,用起来还挺方便。今天就来聊聊怎么获取这些隐藏的值。
请添加图片描述

为什么需要隐藏域

举个例子你就明白了。

假设用户发了一条动态:今天和@张三 @李四 一起吃饭

只存文字的问题

如果我们只存这段文字,后端怎么知道张三是哪个张三?

系统里可能有几百个叫张三的用户:

  • 张三(ID: 10001)- 北京的
  • 张三(ID: 10002)- 上海的
  • 张三(ID: 10003)- 广州的

后端根本没法判断用户@的是哪个。

正确的做法

所以正确的做法是,除了存文字内容,还要存一个@用户的ID列表。

发给后端的数据应该长这样:

{
  "content": "今天和@张三 @李四 一起吃饭",
  "at_users": ["10001", "10002"]
}

这样后端就能精确地知道@的是谁,也能给对应的用户发通知。


实际应用场景

这个功能在很多场景下都用得到:

场景一:发送通知

用户发了一条@别人的动态,后端需要给被@的人发推送通知。如果没有ID,根本没法发。

场景二:权限控制

有些 App 的@功能有权限限制,比如只能@好友,或者只能@同事。后端需要根据ID来判断是否有权限@这个人。

场景三:数据统计

产品经理可能想知道哪些用户被@的次数最多,这就需要统计ID,而不是昵称。因为昵称可能重复,也可能被修改。

回顾一下 AtTextSpan

在上一篇文章里,我们创建了一个自定义的 AtTextSpan:

class AtTextSpan extends TextSpan {
  final String id;

  const AtTextSpan({
    required this.id,
    String? text,
    TextStyle? style,
  }) : super(text: text, style: style);
}

这个 id 字段就是我们的隐藏域。用户看不到它,但它一直跟着 TextSpan 存在。

获取隐藏域的核心方法

TextSpanBuilder 提供了一个 getWidgets() 方法,可以拿到输入框里所有的自定义组件。

调用 getWidgets 方法

List<TextSpanWidget> widgets = _textSpanBuilder.getWidgets();

返回的是一个 TextSpanWidget 列表。

TextSpanWidget 是什么?

它是一个数据类,包含了我们之前插入的 TextSpan 对象,以及这个 TextSpan 在文本中的位置信息。

简单来说,每个 TextSpanWidget 代表输入框里的一个"块",可能是普通文本,也可能是@块。


筛选出 AtTextSpan

拿到列表之后,我们需要遍历它,把 AtTextSpan 类型的筛选出来:

widgets.forEach((element) {
  if (element.span is AtTextSpan) {
    AtTextSpan at = element.span as AtTextSpan;
    print("昵称: ${at.text}, ID: ${at.id}");
  }
});

为什么要做类型判断?

因为列表里可能还有普通的 TextSpan(用户手动输入的文字),我们只需要 AtTextSpan。

is 关键字判断类型,然后用 as 转换成 AtTextSpan,就能访问到我们自定义的 id 字段了。


封装成方法

实际项目中,我们一般会封装成一个方法,直接返回ID列表:

List<String> getAtUserIds() {
  List<String> ids = [];
  
  _textSpanBuilder.getWidgets().forEach((element) {
    if (element.span is AtTextSpan) {
      AtTextSpan at = element.span as AtTextSpan;
      ids.add(at.id);
    }
  });
  
  return ids;
}

这样在发送消息的时候,直接调用这个方法就能拿到所有@用户的ID了。

为什么要封装?

一是代码更简洁,二是方便复用。可能有多个地方需要获取@用户ID,封装成方法之后,哪里需要就在哪里调用。

完整的发送流程

来看一个比较完整的发送消息流程:

_sendMessage() {
  // 1. 获取文本内容
  String content = _textSpanBuilder.controller?.text ?? "";
  
  // 2. 获取@用户ID列表
  List<String> atUserIds = getAtUserIds();
  
  // 3. 组装数据
  Map<String, dynamic> data = {
    "content": content,
    "at_users": atUserIds,
  };
  
  // 4. 发送给后端
  // api.post("/message/send", data);
  
  print("发送的数据: $data");
}

第一步拿文本内容,这个直接从 controller 里取就行。

第二步拿@用户ID列表,用我们刚才封装的方法。

第三步组装成后端需要的格式,一般是一个 Map。

第四步调接口发送,这里就不展开了。


打印出来的数据大概长这样:

发送的数据: {
  content: "今天和@张三 @李四 一起吃饭",
  at_users: ["10001", "10002"]
}

后端拿到这个数据,就能知道要给ID为10001和10002的用户发@通知了。

获取更多信息

有时候我们不只需要ID,可能还需要知道@用户在文本中的位置。TextSpanWidget 里有一个 range 属性可以用:

widgets.forEach((element) {
  if (element.span is AtTextSpan) {
    AtTextSpan at = element.span as AtTextSpan;
    
    print("昵称: ${at.text}");
    print("ID: ${at.id}");
    print("起始位置: ${element.range.start}");
    print("结束位置: ${element.range.end}");
  }
});

range.start 是这个@块在文本中的起始下标,range.end 是结束下标。

这个信息在某些场景下挺有用的,比如你想高亮显示@的用户,或者做一些文本分析。

一个实用的展示组件

写了一个简单的组件,用来展示获取到的隐藏域值,方便调试:

Widget _buildValueDisplay() {
  return Container(
    padding: EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(8),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          "获取到的@用户:",
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 8),
        Text(_valueContent.isEmpty ? "暂无数据" : _valueContent),
      ],
    ),
  );
}

样式比较简单,就是一个灰色背景的容器,里面显示获取到的内容。


点击按钮的时候更新显示内容:

_getValue() {
  List<TextSpanWidget> widgets = _textSpanBuilder.getWidgets();
  
  _valueContent = "";
  int count = 0;
  
  widgets.forEach((element) {
    if (element.span is AtTextSpan) {
      count++;
      AtTextSpan at = element.span as AtTextSpan;
      _valueContent += "$count. ${at.text} (ID: ${at.id})\n";
    }
  });
  
  if (count == 0) {
    _valueContent = "没有@任何用户";
  }
  
  setState(() {});
}

这样就能直观地看到输入框里@了哪些用户,以及他们的ID是什么。

鸿蒙适配说明

隐藏域值获取这块完全是 Dart 层面的逻辑,不涉及任何平台相关的代码,所以在鸿蒙上跑起来没有任何问题。

我在鸿蒙模拟器和真机上都测试过,getWidgets() 方法返回的数据和安卓、iOS上完全一致。

注意事项

1. 时机问题

调用 getWidgets() 的时机要注意。如果用户还在输入(比如正在用拼音输入法打字),这时候拿到的数据可能不准确。建议在用户明确点击"发送"按钮之后再获取。

2. 空值处理

getWidgets() 返回的列表可能为空(用户没有@任何人),也可能包含非 AtTextSpan 类型的元素。所以遍历的时候一定要做类型判断。

3. 顺序问题

返回的列表顺序是按照@块在文本中的位置排列的,先出现的在前面。如果你需要按其他顺序排列,可以自己再排一下序。

写在最后

隐藏域值获取是@功能的重要组成部分,没有它,@功能就只是个"花架子",好看但没用。

FlutterTextSpanField 把这块封装得还不错,一个 getWidgets() 方法就能搞定,省了不少事。

下一篇文章会介绍文本的删除和清空功能,包括那个很有意思的"块删除"效果,感兴趣的话继续关注~


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐