This is the second day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Following the release of Compose for Android 1.0 in late July, JetBrains announced the latest development for its Compose Multiplatform, which is now in alpha, on August 4.

Compose, as a declarative UI framework, is platform independent for most of its features, except for the rendering part. Kotlin, in particular, is a cross-platform language that has long laid the foundation for future CROSS-platform UIs.

Compose Multiplatform will integrate three existing Compose projects: In the future, Android, Desktop and Web applications can be developed in one Project just like Kotlin Multiplatform Project. The unified declarative paradigm enables code to be reused to the greatest extent and truly write once. The run anywhere. The alPAH phase marks the maturation of the API, and it is believed that the official version will be available in the near future.

Through the official TodoApp example, we can experience the charm of Compose Multiplatform in advance github.com/JetBrains/c…

Todoapp engineering

  • todoapp
    • common: Platform independent code
      • Compose – UI: Reusable code for the UI layer
      • Main: Logical layer reusable code (home page)
      • Edit: Logical layer reusable code
      • Root: logical layer entry, navigation management (between main and EIDT)
      • Utils: utility class
      • -sheldon: I don’t have a database.
    • Android: platform-related code, Activity, etc
    • Desktop: platform-related code, application, etc
    • Web: platform-related, index.html, etc
    • For ios: Compose – UI does not support ios yet, but with KMM and SwiftUI, you can implement ios code

The project is built based on Model-view-Intent (AKA MVI). The code of Model layer and ViewModel layer can be reused almost 100%. Most of the View layer can also be reused on Desktop and Android.

In addition to Jetpack Compose, multiple KM-based tripartite frameworks are used in the project to ensure the consistent experience of the upper development paradigm on multiple platforms:

KM tripartite library instructions
Decompose Data Communications (BLoC)
MVIKotlin Cross-platform MVI
Rektive Asynchronous responsive library
SQLDelight The database

Todoapp code

Platform entry code

Compare the Android side with the Desktop side entry code

//todoapp/android/src/main/java/example/todo/android/MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        val root = todoRoot(defaultComponentContext())

        setContent {
            ComposeAppTheme {
                Surface(color = MaterialTheme.colors.background) {
                    TodoRootContent(root)
                }
            }
        }
    }

    private fun todoRoot(componentContext: ComponentContext): TodoRoot =
        TodoRootComponent(
            componentContext = componentContext,
            storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory())),
            database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this)))}Copy the code
//todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt

fun main(a) {
    overrideSchedulers(main = Dispatchers.Main::asScheduler)

    val lifecycle = LifecycleRegistry()
    val root = todoRoot(DefaultComponentContext(lifecycle = lifecycle))

    application {
        val windowState = rememberWindowState()
        LifecycleController(lifecycle, windowState)

        Window(
            onCloseRequest = ::exitApplication,
            state = windowState,
            title = "Todo"
        ) {
            Surface(modifier = Modifier.fillMaxSize()) {
                MaterialTheme {
                    DesktopTheme {
                        TodoRootContent(root)
                    }
                }
            }
        }
    }
}

private fun todoRoot(componentContext: ComponentContext): TodoRoot =
    TodoRootComponent(
        componentContext = componentContext,
        storeFactory = DefaultStoreFactory(),
        database = DefaultTodoSharedDatabase(TodoDatabaseDriver())
    )
Copy the code
  • TodoRootContent: Root Composable, View layer entry
  • TodoRootComponent: The root state manager, ViewModel layer entry
    • DefaultStoreFactory: Creates a Store and manages the status
    • DefaultTodoShareDatabase: M layer for data management

TodoRootContent and TodoRootComponent are entries to the View and ViewModel layers, respectively. TodoRootComponent manages the global state, or page navigation state.

It can be seen that Android and Desktop have been extensively reused in View, VM, M and other layers.

The VM layer code

Although there is no ViewModel in MVI, there is an equivalent concept, which is conventionally called the VM layer. The VM layer is actually the management place of the state. We take mian on the first page as an example

//todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainComponent.kt

class TodoMainComponent(
    componentContext: ComponentContext,
    storeFactory: StoreFactory,
    database: TodoSharedDatabase,
    private val output: Consumer<Output>
) : TodoMain, ComponentContext by componentContext {

    private val store =
        instanceKeeper.getStore {
            TodoMainStoreProvider(
                storeFactory = storeFactory,
                database = TodoMainStoreDatabase(database = database)
            ).provide()
        }

    override val models: Value<Model> = store.asValue().map(stateToModel)

    override fun onItemClicked(id: Long) {
        output(Output.Selected(id = id))
    }

    override fun onItemDoneChanged(id: Long, isDone: Boolean) {
        store.accept(Intent.SetItemDone(id = id, isDone = isDone))
    }

    override fun onItemDeleteClicked(id: Long) {
        store.accept(Intent.DeleteItem(id = id))
    }

    override fun onInputTextChanged(text: String) {
        store.accept(Intent.SetText(text = text))
    }

    override fun onAddItemClicked(a) {
        store.accept(Intent.AddItem)
    }
}
Copy the code

