Complex Declarative UIs

Hi!

I’m playing with the idea to add a state-based automatic UI rebuild (a.k.a declarative UI) to my framework (https://zakadabar.io), mostly for the JS target.

I used React before and also I’ve checked Compose (core and web) but they do not solve my base problem: handling complex (i.e. 50+ fields) forms effectively. This is not a problem for mobiles where you rarely handle such a complexity, but it is a problem on the web.

As I see all these solutions are great when you start, but when you have to handle complex relations between components, you have to add so much supporting code that they loose their main advantages.

I’ve been thinking about this and my conclusion at the moment is that the problem is that all these frameworks are not capable to delegate fragments of states to lower level components.

Let me explain with an example:

  • You have a list of cards.
  • Each card has an input field.
  • What happens when the user changes the value in the input field?

Most probably you pass a callback to the Input component that sends an event to change the state somewhere else.

And this “callback” and “somewhere else” is my real problem.

As I see, the card and the input field should get a “state fragment” and change it without knowing what happens after the change. The change itself (a.value = 12) should notify the framework that there has been a state change, and from then it is basically the same as React or Compose.

I would be grateful if anyone could point me to information about how to handle complexity without adding many-many boilerplate, stores, actions (been there, done that) just to edit one record type.

I know that I could solve this by tailoring the Compose compiler plugin, but I’m not sure that it is the best options, hence asking here.

Thank you for your help.

1 Like

I think you would need to be more specific on what is the problem with a form with 50+ fields. And how your own concept improves on such situations which is impossible or harder to do with Compose/React.

If this is really what you think would greatly improve the situation then… well, this is interesting, because for me both of these solutions are effectively the same :smiley: Such “state fragment” is just a wrapper around a value and its change listener, but I think everything else is exactly the same. You are concerned that the change event affects the state “somewhere else”, but at the same time you say it is advantage that the component does not know what happens when it modifies the “state fragment”. Aren’t these two exactly the same things?

Having said that, I think what you need is actually possible, at least with Compose. Just create a list of e.g. MutableState<String> objects, pass them to cards/inputs, modify there and whoever observe them will be notified. I think in such simple cases this is actually worse, because we need to create an additional wrapper instead of just providing a listener, but if you really like this approach, then I think you could do this.

My goal is to avoid the boilerplate code required by the current techniques.

Right now my forms looks like this (fully functional code with data validation, automatic translation, etc. based on the data model):

class MyForm : ZkForm<DataModel>() {
    override fun onCreate() {
        super.onCreate()
        build(translate<SimpleExampleForm>()) {
            + section(strings.basics) {
                + bo::field1
                // ...
                + bo::field50
            }
        }
     }
}

Resulting in something like this:

I would like to convert it to:

@Composable
fun MyDeclarativeForm(bo : DataModel) {
    FormFrame(translate<SimpleExampleForm>()) {
       Section(string.basics) {
           + bo::field1
           ///...
           + bo::field50
        }
    }
}

The point is that the binding between the components and the data model is trivial and it should be automatic. I think this does not go against the principles of declarative UI and state management.

I feel that it is counter-intuitive to write something like this:

@Composable
fun MyDeclarativeForm(bo : DataModel) {
    FormFrame(translate<SimpleExampleForm>()) {
        Section(string.basics) {
            TextField(
                bo.field1, 
                mandatory = false, 
                label = translate("field1"), 
                onChange = { value -> 
                    context.changeField(DataModel::field1, bo) // this is just a guess, how to do it
                }
            )
            // ...
            TextField(
                 bo.field1,
                 mandatory = true,
                 label = translate("field1"),
                 onChange = { value -> 
                     context.changeField(DataModel::field50, bo)
                 }
            )
        }
    }
}

The code above actually contains errors. The second field shows the wrong data and shows the wrong label because I’ve cut and pasted the code and forgot not change everything. I’ve noticed just before sending the post and decided not to fix as they show my exact point.

Please note, that mandatory and label are automatically handled by the current system, based on the data schema. These somewhat ugly field definitions are functionally equivalent to my current + bo::field50.

Also, I would like to avoid the observer pattern (the manual coding of it field by field). I’ve seen bad things when my colleagues used it so I did some research. According to an Adobe study:

  • 1/3 of the code in Adobe’s desktop applications is devoted to event handling logic
  • 1/2 of the bugs reported during a product cycle exist in this code

Source: Deprecating the Observer Pattern

I hope this clarifies my original question a bit.

EDIT: Removed the plus signs from the compose versions of the code. I’m not sure I really want to remove them but compose does not use them. Also, added some line breaks so it is easier to read.

It is hard to follow you without the knowledge about your framework. But generally it seems to me that you just created some magic to automatically create UI elements (from the model?). I don’t see the reason why the same can’t be done with Compose. There would be probably some limitations like e.g. field1 couldn’t be a String, but would have to be MutableState<String>, but this is performance optimization, because otherwise re-drawing would be pretty heavy. Still, I believe you should be able to cut the code into one line per field.

Also, I think there is no need to convince anyone to use reactive programming over raw events handling. This is exactly the goal of Compose.

Thanks for the discussion, it gave me the idea how to proceed.

Unfortunately, when I tried to write a PoC for, it I realised that compose is IR only and the JS IR compiler is not compatible with the “noarg” plugin (throws internal compliler error), so I can’t use Compose as of now.

EDIT: Ah, after some reading, I’ve found that kotlinx.serialization should not be used with compose for web anyway. Rather disappointing to be honest.

The combination of Compose and kotlinx.serialization works, even within the same multiplatform project. Previously, you had to create a sub-project for serialization, but this is no longer required. I’m actually using the combination (specifically Compose for Web) successfully.

In addition, I see no scalability problems with Compose, such as supporting a larger number of fields or nested structures. Proper state management should be part of any non-trivial application’s design, but once this is in place, Compose’s declarative approach works nicely and reduces overall complexity significantly. As Compose for Web is still a technology preview, performance might be an issue, but will surely get resolved.

1 Like

Ah, thank you, this is good news. I’ll try to give it a shot if I can solve the IR compiler crashing on noarg.

I would like to avoid going into duplicating the compose plugin as it would need continuous maintenance.