MyCuppa

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