Agent skill
android-expert
Install this agent skill to your Project
npx add-skill https://github.com/vitorpamplona/amethyst/tree/main/.claude/skills/android-expert
SKILL.md
android-expert
Android platform expertise for Amethyst Multiplatform project. Covers Compose Navigation, Material3, permissions, lifecycle, and Android-specific patterns in KMP architecture.
When to Use
Auto-invoke when working with:
- Android navigation (Navigation Compose, routes, bottom nav)
- Runtime permissions (camera, notifications, biometric)
- Platform APIs (Intent, Context, Activity)
- Material3 theming and edge-to-edge UI
- Android build configuration (Proguard, APK optimization)
- AndroidManifest.xml configuration
- Android lifecycle (ViewModel, collectAsStateWithLifecycle)
Core Mental Model
Single Activity Architecture + Compose Navigation
MainActivity (Single Entry Point)
├── enableEdgeToEdge()
├── AmethystTheme { }
└── NavHost
├── Route.Home → HomeScreen
├── Route.Profile(id) → ProfileScreen
└── Route.Settings → SettingsScreen
Intent Filters (11+)
├── ACTION_MAIN (launcher)
├── ACTION_SEND (share)
├── ACTION_VIEW (deep links: nostr://, https://...)
└── NFC_ACTION_NDEF_DISCOVERED
Key Principles:
- Type-Safe Navigation - @Serializable routes, no strings
- Declarative Permissions - Request contextually with Accompanist
- Edge-to-Edge + Insets - Scaffold handles system bars
- ViewModel + Flow → State - Survive config changes
- Platform Isolation - Android code in
amethyst/module orandroidMain/
Architecture Overview
Module Structure
amethyst/ # Android app module
├── src/
│ ├── main/
│ │ ├── java/com/vitorpamplona/amethyst/
│ │ │ ├── ui/
│ │ │ │ ├── MainActivity.kt # Entry point
│ │ │ │ ├── navigation/
│ │ │ │ │ ├── AppNavigation.kt # NavHost
│ │ │ │ │ ├── routes/Routes.kt # @Serializable routes
│ │ │ │ │ └── bottombars/AppBottomBar.kt
│ │ │ │ ├── screen/ # 80+ screens
│ │ │ │ └── theme/Theme.kt # Material3 theme
│ │ │ └── Amethyst.kt # Application class
│ │ └── AndroidManifest.xml # Permissions, intent filters
│ └── androidMain/ # KMP Android source set
│ └── kotlin/ # Platform-specific code
└── build.gradle # Android config
1. Type-Safe Navigation
Pattern: @Serializable Routes
Best Practice (Navigation 2.8.0+):
// Routes.kt - Define all routes with type safety
@Serializable
sealed class Route {
@Serializable object Home : Route()
@Serializable object Search : Route()
@Serializable data class Profile(val pubkey: String) : Route()
@Serializable data class Note(val noteId: String) : Route()
@Serializable data class Thread(val noteId: String) : Route()
}
// AppNavigation.kt - NavHost setup
@Composable
fun AppNavigation(
navController: NavHostController,
accountViewModel: AccountViewModel
) {
NavHost(
navController = navController,
startDestination = Route.Home,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
composable<Route.Home> {
HomeScreen(accountViewModel, navController)
}
composable<Route.Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Route.Profile>()
ProfileScreen(profile.pubkey, accountViewModel, navController)
}
composable<Route.Note> { backStackEntry ->
val note = backStackEntry.toRoute<Route.Note>()
NoteScreen(note.noteId, accountViewModel, navController)
}
}
}
Navigation Manager Pattern
Amethyst Pattern (Nav.kt):
class Nav(
val controller: NavHostController,
val drawerState: DrawerState,
val scope: CoroutineScope
) {
fun nav(route: Route) {
scope.launch {
controller.navigate(route)
drawerState.close()
}
}
fun newStack(route: Route) {
scope.launch {
controller.navigate(route) {
popUpTo(Route.Home) { inclusive = false }
}
drawerState.close()
}
}
fun popBack() {
controller.popBackStack()
}
}
// Usage in composables
@Composable
fun HomeScreen(nav: Nav) {
Button(onClick = { nav.nav(Route.Profile("npub1...")) }) {
Text("View Profile")
}
}
Bottom Navigation
Material3 Pattern:
@Composable
fun AppBottomBar(
selectedRoute: Route,
nav: Nav
) {
NavigationBar {
BottomBarItem.entries.forEach { item ->
NavigationBarItem(
selected = selectedRoute::class == item.route::class,
onClick = { nav.nav(item.route) },
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }
)
}
}
}
enum class BottomBarItem(val route: Route, val icon: ImageVector, val label: String) {
HOME(Route.Home, Icons.Default.Home, "Home"),
MESSAGES(Route.Messages, Icons.Default.Message, "Messages"),
NOTIFICATIONS(Route.Notifications, Icons.Default.Notifications, "Notifications"),
SEARCH(Route.Search, Icons.Default.Search, "Search"),
PROFILE(Route.Profile, Icons.Default.Person, "Profile")
}
Reference: See references/android-navigation.md for complete navigation patterns.
2. Runtime Permissions
Declarative Permission Handling
Accompanist Pattern (Experimental API):
import com.google.accompanist.permissions.*
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraFeature() {
val cameraPermissionState = rememberPermissionState(
Manifest.permission.CAMERA
)
when {
cameraPermissionState.status.isGranted -> {
// Permission granted - show camera UI
CameraPreview()
}
cameraPermissionState.status.shouldShowRationale -> {
// Show rationale and request again
Column {
Text("Camera permission is needed to scan QR codes")
Button(
onClick = { cameraPermissionState.launchPermissionRequest() }
) {
Text("Grant Permission")
}
}
}
else -> {
// First time - request permission
Button(
onClick = { cameraPermissionState.launchPermissionRequest() }
) {
Text("Enable Camera")
}
}
}
}
Multiple Permissions
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MediaUploadFeature() {
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
when {
permissionsState.allPermissionsGranted -> {
MediaUploadUI()
}
permissionsState.shouldShowRationale -> {
RationaleDialog(
onConfirm = { permissionsState.launchMultiplePermissionRequest() },
onDismiss = { /* Handle dismissal */ }
)
}
else -> {
PermissionRequestButton(
onClick = { permissionsState.launchMultiplePermissionRequest() }
)
}
}
}
Lifecycle-Aware Permission Requests
Amethyst Pattern (LoggedInPage.kt):
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NotificationRegistration(accountViewModel: AccountViewModel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val notificationPermissionState = rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS
)
if (notificationPermissionState.status.isGranted) {
LifecycleResumeEffect(
key1 = accountViewModel,
notificationPermissionState.status.isGranted
) {
val scope = rememberCoroutineScope()
scope.launch {
PushNotificationUtils.checkAndInit(
context = context,
accountViewModel = accountViewModel
)
}
onPauseOrDispose {
// Cleanup when paused
}
}
}
}
}
AndroidManifest Permission Declarations
Key Permissions in Amethyst:
<!-- AndroidManifest.xml -->
<manifest>
<!-- Network -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- Media -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Android 13+ Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- NFC -->
<uses-permission android:name="android.permission.NFC" />
<!-- Location (for geohashing) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Foreground Services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
</manifest>
Reference: See references/android-permissions.md for complete permission patterns.
3. Material3 + Edge-to-Edge
Edge-to-Edge Setup
MainActivity Pattern:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // Android 15+ immersive UI
super.onCreate(savedInstanceState)
setContent {
AmethystTheme {
AccountScreen()
}
}
}
}
Theme Configuration
Material3 Color Schemes:
// theme/Theme.kt
private val DarkColorPalette = darkColorScheme(
primary = Purple200,
secondary = Teal200,
tertiary = Pink80,
background = Color.Black,
surface = Color.Black,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.White,
onSurface = Color.White
)
private val LightColorPalette = lightColorScheme(
primary = Purple500,
secondary = Teal700,
tertiary = Pink40,
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color.Black,
onSurface = Color.Black
)
@Composable
fun AmethystTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorPalette else LightColorPalette
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Scaffold with Insets
Handling System Bars:
@Composable
fun MainScreen(navController: NavHostController) {
val currentRoute by navController.currentBackStackEntryAsState()
Scaffold(
topBar = { AppTopBar(currentRoute) },
bottomBar = { AppBottomBar(currentRoute, navController) },
floatingActionButton = { NewPostFab() }
) { innerPadding ->
// Scaffold automatically handles system bar insets
NavHost(
navController = navController,
modifier = Modifier.padding(innerPadding)
) {
// Routes...
}
}
}
Custom Inset Handling:
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
@Composable
fun CustomEdgeToEdgeScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding() // Add padding for system bars
) {
// Content draws edge-to-edge with safe padding
}
}
4. ViewModel + Lifecycle
ViewModel Pattern
Standard Structure (80+ ViewModels in Amethyst):
class FeedViewModel(
private val accountStateViewModel: AccountStateViewModel
) : ViewModel() {
private val _feedState = MutableStateFlow<FeedState>(FeedState.Loading)
val feedState: StateFlow<FeedState> = _feedState.asStateFlow()
init {
loadFeed()
}
fun loadFeed() {
viewModelScope.launch {
_feedState.value = FeedState.Loading
try {
val posts = repository.getFeed()
_feedState.value = FeedState.Success(posts)
} catch (e: Exception) {
_feedState.value = FeedState.Error(e.message)
}
}
}
fun refresh() {
loadFeed()
}
}
sealed class FeedState {
object Loading : FeedState()
data class Success(val posts: List<Post>) : FeedState()
data class Error(val message: String?) : FeedState()
}
Compose Integration
collectAsStateWithLifecycle Pattern:
@Composable
fun FeedScreen(
feedViewModel: FeedViewModel = viewModel()
) {
val feedState by feedViewModel.feedState.collectAsStateWithLifecycle()
when (feedState) {
is FeedState.Loading -> {
LoadingIndicator()
}
is FeedState.Success -> {
val posts = (feedState as FeedState.Success).posts
LazyColumn {
items(posts) { post ->
PostCard(post)
}
}
}
is FeedState.Error -> {
ErrorScreen(
message = (feedState as FeedState.Error).message,
onRetry = { feedViewModel.refresh() }
)
}
}
}
Lifecycle Effects
LifecycleResumeEffect Pattern:
@Composable
fun ChatScreen(chatViewModel: ChatViewModel) {
LifecycleResumeEffect(key1 = chatViewModel) {
// Called when composable resumes (onResume)
chatViewModel.connectToRelay()
onPauseOrDispose {
// Called when composable pauses (onPause) or disposes
chatViewModel.disconnectFromRelay()
}
}
}
DisposableEffect for Cleanup:
@Composable
fun VideoPlayer(videoUrl: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUrl))
prepare()
}
}
DisposableEffect(videoUrl) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = { PlayerView(it).apply { player = exoPlayer } }
)
}
5. Platform APIs
Activity & Context Access
LocalContext Pattern:
@Composable
fun ShareButton(text: String) {
val context = LocalContext.current
Button(
onClick = {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
context.startActivity(Intent.createChooser(intent, "Share via"))
}
) {
Text("Share")
}
}
Activity Reference:
// WindowUtils.kt pattern
@Composable
fun getActivity(): Activity? = LocalContext.current.getActivity()
tailrec fun Context.getActivity(): ComponentActivity =
when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.getActivity()
else -> throw IllegalStateException("Context not an Activity")
}
// Usage
@Composable
fun FullscreenToggle() {
val activity = getActivity()
Button(
onClick = {
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
}
) {
Text("Go Fullscreen")
}
}
Intent Handling
Deep Links (AppNavigation.kt pattern):
@Composable
fun AppNavigation(
navController: NavHostController,
accountViewModel: AccountViewModel
) {
val activity = LocalContext.current as? Activity
LaunchedEffect(activity?.intent) {
activity?.intent?.let { intent ->
when (intent.action) {
Intent.ACTION_SEND -> {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
val sharedImage = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
navController.navigate(
Route.NewPost(message = sharedText, attachment = sharedImage.toString())
)
}
Intent.ACTION_VIEW -> {
val uri = intent.data
when (uri?.scheme) {
"nostr" -> handleNostrUri(uri, navController)
"https", "http" -> handleWebUri(uri, navController)
}
}
}
}
}
NavHost(navController = navController) {
// Routes...
}
}
fun handleNostrUri(uri: Uri, navController: NavHostController) {
// nostr:npub1... -> Profile
// nostr:note1... -> Note
// nostr:nevent1... -> Event
when {
uri.path?.startsWith("npub") == true -> {
navController.navigate(Route.Profile(uri.path!!))
}
uri.path?.startsWith("note") == true -> {
navController.navigate(Route.Note(uri.path!!))
}
}
}
File Sharing with FileProvider
ShareHelper Pattern:
fun shareImage(context: Context, imageUri: Uri) {
try {
// Get file from cache
val cachedFile = getCachedFile(context, imageUri)
// Create content URI via FileProvider
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
cachedFile
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
putExtra(Intent.EXTRA_STREAM, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, "Share Image"))
} catch (e: Exception) {
Toast.makeText(context, "Failed to share: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
FileProvider Configuration (AndroidManifest.xml):
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
Activity Results
External Signer Integration (Amethyst pattern):
@Composable
fun SignerIntegration(accountViewModel: AccountViewModel) {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { data ->
accountViewModel.account.signer.newResponse(data)
}
}
}
Button(
onClick = {
val signerIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("nostrsigner:...")
}
launcher.launch(signerIntent)
}
) {
Text("Sign with External App")
}
}
6. Build Configuration
Android Block
build.gradle (Amethyst pattern):
android {
namespace = 'com.vitorpamplona.amethyst'
compileSdk = 36
defaultConfig {
applicationId = "com.vitorpamplona.amethyst"
minSdk = 26 // Android 8.0 (Oreo)
targetSdk = 36 // Android 15
versionCode = 442
versionName = "1.08.0"
vectorDrawables {
useSupportLibrary = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
compose = true
buildConfig = true // Enable BuildConfig access
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
// Product flavors for Play Store vs F-Droid
flavorDimensions = ["channel"]
productFlavors {
create("play") {
dimension = "channel"
// Firebase, Google services
}
create("fdroid") {
dimension = "channel"
// UnifiedPush, open-source alternatives
}
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
}
Dependencies
Key Android Dependencies:
dependencies {
// Compose BOM
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
// Navigation
implementation(libs.androidx.navigation.compose)
// Lifecycle
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Activity
implementation(libs.androidx.activity.compose)
// Accompanist
implementation(libs.accompanist.permissions)
// Shared module
implementation(project(":commons"))
implementation(project(":quartz"))
}
Proguard Rules
Common Rules for Amethyst:
# Keep Kotlin metadata
-keep class kotlin.Metadata { *; }
# Keep Nostr event classes
-keep class com.vitorpamplona.quartz.events.** { *; }
# Keep serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
# OkHttp
-dontwarn okhttp3.**
-keep class okhttp3.** { *; }
# Compose
-keep class androidx.compose.** { *; }
-dontwarn androidx.compose.**
Reference: See references/proguard-rules.md for complete Proguard configuration.
APK Optimization
Reference: See scripts/analyze-apk-size.sh for APK size analysis.
7. KMP Android Source Sets
Android Module Layout
Amethyst Structure:
amethyst/
├── src/
│ ├── main/ # Standard Android
│ │ ├── java/com/.../ # Compose UI code
│ │ ├── res/ # Android resources
│ │ └── AndroidManifest.xml
│ └── androidMain/ # KMP Android source set (if needed)
│ └── kotlin/ # Platform-specific utilities
└── build.gradle
Platform-Specific Code:
// commons/src/androidMain/kotlin/Platform.android.kt
actual fun openExternalUrl(url: String, context: Any) {
val ctx = context as Context
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
ctx.startActivity(intent)
}
actual fun shareText(text: String, context: Any) {
val ctx = context as Context
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
ctx.startActivity(Intent.createChooser(intent, "Share"))
}
Build Configuration for KMP
kotlin {
androidTarget {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
}
}
android {
sourceSets {
// Link androidMain source set
getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml")
java.srcDirs("src/main/java", "src/androidMain/kotlin")
}
}
}
Common Patterns
1. Single Activity Architecture
All screens in one activity, navigation via Compose:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
AmethystTheme {
val accountViewModel: AccountStateViewModel = viewModel()
AccountScreen(accountViewModel)
}
}
}
override fun onResume() {
super.onResume()
DEFAULT_MUTED_SETTING.value = true
}
override fun onPause() {
super.onPause()
LanguageTranslatorService.clear()
}
}
2. Configuration Changes
ViewModels survive rotation:
// ViewModel persists across config changes
@Composable
fun ProfileScreen(
profileViewModel: ProfileViewModel = viewModel()
) {
val profile by profileViewModel.profile.collectAsStateWithLifecycle()
// UI rebuilds on rotation, but ViewModel data persists
ProfileContent(profile)
}
3. Resource Access
@Composable
fun LocalizedButton() {
val context = LocalContext.current
Button(
onClick = {
val message = context.getString(R.string.button_clicked)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
) {
Text(stringResource(R.string.button_label))
}
}
Testing Android Components
Navigation Testing
@Test
fun testNavigationToProfile() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
composeTestRule.setContent {
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavigation(navController, accountViewModel)
}
composeTestRule.onNodeWithText("Profile").performClick()
assertEquals(
Route.Profile::class,
navController.currentBackStackEntry?.destination?.route::class
)
}
Permission Testing
@Test
fun testPermissionRequest() {
val scenario = launchActivity<MainActivity>()
scenario.onActivity { activity ->
// Grant permission via UiAutomator
grantPermissionViaUi(Manifest.permission.CAMERA)
}
composeTestRule.onNodeWithText("Camera Ready").assertExists()
}
Anti-Patterns to Avoid
- String-based navigation - Use type-safe @Serializable routes
- Requesting permissions eagerly - Request contextually before feature use
- Ignoring edge-to-edge - Handle insets properly with Scaffold
- Using GlobalScope - Use viewModelScope or rememberCoroutineScope
- Not handling config changes - Use ViewModel + collectAsStateWithLifecycle
- Hardcoded system bar heights - Use WindowInsets APIs
- Blocking main thread - Use viewModelScope.launch(Dispatchers.IO)
Quick Reference
| Task | Pattern |
|---|---|
| Navigate | navController.navigate(Route.Profile(id)) |
| Request Permission | rememberPermissionState().launchPermissionRequest() |
| Access Context | val context = LocalContext.current |
| Get Activity | val activity = context.getActivity() |
| Open URL | Intent(ACTION_VIEW, Uri.parse(url)) |
| Share Text | Intent(ACTION_SEND).putExtra(EXTRA_TEXT, text) |
| Observe Flow | flow.collectAsStateWithLifecycle() |
| Lifecycle Effect | LifecycleResumeEffect { ... } |
| Handle Insets | Modifier.systemBarsPadding() |
| Theme | MaterialTheme(colorScheme = ...) { } |
File Locations
Key Android Files:
amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.ktamethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.ktamethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.ktamethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.ktamethyst/src/main/AndroidManifest.xmlamethyst/build.gradle
Additional Resources
references/android-navigation.md- Complete navigation patterns and examplesreferences/android-permissions.md- Permission handling patternsreferences/proguard-rules.md- Proguard configurationscripts/analyze-apk-size.sh- APK size optimization script
When NOT to Use
- Desktop-specific features → Use
desktop-expertskill - iOS-specific features → Use
ios-expertskill - Shared KMP code → Use
kotlin-multiplatformskill - Nostr protocol → Use
nostr-expertskill - Compose UI components → Use
compose-expertskill
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
find-non-lambda-logs
Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
quartz-integration
desktop-expert
compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
find-missing-translations
Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language
kotlin-multiplatform
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
Didn't find tool you were looking for?