implementing-android-code
npx skills add https://github.com/bitwarden/android --skill implementing-android-code
Agent 安装分布
Skill 文档
Implementing Android Code – Bitwarden Quick Reference
This skill provides tactical guidance for Bitwarden-specific patterns. For comprehensive architecture decisions and complete code style rules, consult docs/ARCHITECTURE.md and docs/STYLE_AND_BEST_PRACTICES.md.
Critical Patterns Reference
A. ViewModel Implementation (State-Action-Event Pattern)
All ViewModels follow the State-Action-Event (SAE) pattern via BaseViewModel<State, Event, Action>.
Key Requirements:
- Annotate with
@HiltViewModel - State class MUST be
@Parcelize data class : Parcelable - Implement
handleAction(action: A)– MUST be synchronous - Post internal actions from coroutines using
sendAction() - Save/restore state via
SavedStateHandle[KEY_STATE] - Private action handlers:
private fun handle*naming convention
Template: See ViewModel template
Pattern Summary:
@HiltViewModel
class ExampleViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository,
) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(
initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),
) {
init {
stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)
}
override fun handleAction(action: ExampleAction) {
// Synchronous dispatch only
when (action) {
is Action.Click -> handleClick()
is Action.Internal.DataReceived -> handleDataReceived(action)
}
}
private fun handleClick() {
viewModelScope.launch {
val result = repository.fetchData()
sendAction(Action.Internal.DataReceived(result)) // Post internal action
}
}
private fun handleDataReceived(action: Action.Internal.DataReceived) {
mutableStateFlow.update { it.copy(data = action.result) }
}
}
Reference:
ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt(seehandleActionmethod)app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt(see class declaration)
Critical Gotchas:
- â NEVER update
mutableStateFlowdirectly inside coroutines - â
ALWAYS post internal actions from coroutines, update state in
handleAction() - â NEVER forget
@IgnoredOnParcelfor sensitive data (causes security leak) - â
ALWAYS use
@Parcelizeon state classes for process death recovery - â
State restoration happens automatically if properly saved to
SavedStateHandle
B. Navigation Implementation (Type-Safe)
All navigation uses type-safe routes with kotlinx.serialization.
Pattern Structure:
@Serializableroute data class with parameters...Argshelper class for extracting fromSavedStateHandleNavGraphBuilder.{screen}Destination()extension for adding screen to graphNavController.navigateTo{Screen}()extension for navigation calls
Template: See Navigation template
Pattern Summary:
@Serializable
data class ExampleRoute(val userId: String, val isEditMode: Boolean = false)
data class ExampleArgs(val userId: String, val isEditMode: Boolean)
fun SavedStateHandle.toExampleArgs(): ExampleArgs {
val route = this.toRoute<ExampleRoute>()
return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)
}
fun NavController.navigateToExample(
userId: String,
isEditMode: Boolean = false,
navOptions: NavOptions? = null,
) {
this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)
}
fun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {
composableWithSlideTransitions<ExampleRoute> {
ExampleScreen(onNavigateBack = onNavigateBack)
}
}
Reference: app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt (see LoginRoute and extensions)
Key Benefits:
- â Type safety: Compile-time errors for missing parameters
- â No string literals in navigation code
- â Automatic serialization/deserialization
- â Clear contract for screen dependencies
C. Screen/Compose Implementation
All screens follow consistent Compose patterns.
Template: See Screen/Compose template
Key Patterns:
@Composable
fun ExampleScreen(
onNavigateBack: () -> Unit,
viewModel: ExampleViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
ExampleEvent.NavigateBack -> onNavigateBack()
}
}
BitwardenScaffold(
topBar = {
BitwardenTopAppBar(
title = stringResource(R.string.title),
navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.BackClick) }
},
)
},
) {
// UI content
}
}
Reference: app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt (see LoginScreen composable)
Essential Requirements:
- â
Use
hiltViewModel()for dependency injection - â
Use
collectAsStateWithLifecycle()for state (notcollectAsState()) - â
Use
EventsEffect(viewModel)for one-shot events - â
Use
remember(viewModel) { }for stable callbacks to prevent recomposition - â
Use
Bitwarden*prefixed components from:uimodule
State Hoisting Rules:
- ViewModel state: Data that needs to survive process death or affects business logic
- UI-only state: Temporary UI state (scroll position, text field focus) using
rememberorrememberSaveable
D. Data Layer Implementation
The data layer follows strict patterns for repositories, managers, and data sources.
Interface + Implementation Separation (ALWAYS)
Template: See Data Layer template
Pattern Summary:
// Interface (injected via Hilt)
interface ExampleRepository {
suspend fun fetchData(id: String): ExampleResult
val dataFlow: StateFlow<DataState<ExampleData>>
}
// Implementation (NOT directly injected)
class ExampleRepositoryImpl(
private val exampleDiskSource: ExampleDiskSource,
private val exampleService: ExampleService,
) : ExampleRepository {
override suspend fun fetchData(id: String): ExampleResult {
// NO exceptions thrown - return Result or sealed class
return exampleService.getData(id).fold(
onSuccess = { ExampleResult.Success(it.toModel()) },
onFailure = { ExampleResult.Error(it.message) },
)
}
}
// Sealed result class (domain-specific)
sealed class ExampleResult {
data class Success(val data: ExampleData) : ExampleResult()
data class Error(val message: String?) : ExampleResult()
}
// Hilt Module
@Module
@InstallIn(SingletonComponent::class)
object ExampleRepositoryModule {
@Provides
@Singleton
fun provideExampleRepository(
exampleDiskSource: ExampleDiskSource,
exampleService: ExampleService,
): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)
}
Reference:
app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.ktapp/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt
Three-Layer Data Architecture:
- Data Sources – Raw data access (network, disk, SDK). Return
Result<T>, never throw. - Managers – Single responsibility business logic. Wrap OS/external services.
- Repositories – Aggregate sources/managers. Return domain-specific sealed classes.
Critical Rules:
- â NEVER throw exceptions in data layer
- â
ALWAYS use interface +
...Implpattern - â ALWAYS inject interfaces, never implementations
- â
Data sources return
Result<T>, repositories return domain sealed classes - â
Use
StateFlowfor continuously observed data
E. UI Components
Use Existing Components First:
The :ui module provides reusable Bitwarden* prefixed components. Search before creating new ones.
Common Components:
BitwardenFilledButton– Primary action buttonsBitwardenOutlinedButton– Secondary action buttonsBitwardenTextField– Text input fieldsBitwardenPasswordField– Password input with show/hideBitwardenSwitch– Toggle switchesBitwardenTopAppBar– Toolbar/app barBitwardenScaffold– Screen container with scaffoldBitwardenBasicDialog– Simple dialogsBitwardenLoadingDialog– Loading indicators
Component Discovery:
Search ui/src/main/kotlin/com/bitwarden/ui/platform/components/ for existing Bitwarden* components. For build, test, and codebase discovery commands, use the build-test-verify skill.
When to Create New Reusable Components:
- Component used in 3+ places
- Component needs consistent theming across app
- Component has semantic meaning (accessibility)
- Component has complex state management
New Component Requirements:
- Prefix with
Bitwarden - Accept themed colors/styles from
BitwardenTheme - Include preview composables for testing
- Support accessibility (content descriptions, semantics)
String Resources:
New strings belong in the :ui module: ui/src/main/res/values/strings.xml
- Use typographic apostrophes and quotes to avoid escape characters:
youâllnotyou\'ll,âwordânot\"word\" - Reference strings via generated
BitwardenStringresource IDs - Do not add strings to other modules unless explicitly instructed
F. Security Patterns
Encrypted vs Unencrypted Storage:
Template: See Security templates
Pattern Summary:
class ExampleDiskSourceImpl(
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
) : BaseEncryptedDiskSource(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
),
ExampleDiskSource {
fun storeAuthToken(token: String) {
putEncryptedString(KEY_TOKEN, token) // Sensitive â uses base class method
}
fun storeThemePreference(isDark: Boolean) {
putBoolean(KEY_THEME, isDark) // Non-sensitive â uses base class method
}
}
Android Keystore (Biometric Keys):
- User-scoped encryption keys:
BiometricsEncryptionManager - Keys stored in Android Keystore (hardware-backed when available)
- Integrity validation on biometric state changes
Input Validation:
// Validation returns boolean, NEVER throws
interface RequestValidator {
fun validate(request: Request): Boolean
}
// Sanitization removes dangerous content
fun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {
if (this.isNullOrBlank()) return null
// Sanitize and return safe value
}
Security Checklist:
- â
Use
@EncryptedPreferencesfor credentials, keys, tokens - â
Use
@UnencryptedPreferencesfor UI state, preferences - â
Use
@IgnoredOnParcelfor sensitive ViewModel state - â NEVER log sensitive data (passwords, tokens, vault items)
- â Validate all user input before processing
- â Use Timber for non-sensitive logging only
G. Testing Patterns
ViewModel Testing:
Template: See Testing templates
Pattern Summary:
class ExampleViewModelTest : BaseViewModelTest() {
private val mockRepository: ExampleRepository = mockk()
@Test
fun `ButtonClick should fetch data and update state`() = runTest {
val expectedResult = ExampleResult.Success(data = "test")
coEvery { mockRepository.fetchData(any()) } returns expectedResult
val viewModel = createViewModel()
viewModel.trySendAction(ExampleAction.ButtonClick)
viewModel.stateFlow.test {
assertEquals(EXPECTED_STATE.copy(data = "test"), awaitItem())
}
}
private fun createViewModel(): ExampleViewModel = ExampleViewModel(
savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),
repository = mockRepository,
)
}
Reference: app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt
Key Testing Patterns:
- â
Extend
BaseViewModelTestfor proper dispatcher management - â
Use
runTestfromkotlinx.coroutines.test - â
Use Turbine’s
.test { awaitItem() }for Flow assertions - â
Use MockK:
coEveryfor suspend functions,everyfor sync - â Test both state changes and event emissions
- â Test both success and failure Result paths
Flow Testing with Turbine:
// Test state and events simultaneously
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
viewModel.trySendAction(ExampleAction.Submit)
assertEquals(ExpectedState.Loading, stateFlow.awaitItem())
assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())
}
MockK Quick Reference:
coEvery { repository.fetchData(any()) } returns Result.success("data") // Suspend
every { diskSource.getData() } returns "cached" // Sync
coVerify { repository.fetchData("123") } // Verify
H. Clock/Time Handling
All code needing current time must inject Clock for testability.
Key Requirements:
- â
Inject
Clockvia Hilt in ViewModels - â
Pass
Clockas parameter in extension functions - â
Use
clock.instant()to get current time - â Never call
Instant.now()orDateTime.now()directly - â Never use
mockkStaticfor datetime classes in tests
Pattern Summary:
// ViewModel with Clock
class MyViewModel @Inject constructor(
private val clock: Clock,
) {
val timestamp = clock.instant()
}
// Extension function with Clock parameter
fun State.getTimestamp(clock: Clock): Instant =
existingTime ?: clock.instant()
// Test with fixed clock
val FIXED_CLOCK = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
Reference:
docs/STYLE_AND_BEST_PRACTICES.md(see Time and Clock Handling section)core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt(seeprovideClockfunction)
Critical Gotchas:
- â
Instant.now()creates hidden dependency, non-testable - â
mockkStatic(Instant::class)is fragile, can leak between tests - â
Clock.fixed(...)provides deterministic test behavior
Bitwarden-Specific Anti-Patterns
General anti-patterns are documented in CLAUDE.md. This section covers violations specific to Bitwarden’s State-Action-Event, navigation, and data layer patterns:
â NEVER update ViewModel state directly in coroutines
- Post internal actions, update state synchronously in
handleAction()
â NEVER inject ...Impl classes
- Only inject interfaces via Hilt
â NEVER create navigation without @Serializable routes
- No string-based navigation, always type-safe
â NEVER use raw Result<T> in repositories
- Use domain-specific sealed classes for better error handling
â NEVER make state classes without @Parcelize
- All ViewModel state must survive process death
â NEVER skip SavedStateHandle persistence for ViewModels
- Users lose form progress on process death
â NEVER forget @IgnoredOnParcel for passwords/tokens
- Causes security vulnerability (sensitive data in parcel)
â NEVER use generic Exception catching
- Catch specific exceptions only (
RemoteException,IOException)
â NEVER call Instant.now() or DateTime.now() directly
- Inject
Clockvia Hilt, useclock.instant()for testability
Quick Reference
For build, test, and codebase discovery commands, use the build-test-verify skill.
File Reference Format:
When pointing to specific code, use: file_path:line_number
Example: ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt (see handleAction method)