Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Form Frenzy to Form Framework

Form Frenzy to Form Framework

Forms are at the core of many user experiences—from onboarding to payments—but building scalable, and reusable form UIs comes with unique challenges.

In this talk, I’ll take you behind the scenes of our journey from repeatedly rebuilding similar form screens, it's business layer to building a reusable form architecture that balances dynamic UI rendering, validation, and state management effectively.

Through real-world implementation details, we’ll discuss:
• Our Journey with Forms & Why We Built This
•⁠ How we built a Dynamic Form System with Compose
•⁠ ⁠State Management & UDF in Forms: Balancing immutability, Compose state holders, and Flow for efficient reactivity.
•⁠ Validation & Navigation: Ensuring a seamless user experience with dynamic validation & navigation.
•⁠ ⁠Extensibility & Reusability: How to allow different teams to swap out repositories while using shared UI & logic to build their form UI
•⁠ ⁠Lessons Learned: Performance optimisations, API design pitfalls, and Compose-specific challenges.

By the end of this talk, you’ll have a blueprint for designing a scalable, reusable form system—one that makes it easier to build forms that are composable, testable, and adaptable to different use cases. 🚀

Avatar for Akshay Chordiya

Akshay Chordiya

October 31, 2025
Tweet

More Decks by Akshay Chordiya

Other Decks in Programming

