首页 / 文章

为 React Native Paper 构建可配置的选择组件

详解 react-native-paper-select 的设计与开源过程,涵盖搜索功能、多选标签、分节列表以及性能权衡。

发布者:REACTAPP.TOP阅读约 12 分钟2663 词

来源: I Built a React Native Select Component — and Turned It Into an Open-Source Package: `react-native-paper-select`

在 React Native 中实现真正的下拉菜单几乎从来都不像听起来那么简单

如果你曾花时间尝试在 React Native 中实现一个功能完善的下拉菜单,你就知道“仅仅做一个下拉菜单”很快就会变成一项更为复杂的任务。

你最终需要处理选择状态、实现搜索功能、支持多选、应对长列表、以符合移动端原生体验的方式展示界面、添加验证逻辑,还要确保整个组件在视觉上与应用程序的其他部分协调一致。

如果你的项目已经基于 React Native Paper 构建,仅仅为了获得一个可用的选择字段而引入额外的 UI 库似乎有些过度了。

正是这种不匹配的问题促使人们创造了这个组件来加以解决。

目标是创建一个可重复使用的选择组件,使其能自然融入 React Native Paper 中,同时支持单选和多选功能,从而让各个项目无需反复构建相同的交互逻辑。

于是人们决定从零开始开发它。

最初只是一个功能较为有限的 UI 组件,后来逐渐发展成了 react-native-paper-select —— 一个开源的 React Native 包,如今已具备搜索、多选、芯片式显示、分节列表、单选按钮选择、底部面板、菜单功能,以及注重性能的列表渲染能力。

本文将介绍开发该组件的初衷、其功能随时间的变化历程、影响其设计的工程决策,以及将最初仅为某个应用设计的工具转变为通用开源库过程中所获得的经验教训。

问题所在

表面上看,下拉菜单似乎很简单。

但一旦真正开始实现它,就会出现一大堆设计问题:

  • 如何显示当前选中的值?
  • 该字段需要支持单次选择还是多次选择?
  • 选项列表的展示方式应如何?
  • 如何在该列表中实现搜索功能?
  • Android和iOS上的行为是否需要有所不同?
  • 如何渲染非常长的列表而不影响性能?
  • 如何保持面向开发者的API简洁?
  • 如何在不过度增加复杂性的前提下允许用户自定义?

在每个应用中从头开始解决这些问题并不合理。

与其再次创建一个为特定应用设计的一次性组件,不如从一开始就设计出可重复使用的方案。

其核心理念很简单:

在 React Native Paper 自带的 UI 原语之上构建一个选择组件,使其足够便捷,让其他 React Native 开发者愿意使用它。

介绍 react-native-paper-select

react-native-paper-select 是一个专为 React Native 设计的可配置组件,支持单选和多选两种使用场景。

第一个版本利用了 React Native Paper 已经提供的常见组件,包括:

  • TextInput
  • Checkbox
  • Dialog

其目的是为开发者提供一种在视觉效果和交互行为上与基于 React Native Paper 设计系统已开发的应用程序保持一致的选中体验。

随着该包的成熟,它已经超越了最初的实现方式,能够支持更多样的展示风格以及更丰富的选中交互功能。

安装该包

启动使用只需几步即可完成。

npm install react-native-paper-select

或者,如果您更喜欢使用 Yarn:

yarn add react-native-paper-select

安装完成后,可按如下方式导入该组件:

import { PaperSelect } from 'react-native-paper-select';

基础选择组件

要创建一个最简单的选择字段,只需准备一份选项列表以及一个用于处理选中的函数。

以下是一个示例:

const [selected, setSelected] = useState({
  value: '',
  label: '',
});
const options = [
  {
    _id: '1',
    value: 'React Native',
  },
  {
    _id: '2',
    value: 'Flutter',
  },
  {
    _id: '3',
    value: 'Native Android',
  },
];

然后即可渲染该组件:

<PaperSelect
  label="Framework"
  value={selected.label}
  onSelection={(value) => setSelected(value)}
  arrayList={options}
  selectedArrayList={selected}
/>

这里重要的并非代码量之少,而是这些代码所隐藏的功能。

从应用程序的角度来看,实际上只需要回答两个问题:

有哪些可选选项?

以及

用户选择了什么?

与展示和交互相关的一切都由组件本身内部处理。

单选与多选

从一开始就支持单选和多选是该组件的核心要求之一。

单选的逻辑相对简单:

Select Framework
----------------
○ React Native
○ Flutter
○ Native Android

然而,多选则需要管理更多的状态:

Select Technologies
-------------------
☑ React Native
☑ TypeScript
☐ Flutter
☑ Node.js

