Android Architecture
Learn how Cuppa integrates with Android to provide a seamless cross-platform development experience while maintaining native performance and Material Design principles.
Architecture Overview
Cuppa for Android follows a layered architecture that separates concerns and enables code sharing:
┌─────────────────────────────────────┐
│ Jetpack Compose UI │ Native Android UI
├─────────────────────────────────────┤
│ ViewModels & State │ Kotlin business logic
├─────────────────────────────────────┤
│ Cuppa Bridge │ Kotlin ↔ TypeScript
├─────────────────────────────────────┤
│ Shared Business Logic │ TypeScript/JavaScript
├─────────────────────────────────────┤
│ Cuppa Core │ Platform abstractions
├─────────────────────────────────────┤
│ Native Android APIs & Libs │ AndroidX, Kotlin, etc.
└─────────────────────────────────────┘
Key Components
1. Jetpack Compose UI
Your Android UI is built entirely with Jetpack Compose, ensuring native look, feel, and performance:
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import io.mycuppa.ui.components.*
@Composable
fun ProductScreen(
productId: String,
viewModel: ProductViewModel = viewModel()
) {
val product by viewModel.product.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
if (isLoading) {
CuppaLoadingSpinner()
} else {
product?.let { product ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Native Compose components
AsyncImage(
model = product.imageUrl,
contentDescription = product.title
)
Text(
text = product.title,
style = MaterialTheme.typography.headlineMedium
)
CuppaButton(
text = "Add to Cart",
onClick = { viewModel.addToCart() }
)
}
}
}
}
2. ViewModels and State
ViewModels connect your Compose UI to shared business logic:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.mycuppa.core.CuppaBridge
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class ProductViewModel(
private val productId: String
) : ViewModel() {
private val _product = MutableStateFlow<Product?>(null)
val product: StateFlow<Product?> = _product.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
init {
loadProduct()
}
private fun loadProduct() {
viewModelScope.launch {
_isLoading.value = true
try {
// Call shared TypeScript business logic
val data = CuppaBridge.call<Product>(
function = "getProduct",
args = mapOf("id" to productId)
)
_product.value = data
} catch (e: Exception) {
// Handle error
} finally {
_isLoading.value = false
}
}
}
fun addToCart() {
viewModelScope.launch {
CuppaBridge.call<Unit>(
function = "addToCart",
args = mapOf("productId" to productId)
)
}
}
}
3. Cuppa Bridge
The bridge enables seamless communication between Kotlin and TypeScript:
import io.mycuppa.core.CuppaBridge
// Call TypeScript from Kotlin
val result = CuppaBridge.call<CartTotal>(
function = "calculateTotal",
args = mapOf(
"items" to cartItems,
"taxRate" to 0.08
)
)
// Register Kotlin function callable from TypeScript
CuppaBridge.register("showNativeDialog") { args ->
val title = args["title"] as? String ?: ""
val message = args["message"] as? String ?: ""
// Show native Android dialog
withContext(Dispatchers.Main) {
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.show()
}
}
4. Shared Business Logic
Your core application logic is written once in TypeScript and shared across platforms:
// shared/src/services/cart.ts
export class CartService {
calculateTotal(items: CartItem[], taxRate: number): number {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const tax = subtotal * taxRate
return subtotal + tax
}
async checkout(items: CartItem[]): Promise<Order> {
// Shared checkout logic
const response = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ items }),
})
return response.json()
}
}
Data Flow
Unidirectional Data Flow
Cuppa follows Android's recommended unidirectional data flow pattern:
User Action (Compose)
↓
ViewModel Method
↓
Shared Business Logic (TypeScript)
↓
API Call / Data Processing
↓
Update StateFlow
↓
Compose Recomposition
Example Flow
// 1. User taps button in Compose
CuppaButton(
text = "Refresh",
onClick = { viewModel.refresh() }
)
// 2. ViewModel updates state and calls shared logic
fun refresh() {
viewModelScope.launch {
_isLoading.value = true
// 3. Shared TypeScript function fetches data
val data = CuppaBridge.call<List<Item>>(
function = "fetchLatestItems",
args = emptyMap()
)
// 4. Update StateFlow
_items.value = data
_isLoading.value = false
}
}
// 5. Compose automatically recomposes
val items by viewModel.items.collectAsState()
Plugin Architecture
Cuppa plugins extend functionality through a standardized interface:
// Plugin definition
interface CuppaPlugin {
val name: String
fun initialize(context: Context, config: Map<String, Any>)
fun cleanup()
}
// Using plugins
import io.mycuppa.plugins.auth.CuppaAuthPlugin
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize Cuppa plugins
CuppaPluginManager.register(CuppaAuthPlugin())
CuppaPluginManager.register(CuppaAnalyticsPlugin())
CuppaPluginManager.initialize(this)
}
}
State Management
Local State (Compose)
Use Compose state for UI-specific state:
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Screen State (ViewModel)
Use StateFlow for state shared across composables:
class AppViewModel : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
private val _isAuthenticated = MutableStateFlow(false)
val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()
companion object {
val instance by lazy { AppViewModel() }
}
}
Global State (TypeScript)
Manage global state in shared TypeScript code:
// shared/src/store/auth.ts
import { create } from 'zustand'
interface AuthState {
user: User | null
token: string | null
setUser: (user: User) => void
setToken: (token: string) => void
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
token: null,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
}))
Performance Considerations
Lazy Loading
Load screens and data only when needed:
@Composable
fun AppNavigation() {
NavHost(navController, startDestination = "home") {
composable("home") {
// Lazily loaded
HomeScreen()
}
composable("products") {
ProductListScreen()
}
}
}
Caching
Cache data to minimize bridge calls:
object DataCache {
private val cache = mutableMapOf<String, Any>()
fun <T> get(key: String): T? {
return cache[key] as? T
}
fun set(key: String, value: Any) {
cache[key] = value
}
fun clear() {
cache.clear()
}
}
Background Processing
Offload heavy work from the main thread:
viewModelScope.launch(Dispatchers.IO) {
// Runs on background thread
val processedData = performHeavyComputation()
withContext(Dispatchers.Main) {
// Update UI on main thread
_data.value = processedData
}
}
Dependency Injection
Use Hilt or Koin for dependency injection:
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository,
private val analytics: AnalyticsService
) : ViewModel() {
// ViewModel implementation
}
Next Steps
- Explore Jetpack Compose Components provided by Cuppa
- Review the Component Reference for detailed APIs
- Learn about Android Best Practices