跪拜 Guibai
← Back to the summary

Kuikly Is a KMP UI Framework That Renders Natively on Six Targets

What is Kuikly? And what is its relationship with KMP?

Kuikly is Tencent's open-source Kotlin Multiplatform cross-platform framework.

It doesn't just handle pure business logic reuse. Instead, it places UI, layout, events, Bridge, and some business logic into a KMP project, which is then compiled separately for Android, iOS, HarmonyOS, Web, Mini Programs, and macOS.

Image

What problem does it solve?

When Android developers look at cross-platform frameworks, they usually ask two questions first: Can one codebase run on multiple platforms, and after it runs, can it still retain a near-native experience? Kuikly's design is quite clear: it uses Kotlin Multiplatform to write cross-platform business logic, and then uses each platform's own Render layer to do the actual UI rendering.

Its project has two core concepts: KuiklyCore and KuiklyRender. KuiklyCore is responsible for cross-platform declarative UI, reactive updates, layout algorithms, and Bridge communication; KuiklyRender is responsible for rendering the controls declared by Core onto specific platforms. There is an Android renderer for Android, an iOS renderer for iOS, and corresponding implementations for HarmonyOS, Web, and Mini Programs.

This is different from many solutions that "write one set of UI and then draw it with a unified engine." Kuikly is closer to a cross-platform UI protocol: business code declares page structure and state changes on the KMP side, while the platform side provides hosting containers, lifecycle, image, logging, routing, threading, and other adaptation capabilities.

Image

From the open-source repository, Kuikly currently supports Android, iOS, HarmonyOS, Web, Mini Programs, and macOS. Among these, Web and Mini Programs are still marked as beta, and macOS is Alpha. System requirements are Android 5.0+, iOS 12.0+, and HarmonyOS Next 5.0.0(12)+. These version details mean it's not just for new projects; older Android devices are also within its coverage.

There is also a specific piece of information in the README: in AOT mode, the Android SDK increment is about 300 KB, and for iOS, it's about 1.2 MB. This number cannot be directly equated to the final app package size, as it also depends on specific pages, resources, dependencies, and platform integration methods, but it at least shows that it doesn't stuff a heavy, general-purpose rendering engine into the host.

Project Structure

Kuikly's source code structure reveals its layering.

core is the cross-platform module, containing commonMain, androidMain, jvmMain, appleMain, ohosArm64Main, and jsMain. These directories correspond to different KMP targets, and the final products are different: .aar for Android, .framework for iOS and macOS, .so for HarmonyOS, and JS-related products for Web and Mini Programs.

The platform rendering layer is split into core-render-android, core-render-ios, core-render-ohos, and core-render-web. The @Page annotation used by business pages is in core-annotations, and annotation processing is in core-ksp. This also explains why you need to add @Page("xxx") to a page when writing it, because the runtime needs to create the corresponding page by its name.

The minimal page in the official example looks roughly like this:

@Page("HelloWorld")
internal class HelloWorldPage : Pager() {
    override fun body(): ViewBuilder {
        return {
            attr {
                allCenter()
            }

            Text {
                attr {
                    text("Hello Kuikly")
                    fontSize(14f)
                }
            }
        }
    }
}

There are three points in this code. Pager is the base class for Kuikly pages, body() returns the page content, and @Page("HelloWorld") registers this page as a page name that can be opened at runtime. Entering or passing HelloWorld in the Android shell project opens this page.

If you only look at this code, it somewhat resembles Compose's declarative style, but it does not directly use the androidx.compose package. The Kuikly repository has a compose directory, which is adapted based on Jetpack Compose 1.7.3 and changes the package name from androidx.compose to com.tencent.kuikly.compose to support Kuikly's own rendering needs. This point is crucial; it cannot be simply understood as "the official Compose Multiplatform plugged in as-is."

Creating a Project

The entry point for a new project is the Android Studio plugin. The path in the documentation is:

File -> New -> New Project -> Kuikly Project Template

After creation, it generates a KMP business project and shell projects for each platform. Common directories include shared, androidApp, iosApp, ohosApp, h5App, miniApp, etc. Business pages are usually first written under shared/src/commonMain/kotlin/... and then run through each platform's shell project.

For a first experience, you can run Android first. After creating the project and completing the Gradle sync, directly select androidApp to run. For iOS, you need to first enter iosApp and execute:

pod install --repo-update

The debugging path for H5 is also quite specific. First, start the dev server, then build the JS Bundle, and finally access the local page in a browser:

npm run serve
./gradlew :shared:packLocalJsBundleDebug
./gradlew :h5App:jsBrowserRun -t
./gradlew :h5App:copyAssetsToWebpackDevServer

If the Kotlin version is 2.0 or above, the H5 run command changes to:

./gradlew :h5App:jsBrowserDevelopmentRun -t

For Mini Programs, the process is also to first compile the business code into JS, then open the generated directory with WeChat Developer Tools. Common Debug build commands are:

./gradlew :shared:packLocalJsBundleDebug
./gradlew :miniApp:jsMiniAppDevelopmentWebpack

Image

These commands show that Kuikly's "multi-platform" isn't just about clicking a button in the IDE. Android, iOS, HarmonyOS, H5, and Mini Programs each have their own shell projects and toolchains. Pages on the KMP side can be shared, but the project configuration, signing, and debugging methods on the platform side still need to be handled per platform.

