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

Building dynamic forms with JSONForms and Kotli...

Gerard
April 25, 2025

Building dynamic forms with JSONForms and Kotlin Multiplatform

Building dynamic forms is never an easy job, especially for those forms that need to adapt in real time according to the user-specific context of language, location, or any other key input. During this talk, we go one step further by integrating JSONForms, a standard maintained by the Eclipse Foundation. While that may be primarily in web development, JSONForms provides a structured approach for the generation of forms, and we're going to see how one could create a native implementation both for Android and iOS using this standard.

The special thing about this session is that the Android and iOS implementation we are going to provide will be based on Kotlin Multiplatform, which enables us to share all the core logics and JSONForms standard implementations between both platforms. You will see how KMP can be used in your development so as to enable you to have a single code base for complex form logic while providing a native experience on both platforms.

We will cover how to solve some of the most common problems that a mobile developer has to face: working with forms that must dynamically show or hide sections, validate fields using predefined lists, regular expressions, and custom rules. In this talk, JSONForms and Kotlin Multiplatform are used for cross-platform, dynamic form generation.

Gerard

April 25, 2025
Tweet

More Decks by Gerard

Other Decks in Technology

Transcript

  1. A BETTER WAY ▸ Standard to describe a form, not

    specific to a need ▸ Use a declarative approach, not hard-coded ▸ Flexible UI rendering, driven by the logic ▸ Maintainable & scalable
  2. THE BUILDING BLOCKS Schema Data structure & Rules UiSchema Layout

    & Appearance Data Current values JSONForms Component / Renderer
  3. @Serializable data class Rule( val effect: Effect, val condition: Condition

    ) enum class Effect { Hide, Show, Disable, Enable } @Serializable data class Condition( val scope: String, val schema: ConditionSchema ) @Serializable data class ConditionSchema( val const: JsonPrimitive? = null, val enum: ImmutableList<String>? = null, val not: ConditionSchema? = null, val pattern: String? = null )
  4. @Composable fun RendererStringScope.Material3StringProperty( value: String?, modifier: Modifier = Modifier, error:

    String? = null, onValueChange: (String) !" Unit ) { !!# } @Composable fun RendererNumberScope.Material3NumberProperty( value: String?, modifier: Modifier = Modifier, error: String? = null, onValueChange: (String) !" Unit ) { !!# } @Composable fun RendererBooleanScope.Material3BooleanProperty( value: Boolean, modifier: Modifier = Modifier, onValueChange: (Boolean) !" Unit ) { !!# } @Composable fun RendererLayoutScope.Material3Layout( modifier: Modifier = Modifier, content: @Composable (UiSchema) !" Unit ) { !!# }
  5. @Composable fun RendererBooleanScope.Material3BooleanProperty( value: Boolean, modifier: Modifier = Modifier, onValueChange:

    (Boolean) !" Unit ) { when { isToggle() !" Switch( value = value, modifier = modifier, label = label(), description = description(), enabled = enabled(), onCheckedChange = onValueChange ) else !" Checkbox( value = value, modifier = modifier, label = label(), enabled = enabled(), onCheckedChange = onValueChange ) } } @Stable interface RendererBooleanScope { fun isToggle(): Boolean fun label(): String? fun description(): String? fun enabled(): Boolean }
  6. @Composable internal fun StringProperty( control: Control, schemaProvider: SchemaProvider, jsonFormState: JsonFormState,

    content: @Composable RendererStringScope.(id: String) !" Unit ) { val scope = remember(control) { RendererStringScopeInstance(control, schemaProvider, jsonFormState) } scope.content(control.propertyKey()) }
  7. @Composable internal fun Property( control: Control, schemaProvider: SchemaProvider, jsonFormState: JsonFormState,

    stringContent: @Composable (RendererStringScope.(id: String) !" Unit), numberContent: @Composable (RendererNumberScope.(id: String) !" Unit), booleanContent: @Composable (RendererBooleanScope.(id: String) !" Unit) ) { when (schemaProvider.getPropertyByControl<Property>(control)) { is StringProperty !" StringProperty( control = control, schemaProvider = schemaProvider, jsonFormState = jsonFormState, content = stringContent ) is BooleanProperty !" BooleanProperty( control = control, schemaProvider = schemaProvider, jsonFormState = jsonFormState, content = booleanContent ) is NumberProperty !" NumberProperty( control = control, schemaProvider = schemaProvider, jsonFormState = jsonFormState, content = numberContent ) is ObjectProperty !" error("Object property can't be specified in the layout") is ArrayProperty !" TODO() } }
  8. @Composable fun JsonForm( schema: Schema, uiSchema: UiSchema, modifier: Modifier =

    Modifier, state: JsonFormState = rememberJsonFormState(initialValues = mutableMapOf()), layoutContent: @Composable (RendererLayoutScope.(@Composable (UiSchema) !" Unit) !" Unit), stringContent: @Composable (RendererStringScope.(id: String) !" Unit), numberContent: @Composable (RendererNumberScope.(id: String) !" Unit), booleanContent: @Composable (RendererBooleanScope.(id: String) !" Unit) ) { val schemeProvider = rememberSchemeProvider(uiSchema = uiSchema, schema = schema) Box(modifier = modifier) { Layout( uiSchema = uiSchema, jsonFormState = state, layoutContent = layoutContent, content = { control !" Property( control = control, schemaProvider = schemeProvider, jsonFormState = state, stringContent = stringContent, numberContent = numberContent, booleanContent = booleanContent ) } ) } }
  9. JsonForm( schema = schema, uiSchema = uiSchema, state = state,

    layoutContent = { Material3Layout(content = it) }, stringContent = { id !" val value = state[id].value as String? val error = state.error(id = id).value Material3StringProperty( value = value, error = error!$message, onValueChange = { state[id] = it } ) }, numberContent = {}, booleanContent = {} )
  10. OUR KOTLIN MULTIPLATFORM ARCHITECTURE JSONForms shared KMP ui Material3 Renderer

    material3 Your Renderer Set Custom Renderer Apple Renderer cupertino Custom Renderer
  11. @Composable fun RendererStringScope.CupertinoStringProperty( value: String?, modifier: Modifier = Modifier, error:

    String? = null, onValueChange: (String) !" Unit ) { !!# } @Composable fun RendererNumberScope.CupertinoNumberProperty( value: String?, modifier: Modifier = Modifier, error: String? = null, onValueChange: (String) !" Unit ) { !!# } @Composable fun RendererBooleanScope.CupertinoBooleanProperty( value: Boolean, modifier: Modifier = Modifier, onValueChange: (Boolean) !" Unit ) { !!# } @OptIn(ExperimentalCupertinoApi!%class) @Composable fun RendererLayoutScope.CupertinoLayout( modifier: Modifier = Modifier, content: @Composable (UiSchema) !" Unit ) { !!# }
  12. @Composable fun RendererStringScope.CupertinoStringProperty( value: String?, modifier: Modifier = Modifier, error:

    String? = null, onValueChange: (String) !" Unit ) { when { isRadio() !" SegmentedControl( !!# ) isDropdown() !" WheelPicker( !!# ) else !" OutlinedTextField( !!# ) } }
  13. IN CONCLUSION ▸ Standardize complex forms with JSONForms implementation ▸

    Respect the JSONForms architecture for customization purpose ▸ Deliver a native user experience ▸ Accelerate development and our rollout ▸ Improve maintainability & scalability