如何使用Retrofit从JSON填充问卷动态布局?
嘿,这个需求我之前帮不少开发者落地过,核心就是用数据驱动UI——把后端返回的JSON结构和前端的UI组件做映射,下面我分步骤拆解具体实现,拿Web(React)和Android(Kotlin)举例子,你可以对应自己的技术栈调整:
一、先约定标准的JSON数据结构(关键前提)
首先得和后端对齐问题的字段规范,每个问题至少要包含「类型、ID、标题、选项(可选)、是否必填」这些核心信息,举个典型的JSON示例:
{ "surveyId": "dev_survey_2024", "questions": [ { "id": "q1", "type": "single_choice", "title": "你常用的客户端开发技术栈是?", "options": [ {"value": "android_kotlin", "label": "Android(Kotlin)"}, {"value": "ios_swift", "label": "iOS(Swift)"}, {"value": "flutter", "label": "Flutter"} ], "required": true }, { "id": "q2", "type": "text", "title": "你最想学习的技术方向?", "placeholder": "比如:大模型应用开发", "required": false }, { "id": "q3", "type": "multiple_choice", "title": "你常用的开发工具?", "options": [ {"value": "android_studio", "label": "Android Studio"}, {"value": "xcode", "label": "Xcode"}, {"value": "vscode", "label": "VS Code"} ], "required": true } ] }
二、获取并解析JSON数据
不管什么平台,第一步都是请求接口拿到数据,再转成对应的数据模型(强类型语言)或者直接用对象(弱类型语言)。
Web(React + Fetch)示例
// 发起请求获取问卷数据 async function fetchSurveyData() { try { const response = await fetch('/api/get-survey'); const surveyData = await response.json(); // 把数据存入组件状态,供后续渲染使用 setSurvey(surveyData); } catch (error) { console.error('加载问卷失败:', error); alert('问卷加载失败,请稍后重试'); } } // 在组件挂载时调用 useEffect(() => { fetchSurveyData(); }, []);
Android(Kotlin + Retrofit)示例
先定义数据模型类:
data class Survey( val surveyId: String, val questions: List<Question> ) data class Question( val id: String, val type: String, val title: String, val options: List<Option>?, // 单选/多选才有选项,文本类型为null val required: Boolean, val placeholder: String? = null ) data class Option( val value: String, val label: String )
再用Retrofit请求接口:
interface SurveyApi { @GET("survey") suspend fun getSurvey(): Survey } // 在ViewModel中发起请求 viewModelScope.launch { try { val survey = surveyApi.getSurvey() _surveyLiveData.postValue(survey) } catch (e: Exception) { // 处理请求错误,比如通知UI显示重试按钮 _errorLiveData.postValue("加载问卷失败") } }
三、根据问题类型动态生成UI组件
这是核心环节——根据每个问题的type字段,渲染对应的UI控件。
Web(React)实现
写一个通用的问题渲染组件,通过switch分支处理不同类型:
function QuestionCard({ question, onAnswerChange }) { const handleUpdate = (value) => { onAnswerChange(question.id, value); }; switch (question.type) { // 单选问题 case 'single_choice': return ( <div className="question-card"> <h4>{question.title} {question.required && <span className="required">*</span>}</h4> <div className="option-group"> {question.options?.map(opt => ( <label key={opt.value}> <input type="radio" name={question.id} value={opt.value} onChange={(e) => handleUpdate(e.target.value)} required={question.required} /> {opt.label} </label> ))} </div> </div> ); // 文本输入问题 case 'text': return ( <div className="question-card"> <h4>{question.title} {question.required && <span className="required">*</span>}</h4> <input type="text" placeholder={question.placeholder} onChange={(e) => handleUpdate(e.target.value)} required={question.required} /> </div> ); // 多选问题 case 'multiple_choice': return ( <div className="question-card"> <h4>{question.title} {question.required && <span className="required">*</span>}</h4> <div className="option-group"> {question.options?.map(opt => ( <label key={opt.value}> <input type="checkbox" value={opt.value} onChange={(e) => { // 多选需要维护选中数组,这里简化处理 const current = answers[question.id] || []; handleUpdate(e.target.checked ? [...current, opt.value] : current.filter(v => v !== opt.value)); }} required={question.required} /> {opt.label} </label> ))} </div> </div> ); // 可扩展更多类型:比如评分星、日期选择器等 default: return <div className="question-card">暂不支持该问题类型</div>; } } // 父组件中遍历渲染所有问题 function SurveyPage() { const [survey, setSurvey] = useState(null); const [answers, setAnswers] = useState({}); const handleAnswerChange = (qId, value) => { setAnswers(prev => ({ ...prev, [qId]: value })); }; if (!survey) return <div className="loading">加载中...</div>; return ( <div className="survey-container"> <h2>开发者调查问卷</h2> {survey.questions.map(q => ( <QuestionCard key={q.id} question={q} onAnswerChange={handleAnswerChange} /> ))} <button onClick={() => submitAnswers(answers)}>提交问卷</button> </div> ); }
Android(Kotlin + RecyclerView)实现
用RecyclerView的多布局类型来处理不同问题:
- 先为每种问题类型创建对应的布局文件(比如
item_single_choice.xml、item_text.xml) - 在Adapter中重写
getItemViewType区分类型,再绑定对应的ViewHolder:
class SurveyAdapter( private val questions: List<Question>, private val onAnswerChanged: (String, Any) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { private const val TYPE_SINGLE_CHOICE = 0 private const val TYPE_TEXT = 1 private const val TYPE_MULTIPLE_CHOICE = 2 } override fun getItemViewType(position: Int): Int { return when (questions[position].type) { "single_choice" -> TYPE_SINGLE_CHOICE "text" -> TYPE_TEXT "multiple_choice" -> TYPE_MULTIPLE_CHOICE else -> throw IllegalArgumentException("未知问题类型") } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { TYPE_SINGLE_CHOICE -> SingleChoiceVH(inflater.inflate(R.layout.item_single_choice, parent, false)) TYPE_TEXT -> TextVH(inflater.inflate(R.layout.item_text, parent, false)) TYPE_MULTIPLE_CHOICE -> MultipleChoiceVH(inflater.inflate(R.layout.item_multiple_choice, parent, false)) else -> throw IllegalArgumentException("未知布局类型") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val question = questions[position] when (holder) { is SingleChoiceVH -> holder.bind(question, onAnswerChanged) is TextVH -> holder.bind(question, onAnswerChanged) is MultipleChoiceVH -> holder.bind(question, onAnswerChanged) } } override fun getItemCount() = questions.size // 单选问题ViewHolder class SingleChoiceVH(itemView: View) : RecyclerView.ViewHolder(itemView) { private val titleTv: TextView = itemView.findViewById(R.id.tv_title) private val radioGroup: RadioGroup = itemView.findViewById(R.id.rg_options) fun bind(question: Question, onAnswerChanged: (String, Any) -> Unit) { titleTv.text = "${question.title}${if (question.required) " *" else ""}" radioGroup.removeAllViews() question.options?.forEach { opt -> val radioBtn = RadioButton(itemView.context).apply { text = opt.label tag = opt.value } radioGroup.addView(radioBtn) } radioGroup.setOnCheckedChangeListener { _, checkedId -> val selectedValue = itemView.findViewById<RadioButton>(checkedId)?.tag as String onAnswerChanged(question.id, selectedValue) } } } // 文本输入问题ViewHolder class TextVH(itemView: View) : RecyclerView.ViewHolder(itemView) { private val titleTv: TextView = itemView.findViewById(R.id.tv_title) private val inputEt: EditText = itemView.findViewById(R.id.et_input) fun bind(question: Question, onAnswerChanged: (String, Any) -> Unit) { titleTv.text = "${question.title}${if (question.required) " *" else ""}" inputEt.hint = question.placeholder inputEt.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { onAnswerChanged(question.id, s.toString()) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} }) } } // 多选问题ViewHolder(代码类似,这里简化) class MultipleChoiceVH(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(question: Question, onAnswerChanged: (String, Any) -> Unit) { // 实现逻辑:动态添加CheckBox,维护选中数组并回调 } } }
四、表单验证与提交
提交前要校验必填项是否已填写,再把答案提交回后端:
Web(React)示例
function submitAnswers(answers) { // 校验必填项 const missingRequired = survey.questions.filter(q => q.required && !answers[q.id]); if (missingRequired.length > 0) { alert(`请完成以下必填问题:${missingRequired.map(q => q.title).join(', ')}`); return; } // 提交到后端 fetch('/api/submit-survey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ surveyId: survey.surveyId, answers }) }) .then(res => res.json()) .then(() => alert('提交成功!')) .catch(err => console.error('提交失败:', err)); }
五、扩展建议
- 新增问题类型:比如评分星、日期选择器、下拉框,只需要在UI分支中添加对应逻辑
- 状态持久化:用LocalStorage(Web)或SharedPreferences(Android)保存已填答案,避免用户中途丢失
- 样式优化:统一不同问题类型的布局样式,提升用户体验
- 错误兜底:网络失败时显示重试按钮,JSON解析出错时提示用户
内容的提问来源于stack exchange,提问作者Garrima Kakkr
相关产品推荐
相关产品推荐