Integrating into Android

If it's not a new project, but integrating Kuikly into an existing Android project, it needs to be split into two parts: integrating KuiklyCore on the KMP side, and integrating KuiklyRender on the Android host side.

You need to add the renderer dependency in the Android host module. The documentation writes it as:

repositories {
    maven("https://mirrors.tencent.com/repository/maven-tencent/")
}

dependencies {
    implementation("com.tencent.kuikly-open:core-render-android:KUIKLY_RENDER_VERSION")
    implementation("com.tencent.kuikly-open:core:KUIKLY_CORE_VERSION")
    implementation("androidx.core:core-ktx:1.7.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.8.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.3")
}

There's an easy pitfall here: the Kuikly versions for core-render-android and core must be consistent with the KMP cross-platform project. After version 2.5.0, you also need to add the Tencent Maven source. When versions are inconsistent, the problem might not be a direct compile-time error, but rather inconsistent page creation, Bridge, or component behavior at runtime.

The host App also needs to provide a hosting container. The documentation example uses KuiklyRenderActivity, which creates a ContextCodeHandler, gets a KuiklyRenderViewBaseDelegator, and then calls openPage() to mount the Kuikly page into a ViewGroup. Lifecycle events must also be forwarded to the delegator: onResume(), onPause(), onDetach().

Simplified, it can be understood as the following structure:

class KuiklyRenderActivity : AppCompatActivity() {
    private lateinit var delegator: KuiklyRenderViewBaseDelegator
    private lateinit var contextCodeHandler: ContextCodeHandler
    private lateinit var container: ViewGroup

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contextCodeHandler = ContextCodeHandler(pageName)
        delegator = contextCodeHandler.initContextHandler()
        container = findViewById(R.id.hr_container)

        contextCodeHandler.openPage(
            this,
            container,
            pageName,
            createPageData()
        )
    }

    override fun onResume() {
        super.onResume()
        delegator.onResume()
    }

    override fun onPause() {
        super.onPause()
        delegator.onPause()
    }

    override fun onDestroy() {
        super.onDestroy()
        delegator.onDetach()
    }
}

Then you also need to implement platform adapters. Required items include an image adapter, a log adapter, an exception adapter, a page routing adapter, and a thread adapter. Image loading uses the host's existing image library, logging and exceptions integrate into existing monitoring, routing integrates into the in-app navigation system, and thread adaptation connects to Android's main thread and background threads.

This is also the difference between Kuikly and a pure UI library. It's not just about adding a View and being done. When truly integrating into a business project, page lifecycle, routing, images, exceptions, and threads all need to align with the host.

Compared to Other Cross-Platform Solutions

Common cross-platform solutions for Android development fall into a few categories: Flutter, React Native, Compose Multiplatform, KMP logic reuse, and Kuikly.

Flutter uses Dart and its own rendering system, offering strong UI consistency and a mature ecosystem, but native teams must accept an independent tech stack and the Flutter Engine. React Native uses JS/TS to write business logic, connecting to platform capabilities through a Bridge or new architecture; it's more natural for frontend teams to pick up, but Android native teams need to handle the JS runtime, package management, and platform interaction boundaries.

Compose Multiplatform uses Kotlin and Compose to write UI, with a low learning curve for Android developers, and Desktop and iOS support are continuously advancing. It's closer to the JetBrains/Compose ecosystem itself, suitable for teams wanting to do multi-platform UI along the Compose technology line.

Kuikly's special points are in a few areas. First, it's based on KMP, but its goal is not just logic reuse, but UI + logic together across platforms. Second, it has a self-developed DSL and also a Compose DSL adapted from Compose. Third, it separates Core and Render, meaning the platform side still needs to integrate the corresponding renderer and adapters. Fourth, it emphasizes dynamic products, which is more attractive for page grayscale releases and module distribution in large apps.

Image

So judging Kuikly can't just be based on "is it Kotlin writing UI?" If a team already has a Kotlin/Android background and wants some pages to run on iOS, HarmonyOS, H5, and Mini Programs, without completely migrating their business to Dart or JS, Kuikly's route is closer to this need. If a team is already on a Flutter or RN tech stack, or only needs unified UI for Android/iOS, it might not be the lowest-cost choice.

Finally

Kuikly can be understood as a cross-platform UI framework based on Kotlin Multiplatform: write pages and logic on the KMP side, use Render on the platform side to host and render, and then connect to host capabilities through adapters.

Its differences from Flutter, RN, and Compose Multiplatform mainly lie in the tech stack, rendering method, platform coverage, and dynamic capabilities. For Android developers wanting to try it, start with one @Page page, one Android container, and a few essential adapters; don't jump straight into refactoring the main framework.

#Android #KotlinMultiplatform #Kuikly #CrossPlatform

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

迎风破浪

I tried the official demo and didn't want to continue. 1. The demo project had a bunch of errors and warnings without changing anything. It runs, but I can't accept that. 2. In a newly created project, the Kuikly version was from last year. It took ages to find where to change the version number, and the official site only lists the latest Kuikly version — I couldn't find the latest KOTLIN_OHOS_VERSION anywhere. 3. Shouldn't the version be declared in Gradle? It's declared through an object instead, so changing it doesn't even trigger a Gradle sync.