这意味着组件必须处理以下事项:

  1. 跟踪选择状态
  2. 处理取消选择
  3. 同时显示多个已选值
  4. 返回更新后的选择集合
  • 保持显示的界面与底层数据同步
  • 其目的是将所有复杂性都封装在组件内部,而非将其转嫁给使用该包的每一位开发者。

    为何选择 React Native Paper?

    由于现有应用已经依赖 React Native Paper,因此基于同一设计系统来构建选择组件是自然而然的选择。

    无需重新设计每个视觉元素,该组件可以直接复用 React Native Paper 已经提供的界面组件。

    从宏观上看,其内部结构大致如下:

                    PaperSelect
                         │
              ┌──────────┴──────────┐
              │                     │
          TextInput            Selection UI
                                    │
                        ┌───────────┼───────────┐
                        │           │           │
                     Checkbox      Radio       List
    

    这种设计选择带来了实际的好处。

    任何已经使用 React Native Paper 的应用都可以直接引入此选择组件,而不会导致视觉风格与界面其他部分冲突。

    从单一种展示方式到多种展示风格

    该组件的最初版本仅提供用于选择的对话框。

    其交互方式如下:

    TextInput
        ↓
    User taps field
        ↓
    Dialog opens
        ↓
    User selects an item
        ↓
    Selected value appears in TextInput
    

    然而,随着组件的成熟,人们逐渐意识到对话框并非适用于所有移动屏幕的理想方案。

    不同应用程序的用户体验需求差异很大。

    为了解决这一问题,该组件发展出了多种展示模式:

    • 对话框
    • 底部面板
    • 菜单

    通过这一改动,同一个底层选择组件可以被应用到截然不同的交互场景中,开发者无需为在不同场景间切换而重新编写集成代码。

    例如:

    <PaperSelect
      presentationStyle="bottomSheet"
      ...
    />
    

    这种灵活性为该库带来了实质性的进步。

    无论选择哪种样式,筛选逻辑都保持不变——只有向用户展示的方式会改变。

    处理大量列表

    下拉式组件一个不太明显的挑战是,当列表内容增多时,其性能表现如何。

    想象一个包含以下内容的选择组件:

    10 items
    

    这样的内容渲染起来非常简单。

    现在再想象一个包含以下内容的组件:

    1,000 items
    

    或者甚至是:

    10,000 items
    

    一次性渲染如此多的列表项可能会影响移动应用的响应速度。

    为避免这种情况,该框架依赖FlashList来渲染选项列表。

    其原理很简单:

    Large dataset
          ↓
    Virtualized list
          ↓
    Render what is needed
          ↓
    Better scrolling performance
    

    性能问题并非可以等到开发大规模应用时才去解决的。

    任何用于重复使用的组件都必须从一开始就考虑扩展性,因为很难提前知道传入该组件的数据集会有多庞大。

    搜索

    当可选选项的数量增加时,搜索就变得至关重要。

    该组件提供了一种可搜索的功能,帮助用户缩小选择范围,而无需在冗长的列表中无限滚动。

    操作流程如下:

    Search
      ↓
    Filter options
      ↓
    Select result
      ↓
    Update selection
    

    从用户的角度看,这似乎只是一个小小的改进,但一旦数据集的项数超过少数几项,它就会对可用性产生巨大影响。

    带芯片的多选功能

    一次性显示多个已选值很容易造成界面杂乱。

    例如,像这样的值:

    React Native, TypeScript, Node.js, MongoDB
    

    如果被塞进标准的文本输入框中,就很难解析。

    芯片功能可以解决这个问题。

    选中的值可以显示为独立的块:

    [ React Native ] [ TypeScript ]
    [ Node.js ]      [ MongoDB ]
    

    该组件支持这种基于芯片的显示方式,适用于多选场景,能让用户更清晰地看到自己选择了什么。

    它还能让删除单个选项的操作更加直观。

    分节列表

    另一个需求是能够将相关的选项归类在一起。

    不必使用像下面这样冗长且扁平的列表:

    React
    Angular
    Vue
    Node.js
    Java
    Python
    MongoDB
    PostgreSQL
    

    选项可以按标题进行组织:

    Frontend
    
    ☐ React
    ☐ Angular
    ☐ Vue
    Backend☐ Node.js
    ☐ Java
    ☐ Python
    Database☐ MongoDB
    ☐ PostgreSQL
    

    这样的分组方式使得庞大的选项集更易于浏览和理解。

    在多选模式下,允许用户一次性选择整个部分能显著提高效率。

    例如:

    ☑ Frontend
    
        ☑ React
        ☑ Angular
        ☑ Vue
    

    这比强迫用户逐个点击每个选项要好得多。

    用于单选的选择按钮

    当可以选择多个选项时,复选框更为合适。

    但对于单选场景,选择按钮能更清晰地向用户传达意图。

    正因如此,该组件库还提供了选择界面的选择按钮版本。

    其核心理念始终如一:

    Options
       ↓
    User chooses one
       ↓
    Selection state updated
    

    只有视觉呈现方式会发生变化。

    这凸显了构建可复用组件的一个重要原则:

    将选择逻辑与选择的显示方式分开。

    TypeScript与API设计

    由于该组件需供其他开发者使用,API的架构与界面外观同样重要。

    使用该组件库的人无需了解其内部工作原理。

    理想情况下,他们可以完全从以下角度来思考这个问题:

    Input
      ↓
    Options
      ↓
    User interaction
      ↓
    Selection
      ↓
    Callback
    

    而不是:

    Dialog state
    Modal state
    List state
    Search state
    Checkbox state
    Rendering state
    ...
    

    一个设计良好的组件 API 能将内部复杂性隐藏起来,不让使用者察觉。

    这正是通过不断构建和优化这个软件包所获得的最明确的经验教训之一。

    开源改变了你的开发方式

    为自己的应用编写组件与为更广泛的开发者群体编写组件是两码事。

    在单个应用程序内部,标准可能是:

    “这能解决我的特定问题。”

    一旦变为开源,问题的焦点就会转变为:

    “对于从未看过源代码的开发者来说,这个 API 是否容易理解?”

    这种转变会改变你在开发过程中的优先级。

    突然间,你会更加关注以下方面:

    • API的一致性
    • 文档说明
    • 向后兼容性
    • 边缘情况
    • 合理的默认值
    • 类型定义
    • 自定义选项
    • 性能表现
    • 版本控制

    最初只是一个简单的小组件,但一旦其他开发人员在实际项目中开始依赖它,它就会变成一个极其复杂的东西。

    项目随时间的发展历程

    最初只是一个简单的选择组件,后来逐渐发展成为一个功能更全面的选型工具包。

    其发展过程大致可以这样描述:

    Simple Select
          ↓
    Single Selection
          ↓
    Multi Selection
          ↓
        Search
          ↓
    Large List Handling
          ↓
        Chips
          ↓
       Sections
          ↓
    Radio Selection
          ↓
    Bottom Sheet
          ↓
    Menu Presentation
    

    每一项新功能都是为了解决与可用性或开发者体验相关的实际问题。

    这正是开源开发最突出的优势之一。

    在发布版本1时,你往往很难清楚地预见到最终完成的库会是什么样子。

    相反,随着实际需求、用户反馈以及新用例的出现,该软件包的形态会不断变化。

    过程中的经验教训

    1. 小问题也可能发展成有价值的开源项目

    最初的那个问题其实并无什么突破性。

    只是缺少一个可复用的选择组件而已。

    但仅为自己使用而解决问题与将其完善到其他人也能使用的高度之间存在着差距。

    有时候,这种差距恰恰足以成为启动一个开源项目的理由。

    2. 要让某样东西可复用,就需要对其进行恰当的抽象

    一个设计良好的组件不应与创造它的特定应用紧密绑定。

    相反,它需要在外观上呈现简洁清晰的界面,同时将内部的复杂性隐藏起来。

    这一原则在 React Native 的应用领域之外也同样适用。

    3. 组件设计同样关乎用户体验

    仅让组件在代码层面可复用还不够。

    用户与之交互时的感受同样重要。

    对话框、底部面板、菜单、标签、搜索栏、单选按钮以及分节列表,其实都是对同一个核心问题的不同解答:

    让用户进行选择的最佳方式是什么?

    4. 可复用组件的设计需考虑性能

    一个在处理十个项目时表现良好的组件,日后也可能被用于需要处理上万项目的场景中。

    正因如此,关于列表渲染和虚拟化的决策应在设计阶段就确定,而非等到生产环境出现性能问题时才临时考虑。

    5. 文档是该包的核心组成部分,而非附加物

    仅有能正常运行的代码并不足以说明该包已经完成。

    开发者仍然需要以下方面的明确解答:

    • 如何安装它
    • 如何使用它
    • 暴露了哪些属性
    • 预期数据应具备的格式
    • 如何自定义其行为和外观
    • 实际支持哪些功能

    完善的文档能够填补以下两种思维状态之间的差距:

    "I found this package."
    

    与能够自信地得出以下结论的状态:

    "I can use this package."
    

    项目的后续发展

    react-native-paper-select最初的定位相当简单:打造一个能够自然融入已使用React Native Paper的应用中的选择组件。

    如今它已发展成功能更为强大的工具,支持多种选择模式、多种展示风格、搜索功能、基于芯片的选择方式、分节数据展示,以及注重性能的列表渲染。

    它仍有很大的改进空间。

    没有任何开源项目是真正完成状态的。

    新的设备不断出现,React Native本身也在持续更新,设计需求在变化,无障碍访问的要求在提升,性能方面的考量再度出现,开发者们也不断提交值得采纳的反馈。

    正是这种持续的演进,让维护这样的库变得如此有趣。

    总结

    参与开发 react-native-paper-select 让我意识到,开源项目并不一定非得从宏大的愿景开始。

    通常它源于在开发其他项目时遇到的某个具体小问题。

    你先自己解决它。

    随后你会发现这个解决方案也能帮助其他面临同样问题的人。

    于是你进一步优化它。

    把它整理成规范的代码。

    然后将其公之于众。

    久而久之,一个最初只是内部使用的小组件就能发展成其他开发者在其项目中依赖的包。

    这就是 react-native-paper-select 背后的故事。

    如果你的项目使用了 React Native Paper,且需要灵活的单选或多选组件,该代码库托管在 GitHub 上,而该包本身则在 npm 上发布。

    如果你使用了该包,我们随时欢迎你的反馈、错误报告以及贡献。