Awesome-omni-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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data-ai/implementing-android-code" ~/.claude/skills/diegosouzapw-awesome-omni-skill-implementing-android-code && rm -rf "$T"
skills/data-ai/implementing-android-code/SKILL.mdImplementing 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
- MUST be synchronoushandleAction(action: A) - Post internal actions from coroutines using
sendAction() - Save/restore state via
SavedStateHandle[KEY_STATE] - Private action handlers:
naming conventionprivate fun handle*
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:
(seeui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt
method)handleAction
(see class declaration)app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt
Critical Gotchas:
- ❌ NEVER update
directly inside coroutinesmutableStateFlow - ✅ ALWAYS post internal actions from coroutines, update state in
handleAction() - ❌ NEVER forget
for sensitive data (causes security leak)@IgnoredOnParcel - ✅ ALWAYS use
on state classes for process death recovery@Parcelize - ✅ 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:
route data class with parameters@Serializable
helper class for extracting from...ArgsSavedStateHandle
extension for adding screen to graphNavGraphBuilder.{screen}Destination()
extension for navigation callsNavController.navigateTo{Screen}()
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
for dependency injectionhiltViewModel() - ✅ Use
for state (notcollectAsStateWithLifecycle()
)collectAsState() - ✅ Use
for one-shot eventsEventsEffect(viewModel) - ✅ Use
for stable callbacks to prevent recompositionremember(viewModel) { } - ✅ Use
prefixed components fromBitwarden*
module:ui
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
orrememberrememberSaveable
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
, never throw.Result<T> - 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 +
pattern...Impl - ✅ ALWAYS inject interfaces, never implementations
- ✅ Data sources return
, repositories return domain sealed classesResult<T> - ✅ Use
for continuously observed dataStateFlow
E. UI Components
Use Existing Components First:
The
:ui module provides reusable Bitwarden* prefixed components. Search before creating new ones.
Common Components:
- Primary action buttonsBitwardenFilledButton
- Secondary action buttonsBitwardenOutlinedButton
- Text input fieldsBitwardenTextField
- Password input with show/hideBitwardenPasswordField
- Toggle switchesBitwardenSwitch
- Toolbar/app barBitwardenTopAppBar
- Screen container with scaffoldBitwardenScaffold
- Simple dialogsBitwardenBasicDialog
- Loading indicatorsBitwardenLoadingDialog
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:
notyou’ll
,you\'ll
not“word”\"word\" - Reference strings via generated
resource IDsBitwardenString - 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
for credentials, keys, tokens@EncryptedPreferences - ✅ Use
for UI state, preferences@UnencryptedPreferences - ✅ Use
for sensitive ViewModel state@IgnoredOnParcel - ❌ 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
for proper dispatcher managementBaseViewModelTest - ✅ Use
fromrunTestkotlinx.coroutines.test - ✅ Use Turbine's
for Flow assertions.test { awaitItem() } - ✅ Use MockK:
for suspend functions,coEvery
for syncevery - ✅ 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
via Hilt in ViewModelsClock - ✅ Pass
as parameter in extension functionsClock - ✅ Use
to get current timeclock.instant() - ❌ Never call
orInstant.now()
directlyDateTime.now() - ❌ Never use
for datetime classes in testsmockkStatic
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:
(see Time and Clock Handling section)docs/STYLE_AND_BEST_PRACTICES.md
(seecore/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt
function)provideClock
Critical Gotchas:
- ❌
creates hidden dependency, non-testableInstant.now() - ❌
is fragile, can leak between testsmockkStatic(Instant::class) - ✅
provides deterministic test behaviorClock.fixed(...)
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
classes...Impl
- Only inject interfaces via Hilt
❌ NEVER create navigation without
routes@Serializable
- No string-based navigation, always type-safe
❌ NEVER use raw
in repositoriesResult<T>
- Use domain-specific sealed classes for better error handling
❌ NEVER make state classes without @Parcelize
- All ViewModel state must survive process death
❌ NEVER skip
persistence for ViewModelsSavedStateHandle
- Users lose form progress on process death
❌ NEVER forget
for passwords/tokens@IgnoredOnParcel
- Causes security vulnerability (sensitive data in parcel)
❌ NEVER use generic
catchingException
- Catch specific exceptions only (
,RemoteException
)IOException
❌ NEVER call
or Instant.now()
directlyDateTime.now()
- Inject
via Hilt, useClock
for testabilityclock.instant()
Quick Reference
For build, test, and codebase discovery commands, use the
skill.build-test-verify
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)