You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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的多布局类型来处理不同问题:

  1. 先为每种问题类型创建对应的布局文件(比如item_single_choice.xml、item_text.xml)
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:05:07