Transcript

  1. • Turn repeated thing into a reusable system • Build

    a dynamic form architecture with Compose • Share learnings which you can use 🗺 🎯 Goal of the talk
  2. • A screen or a sheet which shows • Questions

    • Static Information What is a Form 📝
  3. • - BE driven UI = A wild ride 🎢

    • + Reusable UI components 🤡 • + Proposed an engine to create entire Form flows quickly ⚡ 👔 Stakeholder buy-in
  4. 👔 Stakeholder buy-in Rest of team Screen 1 Screen 2

    Screen 3 Proposal MVP FormKit engineer
  5. Blocks 🧱 interface FormElement { val id: String var visibility:

    Visibility } FormContent Form FormElement
  6. Blocks 🧱 interface FormElement { val id: String var visibility:

    Visibility } interface FormQuestion<T : FormAnswer> : FormElement { var answer: T? val isOptional: Boolean var showError: Boolean fun validate() } FormContent FormElement Form FormQuestion FormQuestion
  7. Blocks 🧱 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  8. Blocks 🧱 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  9. Blocks 🧱 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  10. Blocks 🧱 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  11. Blocks 🧱 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  12. Blocks 🧱 @Composable fun FreeTextContent( state: FormQuestion.FreeText, onTextChanged: (String) ->

    Unit ) { TextField( label = state.label, value = state.answer?.text ?: "", onTextChanged = onTextChanged, hint = state.hint, isError = state.showError, ) }
  13. Blocks 🧱 FormStepType.Form( title = question.title, description = question.markdownDescription, elements

    = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = “Enter what you hit”, ), .. ), primaryButtonText = "Next" )
  14. Blocks 🧱 FormStepType.Form( title = question.title, description = question.markdownDescription, elements

    = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = “Enter what you hit”, ), .. ), primaryButtonText = "Next" )
  15. Navigator 🧭 interface FormNavigator { val currentScreen: StateFlow<FormScreen> val steps:

    List<FormScreen> fun goNext(onLastStep: () -> Unit) fun jumpTo(screen: FormScreen) fun goBack(onFirstStep: () -> Unit) }
  16. interface FormNavigator { val currentScreen: StateFlow<FormScreen> val steps: List<FormScreen> fun

    goNext(onLastStep: () -> Unit) fun jumpTo(screen: FormScreen) fun goBack(onFirstStep: () -> Unit) } Navigator 🧭
  17. interface FormNavigator { val currentScreen: StateFlow<FormScreen> val steps: List<FormScreen> fun

    goNext(onLastStep: () -> Unit) fun jumpTo(screen: FormScreen) fun goBack(onFirstStep: () -> Unit) } Navigator 🧭
  18. interface FormNavigator { val currentScreen: StateFlow<FormScreen> val steps: List<FormScreen> fun

    goNext(onLastStep: () -> Unit) fun jumpTo(screen: FormScreen) fun goBack(onFirstStep: () -> Unit) } Navigator 🧭
  19. Navigator 🧭 override fun goNext(onLastStep: () -> Unit) { if

    (currentStepIndex < steps.size - 1) { currentStepIndex++ } } override fun goBack(onFirstStep: () -> Unit) { if (currentStepIndex > 0) { currentStepIndex-- } }
  20. Navigator 🧭 private val _currentScreen: MutableStateFlow<FormScreen> = MutableStateFlow() override val

    currentScreen: StateFlow<FormScreen> = _currentScreen.asStateFlow() private var currentStepIndex = 0 set(value) { field = value _currentScreen.value = steps[currentStepIndex] }
  21. internal enum class VehicleDamageScreen : FormScreen { WasYourVehicleDamaged, DescribeYourVehicleDamage, LevelOfDamage,

    ExistingVehicleDamage, AreYouHappyWithApprovedRepairer, VehicleDamageSummary, } Navigator 🧭 Example
  22. internal enum class VehicleDamageScreen : FormScreen { WasYourVehicleDamaged, DescribeYourVehicleDamage, LevelOfDamage,

    ExistingVehicleDamage, AreYouHappyWithApprovedRepairer, VehicleDamageSummary, } Navigator 🧭 Example
  23. • Provides a common way to validate each question •

    Each question type provides it’s own validation logic Validator 👮
  24. Validator 👮 class FreeText( val title: String? = null, override

    val isOptional: Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) }
  25. class FreeText( val title: String? = null, override val isOptional:

    Boolean = false, ) : FormQuestion<FormAnswer.FreeText> { override var answer by mutableStateOf(FormAnswer.FreeText("")) override var showError by mutableStateOf(false) override fun validate() { val text = answer?.text.orEmpty() showError = text.isNullOrBlank() } }
  26. interface FormValidator { fun validate(form: FormStepType.Form): Boolean { val questions

    = form.elements.filterIsInstance<FormQuestion<*>>() return validate(questions) } }
  27. interface FormValidator { fun validate(form: FormStepType.Form): Boolean { val questions

    = form.elements.filterIsInstance<FormQuestion<*>>() return validate(questions) } }
  28. interface FormValidator { fun validate(form: FormStepType.Form): Boolean { val questions

    = form.elements.filterIsInstance<FormQuestion<*>>() return validate(questions) } fun validate(questions: List<FormQuestion<*>): Boolean { return questions .filter { it.isVisible && !it.isOptional } .onEach { it.validate() } .none { it.showError } } }
  29. interface FormValidator { fun validate(form: FormStepType.Form): Boolean { val questions

    = form.elements.filterIsInstance<FormQuestion<*>>() return validate(questions) } fun validate(questions: List<FormQuestion<*>): Boolean { return questions .filter { it.isVisible && !it.isOptional } .onEach { it.validate() } .none { it.showError } } }
  30. private fun mapDescribeVehicleDamage(response: Response): FormStepType.Form { return FormStepType.Form( title =

    response.title, description = response.description, elements = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = "Describe the damage" ) ), primaryButtonText = "Next" ) } Example
  31. private fun mapDescribeVehicleDamage(response: Response): FormStepType.Form { return FormStepType.Form( title =

    response.title, description = response.description, elements = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = "Describe the damage" ) ), primaryButtonText = "Next" ) } Example
  32. private fun mapDescribeVehicleDamage(response: Response): FormStepType.Form { return FormStepType.Form( title =

    response.title, description = response.description, elements = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = "Describe the damage" ) ), primaryButtonText = "Next" ) } Example
  33. private fun mapDescribeVehicleDamage(response: Response): FormStepType.Form { return FormStepType.Form( title =

    response.title, description = response.description, elements = listOf( FormQuestion.FreeText( id = DESCRIBE_DAMAGE, hint = "Describe the damage" ) ), primaryButtonText = "Next" ) } Example
  34. Takeaways 🥡 • Work with stakeholders to get buy-in 🎰

    • Keep it MVP! 🐣 • Over-engineering vs natural progression 📈 • Blueprint to create your own form/UI framework 🗺 • KMP/Open Source [TBD]