SurveyKit-compose
A Server Driven UiKit made with jetpack compose to make beautiful ui’s without extra engineering efforts.
Demo Video
Features
- Ui can be rendered from Backend.
- Each components are solely responsible.
- No coupling between components.
- Easy to customize.
Lessons Learned
Learned the server driven architecture. Also tried to remove all possible coupling and each component is singularly responsible for their usecase.
Roadmap
- Adding more components support.
- will add dynamic positioning to the components
Important Notes
- All Widgets need their configs which needs to be extended from WidgetConfig which had some common properties for all configs.
- widgetId should be unique to all widgets.
interface WidgetConfig { val widgetId:String // Should be unique val topPadding:Int // Should in int i.e if padding is 24dp then = 24 val bottomPadding:Int val startPadding:Int val endPadding:Int val widgetDimens:WidgetDimens } data class WidgetDimens( val fillWidth:Boolean?, // If this is given our widget fill cover parent val fillHeight:Boolean?, val width : Int?, // specifying the widget width val height : Int?, )
Architectural Pattern
All available components
TextWidget
val config = TextWidgetConfig( text = "This is an example of TextWidgetConfig", topPadding = 30, startPadding = 30, textConfig = TextConfig( color = colorBlack.toHex(), fontSize = 20, fontWeight = "semiBold" ), widgetDimens = WidgetDimens( true, null, null, 100 ) ) TextWidget(config)
ProgressBarWidget
val config = ProgressBarWidgetConfig( progressColor = colorBlue.toHex(), bgColor = Color.LightGray.toHex(), widgetDimens = WidgetDimens( true, null, null, 14 ), topPadding = 100, bottomPadding = 100 ) ProgressBarWidget(progress = { 0.5f }, config = config)
ButtonWidget
val config = ButtonWidgetConfig( bgColor = colorBlue.toHex(), btnText = TextWidgetConfig( text = "I am a button", textConfig = TextConfig( colorWhite.toHex(), 16, "bold" ), topPadding = 8, bottomPadding = 8 ), borderRadius = 12, borderStroke = 0, borderColor = colorWhite.toHex(), topPadding = 10, bottomPadding = 10, startPadding = 24, endPadding = 24, widgetDimens = WidgetDimens( fillWidth=true, fillHeight = null, width = null, height = 100 ) ) ButtonWidget(config = config, onCtaClick = { //Button is clicked })
EditTextWidget
val config = EditTextWidgetConfig( widgetDimens = WidgetDimens(true, fillHeight = false, width = null, height = 300), startPadding = 24, endPadding = 24, topPadding = 100, bottomPadding = 100, bgColor = colorUnSelected.toHex(), borderColor = colorBlue.toHex(), borderStroke = 4, textColor = "#000000", ) EditTextWidget(config = config, errorString = {""}, userInput = { // Here we will get user input in this EditTextWidget })
ImageWidget
val config = ImageWidgetConfig( widgetDimens = WidgetDimens(true, fillHeight = false, width = null, height = 300), startPadding = 24, endPadding = 24, topPadding = 10, bottomPadding = 10, borderColor = colorBlue.toHex(), borderStroke = 4, borderRadius = 20, imageUrl = "https://images.unsplash.com/photo-1574169208507-84376144848b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=879&q=80", ) ImageWidget(config = config)
OptionsWidget
val config = OptionsWidgetConfig( optionsList = listOf("Option1", "Option2", "Option3"), unSelectedButtonConfig = ButtonWidgetConfig( startPadding = 24, endPadding = 24, topPadding = 22, widgetDimens = WidgetDimens(true), btnText = TextWidgetConfig( text = "", textConfig = TextConfig( colorUnSelectedText.toHex(), 16, "semiBold" ), startPadding = 4, endPadding = 4, topPadding = 12, bottomPadding = 12, widgetDimens = WidgetDimens(true) ), bgColor = colorUnSelectedArea.toHex() ), selectedButtonConfig = ButtonWidgetConfig( startPadding = 24, endPadding = 24, topPadding = 22, widgetDimens = WidgetDimens(true), btnText = TextWidgetConfig( text = "", textConfig = TextConfig( colorBlue.toHex(), 16, "semiBold" ), startPadding = 4, endPadding = 4, topPadding = 12, bottomPadding = 12, widgetDimens = WidgetDimens(true) ), bgColor = colorWhite.toHex(), borderStroke = 4, borderColor = colorBlue.toHex() ), widgetDimens = WidgetDimens(true, fillHeight = false, width = null, height = 400), startPadding = 24, endPadding = 24, topPadding = 10, bottomPadding = 10, multipleSelection = true, ) OptionsWidget(config = config) { // Here we'll get the list of string }
Usage
This is an example function which uses these widget. In this comp function, widget list is passed from the B.E which we need to construct on frontend first.
@Composable fun SurveyKit(widgetList: MutableList<List<WidgetConfig>>) { val context = LocalContext.current // currIdx is an Int representing the index of the current question in a survey. var currIdx by rememberSaveable { mutableStateOf(0) } // etInput is a String representing the user input in an EditText var etInput by rememberSaveable { mutableStateOf("") } // errorString is a String that stores any error messages generated during the user input validation process. var errorString by rememberSaveable { mutableStateOf("") } // currentQuestionNumber is an Int representing the current question number in the survey. var currentQuestionNumber by rememberSaveable { mutableStateOf(1) } LazyColumn( Modifier.fillMaxSize().background(colorWhite) ) { items(widgetList.getOrNull(currIdx) ?: emptyList()) { widget-> when (widget.widgetId) { Widgets.CurrentQuestionWidgetId.widgetName -> CurrentQuestionWidget( config = widget as CurrentQuestionWidgetWidgetConfig, currentQuestion = { currentQuestionNumber }, totalQuestion = widgetList.size) Widgets.ProgressBarWidgetId.widgetName -> ProgressBarWidget( config = widget as ProgressBarWidgetConfig, progress = { currentQuestionNumber/widgetList.size.toFloat() }) Widgets.TextWidgetId.widgetName -> TextWidget(config = widget as TextWidgetConfig) Widgets.ImageWidgetId.widgetName -> ImageWidget(config = widget as ImageWidgetConfig) Widgets.EditTextWidgetId.widgetName -> EditTextWidget( config = widget as EditTextWidgetConfig, errorString = { errorString }, userInput = { str -> etInput = str errorString = "" }) Widgets.CtaButtonWidgetId.widgetName -> ButtonWidget(config = widget as ButtonWidgetConfig) { if (etInput.isEmpty()) { errorString = "Please, enter some answer" return@ButtonWidget } etInput = "" currentQuestionNumber++ if (currIdx != widgetList.size - 1) currIdx++ } Widgets.OptionsWidgetId.widgetName -> OptionsWidget(config = widget as OptionsWidgetConfig) { it.toString().toast(context) } } } } }
Json to UI example
[ { "bottomPadding": 0, "endPadding": 16, "prefixText": "Question", "primaryTextConfig": { "color": "#000000", "fontSize": 20, "fontWeight": "bold" }, "secondaryTextConfig": { "color": "#000000", "fontSize": 16, "fontWeight": "light" }, "startPadding": 16, "topPadding": 16, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "cqWidget" }, { "bgColor": "#EAF2FD", "bottomPadding": 0, "endPadding": 20, "progressColor": "#2F80ED", "startPadding": 20, "topPadding": 22, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": 12, "width": null }, "widgetId": "pbWidget" }, { "bottomPadding": 0, "endPadding": 24, "startPadding": 24, "text": "In the above image, please tell what it is in less than 500 words?", "textConfig": { "color": "#000000", "fontSize": 20, "fontWeight": "bold" }, "topPadding": 18, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "textWidget" }, { "bottomPadding": 0, "endPadding": 0, "multipleSelection": true, "optionsList": [ "Option 1", "Option 2", "Option 3", "Option 4" ], "selectedButtonConfig": { "bgColor": "#FFFFFF", "borderColor": "#2F80ED", "borderRadius": 12, "borderStroke": 4, "bottomPadding": 0, "btnText": { "bottomPadding": 0, "endPadding": 0, "startPadding": 0, "text": "", "textConfig": { "color": "#2F80ED", "fontSize": 16, "fontWeight": "semiBold" }, "topPadding": 0, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "textWidget" }, "endPadding": 24, "startPadding": 24, "topPadding": 22, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "ctaButtonWidget" }, "startPadding": 0, "topPadding": 0, "unSelectedButtonConfig": { "bgColor": "#D9D9D9", "borderColor": "#FFFFFF", "borderRadius": 12, "borderStroke": 0, "bottomPadding": 0, "btnText": { "bottomPadding": 0, "endPadding": 0, "startPadding": 0, "text": "", "textConfig": { "color": "#000000", "fontSize": 16, "fontWeight": "semiBold" }, "topPadding": 0, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "textWidget" }, "endPadding": 24, "startPadding": 24, "topPadding": 22, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "ctaButtonWidget" }, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": 320, "width": null }, "widgetId": "optionsWidget" }, { "bgColor": "#FFFFFF", "borderColor": "#000000", "borderRadius": 16, "borderStroke": 2, "bottomPadding": 0, "endPadding": 24, "hintTextConfig": { "bottomPadding": 0, "endPadding": 0, "startPadding": 0, "text": "enter your answer here...", "textConfig": { "color": "#000000", "fontSize": 16, "fontWeight": "light" }, "topPadding": 0, "widgetDimens": { "fillHeight": null, "fillWidth": null, "height": null, "width": null }, "widgetId": "textWidget" }, "startPadding": 24, "textColor": "#000000", "topPadding": 23, "widgetDimens": { "fillHeight": false, "fillWidth": true, "height": 300, "width": null }, "widgetId": "etWidget" }, { "bgColor": "#2F80ED", "borderColor": "#FFFFFF", "borderRadius": 12, "borderStroke": 0, "bottomPadding": 32, "btnText": { "bottomPadding": 0, "endPadding": 0, "startPadding": 0, "text": "Next", "textConfig": { "color": "#FFFFFF", "fontSize": 16, "fontWeight": "semiBold" }, "topPadding": 0, "widgetDimens": { "fillHeight": null, "fillWidth": null, "height": null, "width": null }, "widgetId": "textWidget" }, "endPadding": 24, "startPadding": 24, "topPadding": 48, "widgetDimens": { "fillHeight": null, "fillWidth": true, "height": null, "width": null }, "widgetId": "ctaButtonWidget" } ]