The code above should be familiar to those who know MVI. The Store manages the state and exposes it to the UI through the Models, and all the data flows in one direction. Value

is the type in the Decompose library, which can be understood as cross-platform LiveData

The View layer code

@Composable
fun TodoRootContent(component: TodoRoot) {
    Children(routerState = component.routerState, animation = crossfadeScale()) {
        when (val child = it.instance) {
            is Child.Main -> TodoMainContent(child.component)
            is Child.Edit -> TodoEditContent(child.component)
        }
    }
}
Copy the code

Internally, TodoRootContent is simply switching between different pages based on navigation.

Take a look at TodoMainContent

@Composable
fun TodoMainContent(component: TodoMain) {
    val model by component.models.subscribeAsState() 

    Column {
        TopAppBar(title = { Text(text = "Todo List") })

        Box(Modifier.weight(1F)) {
            TodoList(
                items = model.items,
                onItemClicked = component::onItemClicked,
                onDoneChanged = component::onItemDoneChanged,
                onDeleteItemClicked = component::onItemDeleteClicked
            )
        }

        TodoInput(
            text = model.text,
            onAddClicked = component::onAddItemClicked,
            onTextChanged = component::onInputTextChanged
        )
    }
}
Copy the code

SubscribeAsState () subscribes to the state of the Models in the Composable, driving the UI refresh. The Composalbe for Column and Box will be rendered on Descktop and Android respectively.

The web client code

Finally, take a look at the Web implementation.

Compose For Web Composalbe is mainly based on DOM design, which cannot be reused like Android and Desktop Composable. However, VM and M layers can still be reused in a large amount:

//todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt
fun main(a) {
    val rootElement = document.getElementById("root") as HTMLElement

    val lifecycle = LifecycleRegistry()

    val root =
        TodoRootComponent(
            componentContext = DefaultComponentContext(lifecycle = lifecycle),
            storeFactory = DefaultStoreFactory(),
            database = DefaultTodoSharedDatabase(todoDatabaseDriver())
        )

    lifecycle.resume()

    renderComposable(root = rootElement) {
        Style(Styles)

        TodoRootUi(root)
    }
}
Copy the code

Pass the TodoRootComponent to the UI to assist in navigation management

@Composable
fun TodoRootUi(component: TodoRoot) {
    Card(
        attrs = {
            style {
                position(Position.Absolute)
                height(700.px)
                property("max-width".640.px)
                top(0.px)
                bottom(0.px)
                left(0.px)
                right(0.px)
                property("margin", auto)
            }
        }
    ) {
        val routerState by component.routerState.subscribeAsState()

        Crossfade(
            target = routerState.activeChild.instance,
            attrs = {
                style {
                    width(100.percent)
                    height(100.percent)
                    position(Position.Relative)
                    left(0.px)
                    top(0.px)
                }
            }
        ) { child ->
            when (child) {
                is TodoRoot.Child.Main -> TodoMainUi(child.component)
                is TodoRoot.Child.Edit -> TodoEditUi(child.component)
            }
        }
    }
}
Copy the code

TodoMainUi is implemented as follows:

@Composable
fun TodoMainUi(component: TodoMain) {
    val model by component.models.subscribeAsState()

    Div(
        attrs = {
            style {
                width(100.percent)
                height(100.percent)
                display(DisplayStyle.Flex)
                flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
            }
        }
    ) {
        Div(
            attrs = {
                style {
                    width(100.percent)
                    property("flex"."0 1 auto")
                }
            }
        ) {
            NavBar(title = "Todo List")
        }

        Ul(
            attrs = {
                style {
                    width(100.percent)
                    margin(0.px)
                    property("flex"."1 1 auto")
                    property("overflow-y"."scroll")
                }
            }
        ) {
            model.items.forEach { item ->
                Item(
                    item = item,
                    onClicked = component::onItemClicked,
                    onDoneChanged = component::onItemDoneChanged,
                    onDeleteClicked = component::onItemDeleteClicked
                )
            }
        }

        Div(
            attrs = {
                style {
                    width(100.percent)
                    property("flex"."0 1 auto")
                }
            }
        ) {
            TodoInput(
                text = model.text,
                onTextChanged = component::onInputTextChanged,
                onAddClicked = component::onAddItemClicked
            )
        }
    }
}
Copy the code

The last

In my article Jetpack Compose Runtime: Fundamentals of declarative UI, I described the cross-platform technology foundation for Compose, which now works with a variety of KM tri-libraries to complete the development ecosystem. Compose Multiplatform is built on the basis of Kotlin, with upstream and downstream isomorphism. Compared with Flutter and RN, Compose Multiplatform has advantages in the future.