How Greenhouse Admin uses Kotlin Multiplatform to share code across Android, iOS, Desktop, and Web
Greenhouse Admin is built with Kotlin Multiplatform (KMP) to maximize code sharing across all platforms while maintaining the flexibility to use platform-specific APIs when needed.
The commonMain source set contains all code that’s platform-agnostic:
UI components built with Compose Multiplatform
Business logic in ViewModels
Repository interfaces and implementations
Domain models and DTOs
API services using Ktor client
DI modules with Koin
commonMain example
// Works identically on all platformsclass LoginViewModel( private val authRepository: AuthRepository) : ViewModel() { // Platform-agnostic state management private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()}
actual class TokenStorage actual constructor() { // Android uses in-memory storage for now // Production should use EncryptedSharedPreferences private var accessToken: String? = null actual fun getAccessToken(): String? = accessToken actual fun saveAccessToken(token: String) { accessToken = token }}
import platform.Foundation.NSUserDefaultsactual class TokenStorage actual constructor() { private val defaults = NSUserDefaults.standardUserDefaults actual fun getAccessToken(): String? = defaults.stringForKey("greenhouse_access_token") actual fun saveAccessToken(token: String) { defaults.setObject(token, "greenhouse_access_token") }}
jvmMain for desktop applications:
TokenStorage.jvm.kt
import java.util.prefs.Preferencesactual class TokenStorage actual constructor() { private val prefs = Preferences.userNodeForPackage(TokenStorage::class.java) actual fun getAccessToken(): String? = prefs.get("access_token", null) actual fun saveAccessToken(token: String) { prefs.put("access_token", token) }}
wasmJsMain is the primary web target:
TokenStorage.wasmJs.kt
import kotlinx.browser.localStorageactual class TokenStorage actual constructor() { actual fun getAccessToken(): String? = localStorage.getItem("greenhouse_access_token") actual fun saveAccessToken(token: String) { localStorage.setItem("greenhouse_access_token", token) } actual fun getRoles(): List<String> { val rolesString = localStorage.getItem("greenhouse_roles") ?: return emptyList() return rolesString.split(",").filter { it.isNotBlank() } }}
Define the contract that all platforms must implement:
commonMain/data/local/TokenStorage.kt
/** * Platform-specific token storage for JWT tokens. */expect class TokenStorage() { fun getAccessToken(): String? fun saveAccessToken(token: String) fun getTokenType(): String? fun saveTokenType(type: String) fun getUsername(): String? fun saveUsername(username: String) fun getRoles(): List<String> fun saveRoles(roles: List<String>) fun clearTokens() fun isAuthenticated(): Boolean}
2
Implement actual in each platform
Each platform provides its own implementation:
Android implementation
androidMain/data/local/TokenStorage.android.kt
actual class TokenStorage actual constructor() { private var accessToken: String? = null private var tokenType: String? = null private var username: String? = null private var roles: List<String> = emptyList() actual fun getAccessToken(): String? = accessToken actual fun saveAccessToken(token: String) { accessToken = token } // ... other methods}
iOS implementation
iosMain/data/local/TokenStorage.ios.kt
import platform.Foundation.NSUserDefaultsactual class TokenStorage actual constructor() { private val defaults = NSUserDefaults.standardUserDefaults actual fun getAccessToken(): String? = defaults.stringForKey(KEY_ACCESS_TOKEN) actual fun saveAccessToken(token: String) { defaults.setObject(token, KEY_ACCESS_TOKEN) } companion object { private const val KEY_ACCESS_TOKEN = "greenhouse_access_token" }}
WasmJS implementation
wasmJsMain/data/local/TokenStorage.wasmJs.kt
import kotlinx.browser.localStorageactual class TokenStorage actual constructor() { actual fun getAccessToken(): String? = localStorage.getItem(KEY_ACCESS_TOKEN) actual fun saveAccessToken(token: String) { localStorage.setItem(KEY_ACCESS_TOKEN, token) } actual fun clearTokens() { localStorage.removeItem(KEY_ACCESS_TOKEN) localStorage.removeItem(KEY_TOKEN_TYPE) localStorage.removeItem(KEY_USERNAME) localStorage.removeItem(KEY_ROLES) } companion object { private const val KEY_ACCESS_TOKEN = "greenhouse_access_token" private const val KEY_TOKEN_TYPE = "greenhouse_token_type" private const val KEY_USERNAME = "greenhouse_username" private const val KEY_ROLES = "greenhouse_roles" }}
3
Use in common code
The common code can now use TokenStorage without knowing about platform details:
commonMain
class AuthRepositoryImpl( private val authApiService: AuthApiService, private val tokenStorage: TokenStorage // Works on all platforms) : AuthRepository { override suspend fun login(username: String, password: String): Result<UserSession> { return runCatching { val response = authApiService.login(username, password) tokenStorage.saveAccessToken(response.token) // Platform-specific response.toUserSession() } }}