Quick Start
This guide shows how to quickly integrate Tuucho into a Kotlin Multiplatform project (iOS and Android). Tuucho is a server‑driven rendering engine published on Maven Central.
1 Add dependencies to the shared module
In your shared module (commonMain) you need the Tuucho core library plus its supporting libraries:
commonMain.dependencies {
implementation("com.tezov:tuucho.core:0.0.1-alpha25_2.3.0") // for kotlin 2.3.0
implementation("com.tezov:tuucho.ui-component.stable:0.0.1-alpha25_2.3.0")
implementation("io.insert-koin:koin-core:4.2.0-beta2")
implementation("io.ktor:ktor-client-core:3.3.2")
implementation("org.jetbrains.compose.runtime:runtime:1.10.0")
implementation("org.jetbrains.compose.foundation:foundation:1.10.0")
implementation("org.jetbrains.compose.ui:ui:1.10.0")
}
2 Create an AppScreen in commonMain in the shared module
Tuucho exposes a composable engine which loads its configuration from the server and renders the pages. The simplest way to start it is via an AppScreen composable:
@Composable
fun AppScreen(
applicationModules: List<KoinMass>,
koinExtension: (KoinApplication.() -> Unit)? = null,
) {
TuuchoEngineStart(
koinMassModules = SystemSharedModules.invoke() + applicationModules,
koinExtension = koinExtension,
onStartUrl = "lobby/page-login"
)
}
You can make use of middleware navigation to preload component: - Config details to understand the config file format. - Middleware details to understand navigation middleware.
You can also check the tuucho‑sample in the repository.
3 Supply configuration via Koin in the shared module
Tuucho needs configuration properties, like you server base url, endpoint, database file name. Define a Koin module that provides a SystemCoreDataModules.Config instance. In the shared module you can implement this interface with your own values:
object ConfigModule {
fun invoke() = module(ModuleContextCore.Main) {
factory<NetworkRepositoryModule.Config> {
object : NetworkRepositoryModule.Config {
override val timeoutMillis = 5000
override val version = "v1"
override val baseUrl = "http://localhost:3000/"
override val healthEndpoint = "health"
override val resourceEndpoint = "resource"
override val sendEndpoint = "send"
}
}
}
}
The core module defines the Config interface for NetworkRepositoryModule.
The sample application uses
BuildKonfigto generate these values per platform.
4 Android integration
On Android you have to provide the application Context so that Tuucho can access platform services. Define an ApplicationModuleDeclaration that injects the context using Koin:
First you need the to add the dependencies
dependencies {
implementation(project(":app:shared"))
implementation("com.tezov:tuucho.core:0.0.1-alpha25_2.3.0") // for kotlin 2.3.0
implementation("io.insert-koin:koin-core:4.2.0-beta2")
implementation("androidx.activity:activity-compose:1.12.2")
}
Then you need to supply the context with the koin symbol ApplicationModules.Name.APPLICATION_CONTEXT
object ApplicationModule {
fun invoke(
applicationContext: Context
) = module(ModuleContextCore.Main) {
single<Context>(SystemCoreDataModulesAndroid.Name.APPLICATION_CONTEXT) {
applicationContext
}
}
}
Then, in your Activity, set the Compose content to AppScreen and pass in the application context:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
AppScreen(
applicationModules = listOf(ApplicationModule.invoke(applicationContext)),
koinExtension = {
koin.get<NavigationFinishPublisher>().onFinish {
koin.close()
this@MainActivity.finish()
}
}
)
}
}
}
}
At runtime Tuucho will start Koin, load the configuration and display the server‑rendered UI.
5 iOS integration
On iOS you need to expose the AppScreen composable through a ComposeUIViewController, then wrap it in a SwiftUI view. In your shared module create a simple function returning a ComposeUIViewController:
fun uiView(
koinExtension: (KoinApplication.() -> Unit)? = null,
) = ComposeUIViewController {
AppScreen(
applicationModules = emptyList(),
koinExtension = koinExtension
)
}
val KoinApplication.tuuchoKoinIos get(): KoinIos = koin.get<KoinIos>()
Then in your iOS app, bridge the Compose controller into SwiftUI. The sample uses AppCoordinator, ComposeView and ComposeHostController to do this:
//AppCoordinator.swift
import SwiftUI
import Combine
class AppCoordinator: ObservableObject {
@Published var shouldRecreateController = false
private var isKoinClosed = false
func handleAppAppear() {
if isKoinClosed {
shouldRecreateController = true
isKoinClosed = false
}
}
func handleKoinClosed() {
isKoinClosed = true
shouldRecreateController = true
}
}
//ComposeView.swift
import SwiftUI
import UIKit
struct ComposeView: UIViewControllerRepresentable {
@ObservedObject var coordinator: AppCoordinator
func makeUIViewController(context: Context) -> UIViewController {
let controller = ComposeHostController()
controller.coordinator = coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if coordinator.shouldRecreateController {
guard let controller = uiViewController as? ComposeHostController else { return }
controller.recreateComposeView()
DispatchQueue.main.async {
coordinator.shouldRecreateController = false
}
}
}
}
//ComposeHostController.swift
import UIKit
import SwiftUI
import AppSharedFramework
class ComposeHostController: UIViewController {
weak var coordinator: AppCoordinator?
private var composeViewController: UIViewController?
private var isKoinInitialized = false
override func viewDidLoad() {
super.viewDidLoad()
setupComposeView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
composeViewController?.view.frame = view.bounds
}
func recreateComposeView() {
cleanup()
setupComposeView()
}
private func cleanup() {
composeViewController?.willMove(toParent: nil)
composeViewController?.view.removeFromSuperview()
composeViewController?.removeFromParent()
composeViewController = nil
}
private func setupComposeView() {
guard !isKoinInitialized else { return }
let vc = KMPKitKt.uiView(koinExtension: { [weak self] koinApplication in
guard let self = self else { return }
let publisher = koinApplication.tuuchoKoinIos.get(clazz: NavigationFinishPublisher.self) as! NavigationFinishPublisher
publisher.onFinish(block: {
self.coordinator?.handleKoinClosed()
koinApplication.tuuchoKoinIos.close()
self.isKoinInitialized = false
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
})
})
self.composeViewController = vc
self.isKoinInitialized = true
addChild(vc)
view.addSubview(vc.view)
vc.view.frame = view.bounds
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
vc.didMove(toParent: self)
}
}
// iosApp.swift
import SwiftUI
@main
struct iosApp: App {
@StateObject private var coordinator = AppCoordinator()
var body: some Scene {
WindowGroup {
ComposeView(coordinator: coordinator)
.onAppear {
coordinator.handleAppAppear()
}
}
}
}
Ios require more code mainly to be able to manage the swipe back. you can check all that code inside the sample app
6 Backend
Tuucho renders its UI from a backend server. For a quick test you can run the tuucho‑backend dev repository locally with a version matching your Tuucho client.
The engine is under active development. At the time of writing the UI components are limited to basic primitives (fields, labels, linear layouts, buttons and spacers). The ability to register custom views and provide richer UI will evolve in future releases.
7 Sample Application
- The
samplefolder is contains all explain above to try. tuucho‑sample
Note
The sample application includes a mock server. If you launch it in mock build type, you don't need a backend server.