Imported from estebanl/TalkToMeMobile (
AGENTS.md). Install upstream withnpx skills add estebanl/TalkToMeMobile. Copyright stays with the author.
AGENTS.md - TalkToMeMobile Development Guide
Build Commands
- Android Debug APK:
./gradlew :composeApp:assembleDebug - Android Release APK:
./gradlew :composeApp:assembleRelease - iOS Build:
./gradlew :composeApp:compileKotlinIosArm64 - Full Build:
./gradlew build
Test Commands
- All Tests:
./gradlew test - Android Tests Only:
./gradlew :composeApp:testDebugUnitTest - iOS Simulator Tests:
./gradlew iosX64Test - Single Test:
./gradlew test --tests "com.estebanl.talktomeappmobile.SomeClass.someTest" - Instrumentation Tests:
./gradlew :composeApp:connectedDebugAndroidTest
Lint Commands
No dedicated linting tools configured (ktlint, spotless, detekt not found in build.gradle.kts).
Code Style Guidelines
General Kotlin Conventions
- Naming: camelCase for functions/variables, PascalCase for classes/interfaces, UPPER_SNAKE_CASE for constants
- Imports: Remove unused imports before committing; organize with blank lines between groups
- Formatting: 4-space indentation, 100-character line length, trailing commas in multi-line structures
- Types: Prefer
valovervar; use explicit types for public APIs; avoidAnyunless necessary - Error Handling: Use try/catch for recoverable errors; throw exceptions for programming errors; log errors appropriately
Compose Multiplatform Specific
- Stateless Composables: Prefer stateless composables with state hoisting for reusability/testability
- State Management: Hoist state to ViewModels or parent composables; avoid internal
rememberfor business logic - Localization: ALWAYS use
stringResource(Res.string.key)for user-facing text; NEVER hardcode strings - Preview: Place
@Previewcomposables in same file as component; use correct import:org.jetbrains.compose.ui.tooling.preview.Preview
Platform Patterns
- expect/actual: Use for platform-specific implementations (Bluetooth, DataStore, ImagePicker)
- Dependency Injection: Koin modules in AppModule.kt; use named qualifiers for HTTP clients
- Architecture: MVVM with repository pattern; separate business logic from UI
Security & Best Practices
- Permissions: Check Bluetooth/location permissions before operations
- Secrets: Never commit API keys or sensitive data
- Threading: Use coroutines for async operations; avoid blocking UI thread
Copilot Instructions
TalkToMe Mobile - AI Agent Instructions
Project Overview
TalkToMe is a Kotlin Multiplatform Mobile (KMP) app for iOS and Android that uses iBeacon technology for proximity-based user discovery. Users broadcast as beacons and discover nearby users through BLE scanning, with user identification via a backend API.
Architecture
Platform Structure
commonMain/: Shared Kotlin code (ViewModels, repositories, network, data models)androidMain/: Android platform implementations (BluetoothManager, DataStore, ImagePicker)iosMain/: iOS platform implementations using CoreLocation/CoreBluetoothiosApp/: SwiftUI wrapper for the iOS application
Key Components
1. Bluetooth/iBeacon System
- Interface:
BluetoothManager.kt- Common interface withIBEACON_UUIDconstant - Android:
BluetoothManager.android.kt- Uses AltBeacon library for BLE advertising/scanning - iOS:
BluetoothManager.ios.kt- Uses CLLocationManager for beacon ranging, CBPeripheralManager for advertising - Encoding:
BeaconDataEncoder.kt- Converts user UUIDs to Major/Minor using hash (32-bit: 16-bit major + 16-bit minor)
2. Network Layer
- HTTP Clients: Two separate Ktor clients in
HttpClientFactory.ktauthClient: For login/registration (no auth headers)apiClient: For authenticated endpoints (includes token viaAuthInterceptorPlugin)
- Token Management:
TokenManager.kt- Auto-refreshes tokens 5 minutes before expiration using mutex for thread safety - API Services: Located in
data/network/api/- one service per domain (UserApiService, BeaconApiService, ProfileApiService, etc.)
3. Dependency Injection
- Koin: All DI configured in
AppModule.kt - Named qualifiers: Use
named("authClient")andnamed("apiClient")to distinguish HTTP clients - Mock Repository: Currently using
MockBeaconRepositoryinstead of realBeaconRepository(see commented line in AppModule)
4. Data Persistence
- DataStore: Platform-specific implementations via
expect/actualpattern - UserPreferencesManager: Manages auth tokens, user data, and login state
5. Localization
- String Resources: Located in
composeApp/src/commonMain/composeResources/values/strings.xml: English translations (default)values-es/strings.xml: Spanish translations
- Usage Pattern: Always use
stringResource(Res.string.key_name)in Compose UI - Resource Access: Import from
talktomeappmobile.composeapp.generated.resources.*
Example:
// ❌ Wrong - hardcoded string
Text(text = "Welcome Back")
// ✅ Correct - localized string resource
Text(text = stringResource(Res.string.welcome_back))
// String resources (values/strings.xml)
<string name="welcome_back">Welcome Back</string>
// String resources (values-es/strings.xml)
<string name="welcome_back">Bienvenido de Nuevo</string>
Critical Patterns
expect/actual Pattern
Use for platform-specific implementations:
// commonMain
expect class ImagePicker { suspend fun pickImage(): FileData? }
// androidMain
actual class ImagePicker(private val context: Context) { ... }
// iosMain
actual class ImagePicker { ... }
State Hoisting and Stateless Composables
Prefer stateless composables with state hoisting for all UI components to maximize reusability, testability, and predictability.
- Stateless Composables: Receive data via parameters and communicate events via callbacks. No internal state management with
remember. - State Hoisting: Move state to the composable's caller (typically ViewModel or parent composable) and pass it down.
- When to use stateful: Only for truly self-contained UI logic that won't need testing or reuse (e.g., internal animation state, text field focus).
// ❌ Wrong - stateful composable with hidden state
@Composable
fun UserCard(user: User) {
var isExpanded by remember { mutableStateOf(false) }
Card(onClick = { isExpanded = !isExpanded }) {
Text(user.name)
if (isExpanded) Text(user.bio)
}
}
// ✅ Correct - stateless composable with hoisted state
@Composable
fun UserCard(
user: User,
isExpanded: Boolean,
onExpandChange: (Boolean) -> Unit
) {
Card(onClick = { onExpandChange(!isExpanded) }) {
Text(user.name)
if (isExpanded) Text(user.bio)
}
}
// Usage in ViewModel-backed screen
@Composable
fun UsersScreen(viewModel: UsersViewModel) {
val expandedUserId by viewModel.expandedUserId.collectAsState()
LazyColumn {
items(users) { user ->
UserCard(
user = user,
isExpanded = expandedUserId == user.id,
onExpandChange = { viewModel.toggleUserExpanded(user.id) }
)
}
}
}
iBeacon Discovery Flow
- Device A:
startAdvertising(username, userUuid)→ encodes UUID to major/minor → broadcasts as iBeacon - Device B:
startScanning()→ detects beacon → extracts major/minor → callsBeaconRepository.getUserByBeaconHash(hash, rssi) - Backend API maps beacon hash to user profile
- Device B displays user info in UI
See BEACON_USER_IDENTIFICATION_FLOW.md for details.
Development Workflow
Building & Testing
ALWAYS run after making changes:
./gradlew test # Run common and Android tests
./gradlew iosX64Test # Run iOS simulator tests
Android-Specific
./gradlew :composeApp:assembleDebug # Build APK
./gradlew :composeApp:compileDebugKotlinAndroid # Verify Android compilation
iOS-Specific
./gradlew :composeApp:compileKotlinIosArm64 # Verify iOS compilation
# Or open iosApp/ in Xcode to run
Code Quality
- ALWAYS remove unused imports from modified files before committing
- Use
@SuppressLint("MissingPermission")only after verifying proper permission checks exist - Bluetooth operations require runtime permission checks on both platforms
- NEVER use hardcoded strings in UI code - ALL user-facing text must use string resources
- ALWAYS provide both English and Spanish translations for all string resources
- PREFER stateless composables with hoisted state over stateful composables - only manage state internally for truly self-contained logic
- ALWAYS place
@Previewcomposables in the same file as the component they preview - do not create separate preview files - ALWAYS use
import org.jetbrains.compose.ui.tooling.preview.Previewfor@Previewannotations (this is the correct Compose Multiplatform import)
Common Pitfalls
HTTP Client Confusion
// ❌ Wrong - using apiClient for unauthenticated endpoints
single { UserApiService(get(qualifier = named("apiClient"))) }
// ✅ Correct - authClient for login/register
single { UserApiService(get(qualifier = named("authClient"))) }
Localization
- ❌ Never use hardcoded strings in UI components
- ✅ All user-facing text must be in
composeResources/values/strings.xml(English) andvalues-es/strings.xml(Spanish) - ✅ Use descriptive key names:
login_button,email_address,welcome_back - ✅ Escape special characters in XML:
Don\'tfor apostrophes,\nfor newlines
// ❌ Wrong - hardcoded text
Button(onClick = {}) {
Text("Log In")
}
// ✅ Correct - localized text
Button(onClick = {}) {
Text(stringResource(Res.string.login_button))
}
Scaffold Usage
- ❌ Never nest
Scaffoldcomponents - each screen should have only oneScaffold - ✅ The main
Scaffoldis already defined inApp.ktwith bottom navigation - ✅ Individual screens should NOT wrap their content in another
Scaffold - ✅ Use
Column,Box,Surface,Card, or other composables for nested layouts within screens
// ❌ Wrong - nesting Scaffold in a screen
@Composable
fun MyScreen() {
Scaffold(topBar = { TopAppBar(...) }) { padding ->
// This conflicts with the App.kt Scaffold
}
}
// ✅ Correct - use Column or other containers
@Composable
fun MyScreen() {
Column(modifier = Modifier.fillMaxSize()) {
// Screen content here
}
}
Version Catalog
All dependencies managed in gradle/libs.versions.toml. Reference as:
implementation(libs.ktor.client.core)
implementation(libs.altbeacon) // Android beacon library
Documentation
Extensive implementation docs in docs/ directory:
COMPLETE_SYSTEM_STATUS.md- Full system overviewPART1_COMPLETE_STATUS.md- iBeacon migration detailsAPI_INTEGRATION_COMPLETE.md- Backend integration guideBEACON_SCANNING_FIX.md- Troubleshooting Android scanning issues
Testing Strategy
- Unit tests: Mock network and Bluetooth with Koin test module
- Manual testing: Requires two physical devices (one Android + one iOS) for real beacon discovery
- Permissions: Test permission flows thoroughly - Bluetooth permissions vary by Android API level (24+ vs 31+)