MyCuppa

Android Quick Start

This guide will help you build your first Android application with Cuppa and Jetpack Compose.

Creating Your First Screen

Create a new composable in android/app/src/main/kotlin/com/myapp/screens/HomeScreen.kt:

package com.myapp.screens

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.mycuppa.ui.components.CuppaButton

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = viewModel()
) {
    val isLoading by viewModel.isLoading.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Welcome to Cuppa!",
            style = MaterialTheme.typography.headlineLarge
        )

        Spacer(modifier = Modifier.height(20.dp))

        CuppaButton(
            text = "Get Started",
            onClick = { viewModel.handleGetStarted() },
            isLoading = isLoading
        )
    }
}

Creating a ViewModel

Create the corresponding ViewModel using shared Cuppa logic:

package com.myapp.screens

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.mycuppa.core.CuppaBridge
import io.mycuppa.data.CuppaData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class HomeViewModel : ViewModel() {
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    private val _items = MutableStateFlow<List<Item>>(emptyList())
    val items: StateFlow<List<Item>> = _items

    fun handleGetStarted() {
        viewModelScope.launch {
            _isLoading.value = true

            try {
                // Use shared Cuppa data fetching
                val result = CuppaData.query<List<Item>>("api/items")
                _items.value = result
            } catch (e: Exception) {
                // Handle error
                println("Error: ${e.message}")
            } finally {
                _isLoading.value = false
            }
        }
    }
}

data class Item(
    val id: String,
    val title: String,
    val description: String
)

Setting Up Navigation

Configure navigation in MainActivity.kt:

package com.myapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.myapp.screens.HomeScreen
import com.myapp.screens.ProfileScreen
import com.myapp.screens.SettingsScreen
import io.mycuppa.ui.theme.CuppaTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CuppaTheme {
                AppNavigation()
            }
        }
    }
}

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen()
        }
        composable("profile") {
            ProfileScreen()
        }
        composable("settings") {
            SettingsScreen()
        }
    }
}

Using Cuppa Components

Cuppa provides pre-built Compose components:

Buttons

import io.mycuppa.ui.components.*

// Primary button
CuppaButton(
    text = "Submit",
    onClick = { handleSubmit() },
    variant = ButtonVariant.Primary
)

// Secondary button
CuppaButton(
    text = "Cancel",
    onClick = { handleCancel() },
    variant = ButtonVariant.Secondary
)

// Outlined button
CuppaButton(
    text = "Learn More",
    onClick = { openDocs() },
    variant = ButtonVariant.Outlined
)

Form Inputs

@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        CuppaTextField(
            value = email,
            onValueChange = { email = it },
            label = "Email",
            placeholder = "Enter your email",
            leadingIcon = Icons.Default.Email
        )

        CuppaPasswordField(
            value = password,
            onValueChange = { password = it },
            label = "Password",
            placeholder = "Enter your password"
        )

        CuppaButton(
            text = "Sign In",
            onClick = { handleSignIn(email, password) },
            modifier = Modifier.fillMaxWidth()
        )
    }
}

Lists

@Composable
fun ItemListScreen(items: List<Item>) {
    CuppaLazyColumn(
        items = items,
        emptyState = {
            CuppaEmptyState(
                icon = Icons.Default.Inbox,
                title = "No Items",
                message = "Add your first item to get started"
            )
        }
    ) { item ->
        CuppaListItem(
            title = item.title,
            subtitle = item.description,
            onClick = { navigateToDetail(item) },
            trailing = {
                Icon(
                    Icons.Default.ChevronRight,
                    contentDescription = null
                )
            }
        )
    }
}

Cards

CuppaCard(
    modifier = Modifier.padding(16.dp)
) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "Card Title",
            style = MaterialTheme.typography.titleMedium
        )
        Text(
            text = "Card content goes here",
            style = MaterialTheme.typography.bodyMedium
        )
        HorizontalDivider()
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            CuppaButton(
                text = "Action",
                onClick = { },
                modifier = Modifier.weight(1f)
            )
            CuppaButton(
                text = "Cancel",
                onClick = { },
                variant = ButtonVariant.Secondary,
                modifier = Modifier.weight(1f)
            )
        }
    }
}

Integrating with Shared Code

Use your shared TypeScript/JavaScript business logic from Android:

import io.mycuppa.core.CuppaBridge

class DataManager {
    suspend fun fetchUserData(): User {
        // Call shared TypeScript function
        val result = CuppaBridge.call<User>(
            function = "getUserData",
            args = emptyMap()
        )
        return result
    }
}

Adding Plugins

Integrate Cuppa plugins for common functionality:

Authentication Plugin

import io.mycuppa.plugins.auth.CuppaAuth

// Sign in with email
val user = CuppaAuth.signIn(
    email = email,
    password = password
)

// Sign out
CuppaAuth.signOut()

// Get current user
val currentUser = CuppaAuth.currentUser

Data Plugin

import io.mycuppa.plugins.data.CuppaData

// Query data
val items = CuppaData.query<List<Item>>(
    endpoint = "items",
    filters = mapOf("category" to "featured")
)

// Mutate data
CuppaData.mutate(
    endpoint = "items/create",
    data = newItem
)

Running Your App

Build and run your app:

# Run on Android emulator
pnpm android

# Run on specific device
pnpm android --device="Pixel_7_API_34"

Next Steps