npx skills add ...
npx skills add bitwarden/android --skill implementing-android-code
This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by "How do I implement a ViewModel?", "Create a new screen", "Add navigation", "Write a repository", "BaseViewModel pattern", "State-Action-Event", "type-safe navigation", "@Serializable route", "SavedStateHandle persistence", "process death recovery", "handleAction", "sendAction", "Hilt module", "Repository pattern", "implementing a screen", "adding a data source", "handling navigation", "encrypted storage", "security patterns", "Clock injection", "DataState", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.
npx skills add bitwarden/android --skill implementing-android-code
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.
All ViewModels follow the State-Action-Event (SAE) pattern via BaseViewModel<State, Event, Action>.
Key Requirements:
@HiltViewModel@Parcelize data class : ParcelablehandleAction(action: A) - MUST be synchronoussendAction()SavedStateHandle[KEY_STATE]private fun handle* naming conventionTemplate: See ViewModel template
Pattern Summary:
Reference:
ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt (see handleAction method)app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt (see class declaration)Critical Gotchas:
mutableStateFlow directly inside coroutineshandleAction()@IgnoredOnParcel for sensitive data (causes security leak)@Parcelize on state classes for process death recoverySavedStateHandleAll navigation uses type-safe routes with kotlinx.serialization.
Pattern Structure:
@Serializable route data class with parameters...Args helper class for extracting from SavedStateHandleNavGraphBuilder.{screen}Destination() extension for adding screen to graphNavController.navigateTo{Screen}() extension for navigation callsTemplate: See Navigation template
Pattern Summary:
Reference: app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt (see LoginRoute and extensions)
Key Benefits:
All screens follow consistent Compose patterns.
Template: See Screen/Compose template
Key Patterns:
Reference: app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt (see LoginScreen composable)
Essential Requirements:
hiltViewModel() for dependency injectioncollectAsStateWithLifecycle() for state (not collectAsState())EventsEffect(viewModel) for one-shot eventsBitwarden* prefixed components from :ui moduleState Hoisting Rules:
remember or rememberSaveableThe data layer follows strict patterns for repositories, managers, and data sources.
Interface + Implementation Separation (ALWAYS)
Template: See Data Layer template
Pattern Summary:
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.ktThree-Layer Data Architecture:
Result<T>, never throw.Critical Rules:
...Impl patternResult<T>, repositories return domain sealed classesStateFlow for continuously observed dataKDoc on Interfaces vs. Implementations
KDoc on an interface member describes the contract — what the caller can rely on. It must not describe how the implementation fulfills that contract. Implementation details (which data source is consulted, what is cached, what is logged, which manager is delegated to, ordering of internal calls, etc.) belong on the ...Impl member — and only when the why is non-obvious from the code itself.
This applies to every interface in the codebase: repositories, managers, data sources, validators, and UI-layer interfaces alike. The rule is the same one CLAUDE.md states for comments in general — describe the contract or the non-obvious why, never the what a well-named identifier already conveys.
❌ Wrong — interface KDoc leaks the implementation:
✅ Right — interface KDoc describes the contract; implementation details (if needed at all) live on the override:
Red flags that an interface KDoc has drifted into implementation territory:
...DiskSource, ...Service, ...Manager, ...Impl)If callers genuinely depend on a behavior (e.g., "this method is safe to call before vault unlock", "result is cached for the session"), that is part of the contract and belongs on the interface. The test: would a different valid implementation be free to change this? If yes, it's implementation detail — strip it.
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 indicatorsComponent 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:
New Component Requirements:
BitwardenBitwardenThemeString Resources:
New strings belong in the :ui module: ui/src/main/res/values/strings.xml
you’ll not you\'ll, “word” not \"word\"BitwardenString resource IDsEncrypted vs Unencrypted Storage:
Template: See Security templates
Pattern Summary:
Android Keystore (Biometric Keys):
BiometricsEncryptionManagerInput Validation:
Security Checklist:
@EncryptedPreferences for credentials, keys, tokens@UnencryptedPreferences for UI state, preferences@IgnoredOnParcel for sensitive ViewModel stateViewModel Testing:
Template: See Testing templates
Pattern Summary:
Reference: app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt
Key Testing Patterns:
BaseViewModelTest for proper dispatcher managementrunTest from kotlinx.coroutines.test.test { awaitItem() } for Flow assertionscoEvery for suspend functions, every for syncFlow Testing with Turbine:
MockK Quick Reference:
All code needing current time must inject Clock for testability.
Key Requirements:
Clock via Hilt in ViewModelsClock as parameter in extension functionsclock.instant() to get current timeInstant.now() or DateTime.now() directlymockkStatic for datetime classes in testsPattern Summary:
Reference:
docs/STYLE_AND_BEST_PRACTICES.md (see Time and Clock Handling section)core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt (see provideClock function)Critical Gotchas:
Instant.now() creates hidden dependency, non-testablemockkStatic(Instant::class) is fragile, can leak between testsClock.fixed(...) provides deterministic test behaviorProject-specific style conventions enforced in code review. These supplement (not replace) docs/STYLE_AND_BEST_PRACTICES.md.
when branches with wrapped right-hand side require curly braces.
When a when branch's expression is too long to fit on the same line as the arrow and is wrapped to the next line, wrap the body in { }. A bare -> followed by an indented expression on its own line is rejected in review.
❌ Wrong — wrapped body without braces:
✅ Right — wrapped body with braces:
Single-line branches (body fits on the same line as ->) do not need braces.
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
handleAction()❌ NEVER inject ...Impl classes
❌ NEVER create navigation without @Serializable routes
❌ NEVER use raw Result<T> in repositories
❌ NEVER make state classes without @Parcelize
❌ NEVER skip SavedStateHandle persistence for ViewModels
❌ NEVER forget @IgnoredOnParcel for passwords/tokens
❌ NEVER use generic Exception catching
RemoteException, IOException)❌ NEVER call Instant.now() or DateTime.now() directly
Clock via Hilt, use clock.instant() for testability❌ NEVER put implementation details in interface KDoc
...Impl override (and only when the why is non-obvious). See section D for examples.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)