鸿蒙原生应用 ArkUI 声明式实战:从零实现 —— 问卷设计器的四种题型与条件渲染联动
App 21「在线问卷调查」创建 Tab(Func1Tab),是问卷设计器页。整页由 4 个 @Builder 块拼出 Header + 问卷信息(标题/说明)+ 题目类型(单选/多选/评分/填空 4 种类型)+ 题目内容(条件渲染选项设置)+ 发布按钮。本篇基于
21-online-survey/entry/src/main/ets/pages/Func1Tab.ets(约 122 行)逐段拆解,重点解析"题目类型状态联动题目内容条件渲染"——本系列第一个"问卷设计器"页面。附 4 张实机截图。
一、整体结构:4 个 @Builder 的"问卷设计器"
Func1Tab 是本系列第一个"问卷设计器"页面——用户可以创建一份问卷(输入标题、选题目类型、填题目、设选项):
build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.InfoCard() this.TypeCard() this.QuestionCard() this.SubmitBtn() } .width('100%') .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width('100%').height('100%').backgroundColor(C.bg) }4 个块按"信息 → 类型 → 题目 → 发布"漏斗展开:
- InfoCard— 问卷标题 + 说明
- TypeCard— 4 种题目类型选择
- QuestionCard— 题目内容(含选项设置的条件渲染)
- SubmitBtn— 发布问卷
与系列表单页对比:App 13/14/15/16/17/19/20 都是"复杂表单"(5-6 块 + 多种状态 + 模板),App 21 是**"极简单一题表单"(4 块 + 一道题 + 单选/多选/评分/填空 4 种类型)——"问卷 = 一题多类型"**的特殊模式。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Header + InfoCard:问卷标题与说明
Header 是"创建问卷"单行标题(与系列同款):
@Builder Header() { Row() { Text('创建问卷') .fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) } .width('100%').height(this.safeTop + 56) .padding({ top: this.safeTop, left: D.pad, right: D.pad }) .backgroundColor(C.card) .alignItems(VerticalAlign.Bottom) }height(this.safeTop + 56)+padding({ top: this.safeTop })+alignItems(VerticalAlign.Bottom)三件套——状态栏让出 + 内容靠底——系列标准 Header 写法。
InfoCard是问卷信息(标题 + 说明):
@Builder InfoCard() { Column({ space: 12 }) { Text('问卷信息').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) TextInput({ placeholder: '问卷标题' }) .height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) TextArea({ placeholder: '问卷说明 / 简介...' }) .height(70).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) } .width('100%') .padding(14) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }TextInput标题 +TextArea说明——注意:未绑定 @State(没有text: this.xxx也没有onChange)——InfoCard 没有联动状态——demo 简化(数据只入不存)——真实项目应加@State title+@State desc双向绑定。
"问卷标题"占位vs App 13 "请输入标题"——"占位 = 业务提示"(用户一看就知道"这里填标题",不用看"标题"label)。"问卷说明 / 简介..."占位加省略号——"省略号 = 还有更多"(暗示可以写长)。
三、TypeCard:4 种题目类型选择
TypeCard 是 4 种题目类型的胶囊选择,单选高亮:
@Builder TypeCard() { Column({ space: 12 }) { Text('题目类型').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Row({ space: 10 }) { ForEach(this.types, (t: string) => { Text(t) .fontSize(13) .fontColor(this.qType === t ? '#FFFFFF' : C.textSub) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .backgroundColor(this.qType === t ? C.primary : C.cardSoft) .borderRadius(16) .onClick(() => { this.qType = t; }) }, (t: string) => t) } .width('100%') } .width('100%') .padding(14) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }4 种题目类型:
@State qType: string = '单选';private types: string[] = ['单选', '多选', '评分', '填空'];4 种类型 = 问卷平台 4 大题型:
| 类型 | 问卷平台通用名 | 场景 |
|---|---|---|
| 单选 | Single Choice | "你最喜欢的颜色?" |
| 多选 | Multiple Choice | "你经常使用的功能?" |
| 评分 | Rating | "请给本次服务打分 1-5" |
| 填空 | Text Input | "你有什么建议?" |
**@State qType: string = '单选' 默认选中"单选"**——onClick(() => { this.qType = t; })切换——**"qType 一变 → QuestionCard 条件渲染重算"**(与 App 18 发现页selectedCat` 同款"分类状态联动")。
"题目类型"是问卷设计器的"题型选择器"——不同题型对应不同的输入组件(单选=Radio/选项、多选=CheckBox/选项、评分=星级/数字、填空=TextInput)——qType一变 → 题目内容区的输入组件就变。
@State qType: string而不是number——字符串状态直接对比可读(qType === '单选'vsselectedType === 1)——"业务状态用字符串更易读"。
四、QuestionCard:题目内容(条件渲染核心)
QuestionCard 是本系列最值得学习的条件渲染示例——根据qType决定显示哪些子组件:
@Builder QuestionCard() { Column({ space: 12 }) { Text('题目内容').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) TextInput({ placeholder: '输入题目...' }) .height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) if (this.qType === '单选' || this.qType === '多选') { Text('选项设置').fontSize(13).fontColor(C.textSub) TextInput({ placeholder: '选项 A' }) .height(40).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) TextInput({ placeholder: '选项 B' }) .height(40).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) Text('+ 添加选项').fontSize(13).fontColor(C.primary).width('100%') .onClick(() => { promptAction.showToast({ message: '添加选项' }); }) } } .width('100%') .padding(14) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }4.1 条件渲染:本页最大亮点
if (this.qType === '单选' || this.qType === '多选') { Text('选项设置').fontSize(13).fontColor(C.textSub) TextInput({ placeholder: '选项 A' }) .height(40).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) TextInput({ placeholder: '选项 B' }) .height(40).backgroundColor(C.cardSoft).borderRadius(D.rSm) .placeholderColor(C.textDim).placeholderFont({ size: 14 }) Text('+ 添加选项').fontSize(13).fontColor(C.primary).width('100%') .onClick(() => { promptAction.showToast({ message: '添加选项' }); }) }if (this.qType === '单选' || this.qType === '多选')条件渲染——只有单选/多选显示"选项设置"区域,评分/填空不显示——"qType 决定 QuestionCard 的内容结构"。
4 种 qType 的不同呈现:
| qType | 显示内容 |
|---|---|
| 单选 | 题目输入 +选项 A/B 输入 + 添加选项 |
| 多选 | 题目输入 +选项 A/B 输入 + 添加选项(同单选) |
| 评分 | 题目输入(无选项,真实项目应显示 1-5 星级输入) |
| 填空 | 题目输入(无选项,真实项目应显示填空说明) |
"单选/多选 = 必有选项;评分/填空 = 无选项"——"题型决定内容"是问卷设计器的核心逻辑。
"或"条件的写法:qType === '单选' || qType === '多选'——两种题型共用"选项设置"区——"条件合并"简化代码(不用写else if分两支)。
4.2 选项的"半完整"实现
当前只显示 A/B 两个选项 + "+ 添加选项"按钮——
Text('+ 添加选项').fontSize(13).fontColor(C.primary).width('100%') .onClick(() => { promptAction.showToast({ message: '添加选项' }); })"+ 添加选项"是蓝色字——"文字按钮"(不是 Button 组件,是 Text 加 onClick)——点击弹 Toast "添加选项"——真实项目应动态添加 TextInput 到数组:
// 真实项目的动态选项 @State options: string[] = ['选项 A', '选项 B']; addOption() { this.options.push(`选项 ${String.fromCharCode(65 + this.options.length)}`); // 触发 ForEach 重渲染 this.options = [...this.options]; }"动态选项数组"——@State string[]数组 +push+ 整体替换触发渲染——**"动态表单字段"**是问卷设计器的进阶能力。
4.3 状态联动闭环
qType状态联动逻辑链:
用户点击 TypeCard 胶囊 → qType 切换("单选"→"评分") → build() 重执行 → QuestionCard 内的 `if` 重新判断 → "单选"显示选项区 / "评分"隐藏选项区"状态驱动 UI"是 ArkUI 的核心范式——qType一个状态驱动 QuestionCard 的整个结构——不需要手动操作 DOM,不需要隐藏/显示(if 直接控制渲染)——"条件渲染"比"显隐控制"更彻底。
对比 App 18 发现页:
- App 18:
selectedCat/ageRange/matchScope/anonymous4 个状态各自联动对应区域 - App 21:
qType一个状态联动 QuestionCard 整个结构(更彻底的"状态驱动")
"状态联动深度" = 状态影响 UI 结构的程度——App 21 的qType影响 5 个子组件("选项设置"标题、选项 A 输入、选项 B 输入、"+ 添加选项"按钮、潜在的其他选项),联动深度比 App 18 更高。
五、SubmitBtn:发布问卷
SubmitBtn 是发布按钮(无校验):
@Builder SubmitBtn() { Button('发布问卷') .width('100%').height(48) .fontSize(16).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() => { promptAction.showToast({ message: '问卷已发布' }); }) }"发布问卷"48vp 高蓝底白字按钮——点击弹 Toast "问卷已发布"——"无校验"(没有检查标题/题目是否填了)——真实项目应加校验:
- 标题非空
- 题目非空
- 单选/多选必须 ≥ 2 个选项
- 评分/填空必须有评分范围/输入说明
"4 道校验"是问卷发布按钮的完整校验——demo 只做了发布反馈,校验需补全。
六、@State 的"1 状态驱动大结构"
Func1Tab 只有1 个 @State:qType(题目类型)——"1 个状态驱动 QuestionCard 的整个结构":
| @State | 类型 | 用途 | 联动 |
|---|---|---|---|
qType | string | 题目类型(单选/多选/评分/填空) | TypeCard 高亮 + QuestionCard 条件渲染 |
"1 个 @State 影响 5+ 个 UI 元素"——**"@State 数量少但联动深"**是问卷设计器的特点——比"5 个 @State 各自联动 1 个区域"(App 13 创建表单)联动效率更高。
对比:
| 页面 | @State | 联动深度 |
|---|---|---|
| App 13 创建表单 | 5 | 每个联动 1-2 处 |
| App 18 发现页 | 5 | 每个联动 1-2 处 |
| App 21 创建问卷 | 1 | 联动 5+ 处(结构级) |
"1 个 @State 联动结构"是更高级的状态管理——**"少状态、强联动"**是问卷设计器的设计哲学。
七、问卷设计器的"类型字段"设计
App 21 的types: string[]是 4 个中文类型名——真实问卷平台的"题型字段"通常是枚举:
// 真实问卷平台的题型枚举 enum QuestionType { Single = 'single', // 单选 Multiple = 'multiple', // 多选 Rating = 'rating', // 评分 Text = 'text', // 填空 }"中文显示 + 英文存储"是问卷平台的最佳实践:
- 显示用中文("单选"更易读)
- 存储用英文(
single更稳定,不受本地化影响) - 渲染时映射:
typeDisplayMap[type]显示中文
App 21 直接用中文做状态值——demo 简化(不做 i18n),真实项目应分离"显示/存储"。
八、问卷设计器 vs 系列其他表单
App 21 与系列其他表单(App 13/14/15/16/17/19/20)的本质差异:
| 维度 | 系列其他表单 | App 21 问卷设计器 |
|---|---|---|
| 表单类型 | 填空式(用户填字段) | 选择题式(用户选结构) |
| 状态复杂度 | 多字段(5+) | 少状态但联动深(1 状态) |
| 字段类型 | 固定字段(标题/描述/日期) | 动态字段(题型/选项) |
| 业务 | 通用信息录入 | 问卷设计 |
"问卷设计器"是"动态表单"的代表——用户根据题型选择"画"出不同形态的问卷——比"填空式表单"复杂得多(一个问卷含多题,每题类型不同)。
真实问卷平台的"动态表单"实现:
- 题目数组
Question[]:每题有自己的 type/options/range ForEach渲染多题- 每题内根据
type条件渲染对应组件 - "添加题目"按钮往数组 push 一题
- "删除题目"按钮从数组 splice
"动态表单"是表单进阶——App 21 是简化版(一题一问卷),真实平台是多题多类型。
九、问卷填写页与创建页的"双向流程"
App 21 的创建页(Func1Tab)只能创建一道题——真实问卷是多题(一份问卷含 5-20 题)。但 App 21 的设计暗示了"双向流程":
- 创建页(创建问卷)→ 创作者视角,发布问卷
- 数据页(数据统计)→ 创作者视角,查看填写结果
- 首页(热门问卷)→ 填写者视角,参与问卷
- 我的页(我的问卷)→ 创作者视角,管理自己的问卷
"创建 → 展示 → 填写 → 数据"是问卷平台的完整生态——App 21 简化了一题/一问卷的版本,真实平台是"多题问卷" + "题库复用" + "模板市场"。
"创建-填写"双向流程值得读者在自己产品中设计——"创作者"和"消费者"在同一 App 切换视角(小红书 = 创建笔记/查看笔记、TikTok = 创建视频/观看视频、问卷星 = 创建问卷/填写问卷)。
十、总结
App 21 创建问卷页解析完毕。4 种题目类型 + qType 状态联动条件渲染 + 动态选项区是核心组件。**"1 个 @State 驱动 5+ 个 UI 元素"**是本页最大亮点——状态管理的"少而深"哲学。
"问卷设计器"是"动态表单"的代表——App 21 简化为一题/一问卷,真实平台支持多题多类型。**"题型 → 输入组件 → 校验规则"**的动态映射是问卷设计器的核心引擎。读者做表单类项目时,借鉴"qType 状态联动结构"的设计——比"5 个 @State 各自联动"更优雅、更易维护。
"问卷设计器"还是"问卷填写器"的模板——同一份 UI 配置,既是"创建者设计问卷"也是"填写者回答问卷"的源头——**"配置驱动 UI"**是问卷/表单类应用的核心架构思想。