skills/bitwarden/android/implementing-android-code

implementing-android-code

SKILL.md

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 (see handleAction method)
  • app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt (see class declaration)

Critical Gotchas:

  • NEVER update mutableStateFlow directly inside coroutines
  • ALWAYS post internal actions from coroutines, update state in handleAction()
  • NEVER forget @IgnoredOnParcel for sensitive data (causes security leak)
  • ALWAYS use @Parcelize on 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:

  1. @Serializable route data class with parameters
  2. ...Args helper class for extracting from SavedStateHandle
  3. NavGraphBuilder.{screen}Destination() extension for adding screen to graph
  4. NavController.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 = { 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 (not collectAsState())
  • ✅ Use EventsEffect(viewModel) for one-shot events
  • ✅ Use Bitwarden* prefixed components from :ui module

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 remember or rememberSaveable

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.kt
  • app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt

Three-Layer Data Architecture:

  1. Data Sources - Raw data access (network, disk, SDK). Return Result<T>, never throw.
  2. Managers - Single responsibility business logic. Wrap OS/external services.
  3. Repositories - Aggregate sources/managers. Return domain-specific sealed classes.

Critical Rules:

  • NEVER throw exceptions in data layer
  • ALWAYS use interface + ...Impl pattern
  • ALWAYS inject interfaces, never implementations
  • ✅ Data sources return Result<T>, repositories return domain sealed classes
  • ✅ Use StateFlow for 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 buttons
  • BitwardenOutlinedButton - Secondary action buttons
  • BitwardenTextField - Text input fields
  • BitwardenPasswordField - Password input with show/hide
  • BitwardenSwitch - Toggle switches
  • BitwardenTopAppBar - Toolbar/app bar
  • BitwardenScaffold - Screen container with scaffold
  • BitwardenBasicDialog - Simple dialogs
  • BitwardenLoadingDialog - 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’ll not you\'ll, “word” not \"word\"
  • Reference strings via generated BitwardenString resource 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 @EncryptedPreferences for credentials, keys, tokens
  • ✅ Use @UnencryptedPreferences for UI state, preferences
  • ✅ Use @IgnoredOnParcel for 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 BaseViewModelTest for proper dispatcher management
  • ✅ Use runTest from kotlinx.coroutines.test
  • ✅ Use Turbine's .test { awaitItem() } for Flow assertions
  • ✅ Use MockK: coEvery for suspend functions, every for 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 Clock via Hilt in ViewModels
  • ✅ Pass Clock as parameter in extension functions
  • ✅ Use clock.instant() to get current time
  • ❌ Never call Instant.now() or DateTime.now() directly
  • ❌ Never use mockkStatic for 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 (see provideClock function)

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 Clock via Hilt, use clock.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)

Weekly Installs
17
GitHub Stars
8.6K
First Seen
Feb 27, 2026
Installed on
opencode17
github-copilot17
codex17
kimi-cli17
gemini-cli17
cursor17