KMPTestApp is the test application that demonstrates the use of all modules of the SweetMeSoft KMP library. It serves as an implementation example and best practices guide.
KMPTestApp is a complete application that demonstrates:
KMPControls componentsKMPMapsLibrary controlskmptestapp/
├── src/
│ ├── androidMain/
│ │ └── kotlin/
│ │ └── com/sweetmesoft/kmptestapp/
│ │ └── MainActivity.kt
│ ├── commonMain/
│ │ └── kotlin/
│ │ └── com/sweetmesoft/kmptestapp/
│ │ ├── App.kt # Main entry point
│ │ ├── PhotoProfileRequest.kt # Data model
│ │ ├── screens/
│ │ │ ├── about/
│ │ │ │ └── AboutScreen.kt # About screen
│ │ │ ├── main/
│ │ │ │ ├── MainScreen.kt # Main screen
│ │ │ │ └── MainViewModel.kt # Main ViewModel
│ │ │ └── splash/
│ │ │ └── SplashScreen.kt # Splash screen
│ │ └── theme/
│ │ ├── Color.kt # Theme colors
│ │ └── Theme.kt # Theme configuration
│ └── iosMain/
│ └── kotlin/
│ └── com/sweetmesoft/kmptestapp/
│ └── MainViewController.kt
└── build.gradle.kts
commonMain.dependencies {
implementation(projects.library)
implementation(projects.kmpcontrols)
implementation(projects.kmpmaps)
// Navigation
implementation(libs.voyager.navigator)
implementation(libs.voyager.bottom.sheet.navigator)
implementation(libs.voyager.tab.navigator)
implementation(libs.voyager.transitions)
// UI and Compose
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
// Lifecycle
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
// Utilities
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
android {
namespace = "com.sweetmesoft.kmptestapp"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.sweetmesoft.kmptestapp"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
}
@Composable
fun App() {
AppTheme {
Navigator(screen = SplashScreen()) { navigator ->
// Configure global navigator
BaseViewModel.navigator = navigator
// Apply transitions
SlideTransition(navigator)
}
}
}
Features:
class SplashScreen : BaseScreen() {
@Composable
override fun ScreenContent() {
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(2000) // Simulate loading
isLoading = false
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary
)
)
),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Application logo
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(120.dp),
tint = Color.White
)
Text(
text = "KMP Test App",
style = MaterialTheme.typography.headlineLarge,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
text = "Demonstrating SweetMeSoft KMP Library",
style = MaterialTheme.typography.bodyLarge,
color = Color.White.copy(alpha = 0.8f),
textAlign = TextAlign.Center
)
if (isLoading) {
Spacer(modifier = Modifier.height(32.dp))
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.size(32.dp)
)
} else {
LaunchedEffect(Unit) {
navigator?.replace(MainScreen())
}
}
}
}
}
}
Features:
The main screen uses LocalGridList to provide navigation to different feature modules.
class MainScreen : BaseBottomBarScreen() {
@Composable
override fun ScreenContent() {
val navigator = LocalNavigator.currentOrThrow
val menuItems = listOf(
MainMenuItem("Controls", "Input controls", TablerIcons.Forms) {
navigator.push(ControlsScreen())
},
MainMenuItem("Maps", "Map integration", TablerIcons.Map) {
navigator.push(MapScreen())
},
MainMenuItem("Dialogs", "Alerts & Dialogs", TablerIcons.Message) {
navigator.push(DialogsScreen())
},
// ... other items
)
LocalGridList(
list = menuItems,
columns = 2,
contentPadding = PaddingValues(16.dp)
) { _, item ->
MenuCard(item)
}
// Alert and popup handling
PopupHandler.currentPopup?.let { popup ->
// ...
}
}
}
Features:
class MainViewModel : BaseViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
fun showDatePicker() {
// Date picker implementation
}
fun showTimePicker() {
// Time picker implementation
}
fun showMapExample() {
// Navigation to maps example
}
fun testNetworkCall() {
viewModelScope.launch {
setLoading(true)
try {
val result = NetworkUtils.get("https://api.example.com/test")
PopupHandler.showAlert(
title = "Success",
message = "Response: $result"
)
} catch (e: Exception) {
setError(e.message)
} finally {
setLoading(false)
}
}
}
fun showImagePicker() {
// Image picker implementation
}
}
data class MainUiState(
val selectedDate: LocalDate? = null,
val selectedTime: LocalTime? = null,
val profileImage: String? = null,
val searchQuery: String = "",
val selectedCountry: String? = null
)
Features:
class AboutScreen : BaseScreen() {
@Composable
override fun ScreenContent() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Header with back button
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { navigator?.pop() }
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
Text(
text = "About",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.weight(1f)
)
}
// Application information
AboutContent(
appName = "KMP Test App",
version = "1.0.0",
description = "Demonstration application for SweetMeSoft KMP Library. " +
"Shows the use of all components and functionalities " +
"available in the multiplatform library.",
developers = listOf(
Developer(
name = "SweetMeSoft Team",
role = "Development",
email = "team@sweetmesoft.com"
)
),
links = listOf(
AppLink(
title = "GitHub Repository",
url = "https://github.com/erickvelasco11/KmpLibrary",
icon = Icons.Default.Code
),
AppLink(
title = "Documentation",
url = "https://erickvelasco11.github.io/KmpLibrary",
icon = Icons.Default.MenuBook
)
)
)
}
}
}
Features:
val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
val md_theme_dark_primary = Color(0xFFD0BCFF)
val md_theme_dark_onPrimary = Color(0xFF381E72)
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
// Custom colors
val SweetMeSoftBlue = Color(0xFF2196F3)
val SweetMeSoftGreen = Color(0xFF4CAF50)
val SweetMeSoftOrange = Color(0xFFFF9800)
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
// ... more colors
)
private val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
// ... more colors
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
}
Features:
@Composable
fun ComponentsSection(viewModel: MainViewModel) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "KMPControls Components",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
// Date selector
ClickableOutlinedTextField(
value = viewModel.uiState.value.selectedDate?.toString() ?: "",
label = "Select Date",
onClick = { viewModel.showDatePicker() },
modifier = Modifier.fillMaxWidth()
)
// Time selector
ClickableOutlinedTextField(
value = viewModel.uiState.value.selectedTime?.toString() ?: "",
label = "Select Time",
onClick = { viewModel.showTimePicker() },
modifier = Modifier.fillMaxWidth()
)
// Password control
var password by remember { mutableStateOf("") }
PasswordControl(
value = password,
onValueChange = { password = it },
label = "Example Password",
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
fun MapsSection(viewModel: MainViewModel) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "KMPMaps Components",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "Multiplatform maps with Google Maps (Android) and MapKit (iOS)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Button(
onClick = { viewModel.showMapExample() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Map, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("View Map Example")
}
}
}
}
@Composable
fun UtilitiesSection(viewModel: MainViewModel) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Library Utilities",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
// Network test
Button(
onClick = { viewModel.testNetworkCall() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.CloudDownload, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Test Network Call")
}
// Image selector
Button(
onClick = { viewModel.showImagePicker() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Image, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Select Image")
}
// Validation example
var email by remember { mutableStateOf("") }
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Test Email") },
isError = email.isNotBlank() && !StringUtils.isValidEmail(email),
supportingText = {
if (email.isNotBlank() && !StringUtils.isValidEmail(email)) {
Text("Invalid email")
}
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
# Clone the repository
git clone https://github.com/erickvelasco11/KmpLibrary.git
cd KmpLibrary
# Run on Android
./gradlew :kmptestapp:installDebug
# Or from Android Studio
# 1. Open the project
# 2. Select 'kmptestapp' as module
# 3. Run on Android device/emulator
# Generate Xcode project
./gradlew :kmptestapp:embedAndSignAppleFrameworkForXcode
# Open in Xcode
open iosApp/iosApp.xcodeproj
# Or from Android Studio with KMP plugin
# 1. Select iOS configuration
# 2. Run on iOS simulator/device
To use maps functionalities, configure the API keys:
<!-- android/src/main/res/values/strings.xml -->
<resources>
<string name="google_maps_key">YOUR_API_KEY_HERE</string>
</resources>
<!-- iosApp/iosApp/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to show maps</string>