mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 05:20:24 -05:00
Compare commits
1 Commits
remove-ret
...
update-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619c6efcf4 |
BIN
.github/images/android-dark.png
vendored
Normal file
BIN
.github/images/android-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 291 KiB |
BIN
.github/images/android-light.png
vendored
Normal file
BIN
.github/images/android-light.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
248
README.md
248
README.md
@@ -1,232 +1,40 @@
|
||||
# Bitwarden Android
|
||||
|
||||
## Contents
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/images/android-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset=".github/images/android-light.png">
|
||||
<img alt="Bitwarden Android apps screenshots." src=".github/images/android-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/bitwarden/android/actions/workflows/build.yml?query=branch:main" target="_blank"><img src="https://github.com/bitwarden/android/actions/workflows/build.yml/badge.svg?branch=main" alt="GitHub Workflow Android CI build on main" /></a>
|
||||
<a href="https://github.com/bitwarden/android/actions/workflows/test.yml?query=branch:main" target="_blank"><img src="https://github.com/bitwarden/android/actions/workflows/test.yml/badge.svg?branch=main" alt="GitHub Workflow Android Password Manager Test on main" /></a>
|
||||
<a href="https://gitter.im/bitwarden/Lobby" target="_blank"><img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" /></a>
|
||||
</p>
|
||||
|
||||
- [Compatibility](#compatibility)
|
||||
- [Setup](#setup)
|
||||
- [Dependencies](#dependencies)
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
# Bitwarden Android Password Manager & Authenticator Apps
|
||||
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 35
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
Please refer to the [Contributing Documentation](https://contributing.bitwarden.com/) for setup instructions, recommended tooling, code style tips, and lots of other great information to get you started. Relevant Links:
|
||||
|
||||
## Setup
|
||||
- [Getting Started](https://contributing.bitwarden.com/getting-started/mobile/android/)
|
||||
- [Code Style](https://contributing.bitwarden.com/contributing/code-style/android-kotlin)
|
||||
- [Architecture](https://contributing.bitwarden.com/architecture/mobile-clients/android/)
|
||||
- [Push Notifications Deep Dive](https://contributing.bitwarden.com/architecture/deep-dives/push-notifications/mobile)
|
||||
|
||||
## Related projects:
|
||||
|
||||
1. Clone the repository:
|
||||
- [bitwarden/server](https://github.com/bitwarden/server): The core infrastructure backend (API, database, Docker, etc).
|
||||
- [bitwarden/clients](https://github.com/bitwarden/clients): Non-mobile Bitwarden Clients Applications.
|
||||
- [bitwarden/directory-connector](https://github.com/bitwarden/directory-connector): A tool for syncing a directory (AD, LDAP, Azure, G Suite, Okta) to an organization.
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/bitwarden/android
|
||||
```
|
||||
# We're Hiring!
|
||||
|
||||
2. Create a `user.properties` file in the root directory of the project and add the following properties:
|
||||
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are [currently open](https://bitwarden.com/careers/#open-positions) as well as what it's like to work at Bitwarden.
|
||||
|
||||
- `gitHubToken`: A "classic" Github Personal Access Token (PAT) with the `read:packages` scope (ex: `gitHubToken=gph_xx...xx`). These can be generated by going to the [Github tokens page](https://github.com/settings/tokens). See [the Github Packages user documentation concerning authentication](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details.
|
||||
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing.bitwarden.com/getting-started/sdk/#linking-the-sdk-to-clients) for more details.
|
||||
# Contribute
|
||||
|
||||
3. Setup the code style formatter:
|
||||
Code contributions are welcome! Please commit any pull requests against the `main` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||
|
||||
All code must follow the guidelines described in the [Code Style Guidelines document](docs/STYLE_AND_BEST_PRACTICES.md). To aid in adhering to these rules, all contributors should apply `docs/bitwarden-style.xml` as their code style scheme. In IntelliJ / Android Studio:
|
||||
|
||||
- Navigate to `Preferences > Editor > Code Style`.
|
||||
- Hit the `Manage` button next to `Scheme`.
|
||||
- Select `Import`.
|
||||
- Find the `bitwarden-style.xml` file in the project's `docs/` directory.
|
||||
- Import "from" `BitwardenStyle` "to" `BitwardenStyle`.
|
||||
- Hit `Apply` and `OK` to save the changes and exit Preferences.
|
||||
|
||||
Note that in some cases you may need to restart Android Studio for the changes to take effect.
|
||||
|
||||
All code should be formatted before submitting a pull request. This can be done manually but it can also be helpful to create a macro with a custom keyboard binding to auto-format when saving. In Android Studio on OS X:
|
||||
|
||||
- Select `Edit > Macros > Start Macro Recording`
|
||||
- Select `Code > Optimize Imports`
|
||||
- Select `Code > Reformat Code`
|
||||
- Select `File > Save All`
|
||||
- Select `Edit > Macros > Stop Macro Recording`
|
||||
|
||||
This can then be mapped to a set of keys by navigating to `Android Studio > Preferences` and editing the macro under `Keymap` (ex : shift + command + s).
|
||||
|
||||
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Application Dependencies
|
||||
|
||||
The following is a list of all third-party dependencies included as part of the application beyond the standard Android SDK.
|
||||
|
||||
- **AndroidX Appcompat**
|
||||
- https://developer.android.com/jetpack/androidx/releases/appcompat
|
||||
- Purpose: Allows access to new APIs on older API versions.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Autofill**
|
||||
- https://developer.android.com/jetpack/androidx/releases/autofill
|
||||
- Purpose: Allows access to tools for building inline autofill UI.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Biometrics**
|
||||
- https://developer.android.com/jetpack/androidx/releases/biometric
|
||||
- Purpose: Authenticate with biometrics or device credentials.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Browser**
|
||||
- https://developer.android.com/jetpack/androidx/releases/browser
|
||||
- Purpose: Displays webpages with the user's default browser.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX CameraX Camera2**
|
||||
- https://developer.android.com/jetpack/androidx/releases/camera
|
||||
- Purpose: Display and capture images for barcode scanning.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Compose**
|
||||
- https://developer.android.com/jetpack/androidx/releases/compose
|
||||
- Purpose: A Kotlin-based declarative UI framework.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Core SplashScreen**
|
||||
- https://developer.android.com/jetpack/androidx/releases/core
|
||||
- Purpose: Backwards compatible SplashScreen API implementation.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Credentials**
|
||||
- https://developer.android.com/jetpack/androidx/releases/credentials
|
||||
- Purpose: Unified access to user's credentials.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Lifecycle**
|
||||
- https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
- Purpose: Lifecycle aware components and tooling.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Room**
|
||||
- https://developer.android.com/jetpack/androidx/releases/room
|
||||
- Purpose: A convenient SQLite-based persistence layer for Android.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Security**
|
||||
- https://developer.android.com/jetpack/androidx/releases/security
|
||||
- Purpose: Safely manage keys and encrypt files and sharedpreferences.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX WorkManager**
|
||||
- https://developer.android.com/jetpack/androidx/releases/work
|
||||
- Purpose: The WorkManager is used to schedule deferrable, asynchronous tasks that must be run reliably.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Dagger Hilt**
|
||||
- https://github.com/google/dagger
|
||||
- Purpose: Dependency injection framework.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Firebase Cloud Messaging**
|
||||
- https://github.com/firebase/firebase-android-sdk
|
||||
- Purpose: Allows for push notification support. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Firebase Crashlytics**
|
||||
- https://github.com/firebase/firebase-android-sdk
|
||||
- Purpose: SDK for crash and non-fatal error reporting. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Google Play Reviews**
|
||||
- https://developer.android.com/reference/com/google/android/play/core/release-notes
|
||||
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Glide**
|
||||
- https://github.com/bumptech/glide
|
||||
- Purpose: Image loading and caching.
|
||||
- License: BSD, part MIT and Apache 2.0
|
||||
|
||||
- **kotlinx.collections.immutable**
|
||||
- https://github.com/Kotlin/kotlinx.collections.immutable
|
||||
- Purpose: Immutable collection interfaces and implementation prototypes for Kotlin.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **kotlinx.coroutines**
|
||||
- https://github.com/Kotlin/kotlinx.coroutines
|
||||
- Purpose: Kotlin coroutines library for asynchronous and reactive code.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **kotlinx.serialization**
|
||||
- https://github.com/Kotlin/kotlinx.serialization/
|
||||
- Purpose: JSON serialization library for Kotlin.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **kotlinx.serialization converter**
|
||||
- https://github.com/square/retrofit/tree/trunk/retrofit-converters/kotlinx-serialization
|
||||
- Purpose: Converter for Retrofit 2 and kotlinx.serialization.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **OkHttp 3**
|
||||
- https://github.com/square/okhttp
|
||||
- Purpose: An HTTP client used by the library to intercept and log traffic.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Retrofit 2**
|
||||
- https://github.com/square/retrofit
|
||||
- Purpose: A networking layer interface.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Timber**
|
||||
- https://github.com/JakeWharton/timber
|
||||
- Purpose: Extensible logging library for Android.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **zxcvbn4j**
|
||||
- https://github.com/nulab/zxcvbn4j
|
||||
- Purpose: Password strength estimation.
|
||||
- License: MIT
|
||||
|
||||
- **ZXing**
|
||||
- https://github.com/zxing/zxing
|
||||
- Purpose: Barcode scanning and generation.
|
||||
- License: Apache 2.0
|
||||
|
||||
### Development Environment Dependencies
|
||||
|
||||
The following is a list of additional third-party dependencies used as part of the local development environment. This includes test-related artifacts as well as tools related to code quality and linting. These are not present in the final packaged application.
|
||||
|
||||
- **detekt**
|
||||
- https://github.com/detekt/detekt
|
||||
- Purpose: A static code analysis tool for the Kotlin programming language.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **JUnit 5**
|
||||
- https://github.com/junit-team/junit5
|
||||
- Purpose: Unit Testing framework for testing application code.
|
||||
- License: Eclipse Public License 2.0
|
||||
|
||||
- **MockK**
|
||||
- https://github.com/mockk/mockk
|
||||
- Purpose: Kotlin-friendly mocking library.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Robolectric**
|
||||
- https://github.com/robolectric/robolectric
|
||||
- Purpose: A unit testing framework for code directly depending on the Android framework.
|
||||
- License: MIT
|
||||
|
||||
- **Turbine**
|
||||
- https://github.com/cashapp/turbine
|
||||
- Purpose: A small testing library for kotlinx.coroutine's Flow.
|
||||
- License: Apache 2.0
|
||||
|
||||
### CI/CD Dependencies
|
||||
|
||||
The following is a list of additional third-party dependencies used as part of the CI/CD workflows. These are not present in the final packaged application.
|
||||
|
||||
- **Fastlane**
|
||||
- https://fastlane.tools/
|
||||
- Purpose: Automates building, signing, and distributing applications.
|
||||
- License: MIT
|
||||
|
||||
- **Kover**
|
||||
- https://github.com/Kotlin/kotlinx-kover
|
||||
- Purpose: Kotlin code coverage toolset.
|
||||
- License: Apache 2.0
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||
@@ -1,439 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Data Layer](#data-layer)
|
||||
- [Data Sources](#data-sources)
|
||||
- [Managers](#managers)
|
||||
- [Repositories](#repositories)
|
||||
- [Note on Dependency Injection](#note-on-dependency-injection)
|
||||
- [UI Layer](#ui-layer)
|
||||
- [ViewModels / MVVM](#viewmodels--mvvm)
|
||||
- [Example](#example)
|
||||
- [Screens / Compose](#screens--compose)
|
||||
- [State Hoisting](#state-hoisting)
|
||||
- [Example](#example-1)
|
||||
- [Navigation](#navigation)
|
||||
- [State-based Navigation](#state-based-navigation)
|
||||
- [Event-based Navigation](#event-based-navigation)
|
||||
- [Navigation Implementation](#navigation-implementation)
|
||||
- [Example](#example-2)
|
||||
|
||||
## Overview
|
||||
|
||||
The app is broadly divided into the **data layer** and the **UI layer** and this is reflected in the two top-level packages of the app, `data` and `ui`. Each of these packages is then subdivided into the following sub-packages:
|
||||
|
||||
- `auth`
|
||||
- `autofill`
|
||||
- `platform`
|
||||
- `tools`
|
||||
- `vault`
|
||||
|
||||
Note that these packages are currently aligned with the [CODEOWNERS](../.github/CODEOWNERS) files for the project; no additional direct sub-packages of `ui` or `data` should be added. While this top-level structure is deliberately inflexible, the package structure within each `auth`, `autofill`, etc. are not specifically prescribed.
|
||||
|
||||
The responsibilities of the data layer are to manage the storage and retrieval of data from low-level sources (such as from the network, persistence, or Bitwarden SDK) and to expose them in a more ready-to-consume manner by the UI layer via "repository" and "manager" classes. The UI layer is then responsible for any final processing of this data for display in the UI as well for receiving events from the UI, updating the tracked state accordingly.
|
||||
|
||||
## Data Layer
|
||||
|
||||
The data layer is where all the UI-independent data is stored and retrieved. It consists of both raw data sources as well as higher-level "repository" and "manager" classes.
|
||||
|
||||
Note that any functions exposed by a data layer class that must perform asynchronous work do so by exposing **suspending functions** that may run inside [coroutines](https://kotlinlang.org/docs/coroutines-guide.html) while any streaming sources of data are handled by exposing [Flows](https://kotlinlang.org/docs/flow.html).
|
||||
|
||||
### Data Sources
|
||||
|
||||
The lowest level of the data layer are the "data source" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).
|
||||
|
||||
Note that these data sources are constructed in a manner that adheres to a very important principle of the app: **that function calls should not throw exceptions** (see the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--kotlin) for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests or Bitwarden SDK calls should return a [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like Retrofit and the Bitwarden SDK tend to throw exceptions to indicate errors instead.
|
||||
|
||||
### Managers
|
||||
|
||||
Manager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt).
|
||||
|
||||
### Repositories
|
||||
|
||||
Repository classes represent the outermost level of the data layer. They can take data sources, managers, and in rare cases even other repositories as dependencies and are meant to be exposed directly to the UI layer. They synthesize data from multiple sources and combine various asynchronous requests as necessary in order to expose data to the UI layer in a more appropriate form. These classes tend to have broad responsibilities that generally cover a major domain of the app, such as authentication ([AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt)) or vault access ([VaultRepository](../app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt)).
|
||||
|
||||
Repository classes also feature functions that do not throw exceptions, but unlike the lower levels of the data layer the `Result` type should be avoided in favor of custom sealed classes that represent the various success/error cases in a more processed form. Returning raw `Throwable`/`Exception` instances as part of "error" states should be avoided when possible.
|
||||
|
||||
In some cases a source of data may be continuously observed and in these cases a repository may choose to expose a [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) that emits data updates using the [DataState](../app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/DataState.kt) wrapper.
|
||||
|
||||
### Note on Dependency Injection
|
||||
|
||||
Nearly all classes in the data layer consist of interfaces representing exposed behavior and a corresponding `...Impl` class implementing that interface (ex: [AuthDiskSource](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt) / [AuthDiskSourceImpl](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt)). All `...Impl` classes are intended to be manually constructed while their associated interfaces are provided for dependency injection via a [Hilt Module](https://dagger.dev/hilt/modules.html) (ex: [PlatformNetworkModule](../app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt)). This prevents the `...Impl` classes from being injected by accident and allows the interfaces to be easily mocked/faked in tests.
|
||||
|
||||
## UI Layer
|
||||
|
||||
The UI layer adheres to the concept of [unidirectional data flow](https://developer.android.com/develop/ui/compose/architecture#udf) and makes use of the MVVM design pattern. Both concepts are in line what Google currently recommends as the best approach for building the UI-layer of a modern Android application and this allows us to make use of all the available tooling Google provides as part of the [Jetpack suite of libraries](https://developer.android.com/jetpack). The MVVM implementation is built around the Android [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) class and the UI itself is constructed using the [Jetpack Compose](https://developer.android.com/develop/ui/compose), a declarative UI framework specifically built around the unidirectional data flow approach.
|
||||
|
||||
Each screen in the app is associated with at least the following three classes/files:
|
||||
|
||||
- A `...ViewModel` class responsible for managing the data and state for the screen.
|
||||
- A `...Screen` class that contains the Compose implementation of the UI.
|
||||
- A `...Navigation` file containing the details for how to add the screen to the overall navigation graph and how navigate to it within the graph.
|
||||
|
||||
### ViewModels / MVVM
|
||||
|
||||
The app's approach to MVVM is based around the handling of "state", "actions", and "events" and is encoded in the [BaseViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/BaseViewModel.kt) class.
|
||||
|
||||
- **State:** The "state" represents the complete internal and external **total state** of the ViewModel (VM). Any and all information needed to configure the UI that is associated with the VM should be included in this state and is exposed via the `BaseViewModel.stateFlow` property as `StateFlow<S>` (where `S` represents the unique type of state associated with the VM). This state is typically a combination of state from the data layer (such as account information) and state from the UI layer (such as data entered into a text field).
|
||||
|
||||
There should be no additional `StateFlow` exposed from a VM that would represent some other kind of state; all state should be represented by `S`. Additionally, any internal state not directly needed by the UI but which influences the behavior of the VM should be included as well in order to keep all state managed by the VM in a single place.
|
||||
|
||||
- **Actions:** The "actions" represent interactions with the VM in some way that could potentially cause an update to that total state. These can be external actions coming from the user's interaction with the UI, like click events, or internal actions coming from some asynchronous process internal to the VM itself, like the result of some suspending functions. Actions are sent by interacting directly with `BaseViewModel.actionChannel` or by using the `BaseViewModel.sendAction` and `BaseViewModel.trySendAction` helpers. All actions are then processed synchronously in a queue in the `handleAction` function.
|
||||
|
||||
It is worth emphasizing that state should never be updated inside a coroutine in a VM; all asynchronous work that results in a state update should do so by posting an internal action. This ensures that the only place that state changes can occur is synchronously inside the `handleAction` function. This makes the process of finding and reasoning about state changes easier and simplifies debugging.
|
||||
|
||||
- **Events:** The "events" represent discrete, one-shot side-effects and are typically associated with navigation events triggered by some user action. They are sent internally using `BaseViewModel.sendEvent` and may be consumed by the UI layer via the `BaseViewModel.eventFlow` property. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should typically be used to simplify the consumption of these events.
|
||||
|
||||
VMs are typically injected using [Dagger Hilt](https://developer.android.com/training/dependency-injection/hilt-android) by annotating them with `@HiltViewModel`. This allows them to be constructed, retrieved, and cached in the Compose UI layer by calling `hiltViewModel()` with the appropriate type. Dependencies passed into the VM constructor will typically be singletons of the graph, such as [AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt). In cases where the VM needs to initialized with some specific data (like an ID) that is sent from the previous screen, this data should be retrieved by injecting the [SavedStateHandle](https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate) and pulling out data using a type-safe wrapper (ex: [LoginArgs](../app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt)).
|
||||
|
||||
#### Example
|
||||
|
||||
The following is an example that demonstrates many of the above principles and best practices when using `BaseViewModel` to implement a VM. It has the following features:
|
||||
|
||||
- It is injected with a repository (`ExampleRepository`) that provides streaming `ExampleData` and a `SavedStateHandle` in order to pull initial data using a `ExampleArgs` wrapper class.
|
||||
- It has state that manages several properties of interest to the UI: `exampleData`, `isToggledEnabled`, and `dialogState`.
|
||||
- It receives external actions from the UI (`ContinueButtonClick` and `ToggleValueUpdate`) as well as internal actions launch from inside different coroutines coroutines (`Internal.ExampleDataReceive`, `Internal.ToggleValueSync`). These actions result in state updates or the emission of an event (`NavigateToNextScreen`).
|
||||
- It saves the current state to the `SavedStateHandle` in order to restore it later after process death and restoration. This ensures the user's actions and associated state will not be lost if the app goes into the background and is temporarily killed by the OS to conserve memory.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Show example</summary>
|
||||
|
||||
```kotlin
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
@HiltViewModel
|
||||
class ExampleViewModel @Inject constructor(
|
||||
private val exampleRepository: ExampleRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(
|
||||
// If previously saved state data is present in the SavedStateHandle, use that as the initial
|
||||
// state, otherwise initialize from repository and navigation data.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: ExampleState(
|
||||
exampleData = exampleRepository.exampleDataStateFlow.value,
|
||||
isToggleEnabled = ExampleArgs(savedStateHandle).isToggleEnabledInitialValue,
|
||||
dialogState = null,
|
||||
)
|
||||
) {
|
||||
init {
|
||||
// As the state updates, write to saved state handle for retrieval after process death and
|
||||
// restoration.
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
exampleRepository
|
||||
.exampleDataStateFlow
|
||||
// Asynchronously received data is converted to an internal action and sent in order to
|
||||
// be handled by `handleAction`.
|
||||
.map { ExampleAction.Internal.ExampleDataReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: ExampleAction) {
|
||||
when (action) {
|
||||
is ExampleAction.ContinueButtonClick -> handleContinueButtonClick()
|
||||
is ExampleAction.ToggleValueUpdate -> handleToggleValueUpdate(action)
|
||||
is ExampleAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueButtonClick() {
|
||||
// Update the state to show a dialog
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ExampleState.DialogState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
// Run a suspending call in a coroutine to fetch data and post the result back as an
|
||||
// internal action for further processing so that all state changes and event emissions
|
||||
// happen synchronously in `handleAction`.
|
||||
viewModelScope.launch {
|
||||
val completionData = exampleRepository
|
||||
.fetchCompletionData(isToggleEnabled = state.isToggleEnabled)
|
||||
|
||||
sendAction(
|
||||
ExampleAction.Internal.CompletionDataReceive(
|
||||
completionData = completionData,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleToggleValueUpdate(action: ExampleAction.ToggleValueUpdate) {
|
||||
// Update the state
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isToggleEnabled = action.isToggleEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: ExampleAction.Internal) {
|
||||
when (action) {
|
||||
is ExampleAction.Internal.CompletionDataReceive -> handleCompletionDataReceive(action)
|
||||
is ExampleAction.Internal.ExampleDataReceive -> handleExampleDataReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCompletionDataReceive(action: ExampleAction.Internal.CompletionDataReceive) {
|
||||
// Update the state to clear the dialog
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
|
||||
// Send event with data from the action to navigate
|
||||
sendEvent(
|
||||
ExampleEvent.NavigateToNextScreen(
|
||||
completionData = action.completionData,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleExampleDataReceive(action: ExampleAction.Internal.ExampleDataReceive) {
|
||||
// Update the state
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
exampleData = action.exampleData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ExampleState(
|
||||
val exampleData: String,
|
||||
val isToggleEnabled: Boolean,
|
||||
val dialogState: DialogState?,
|
||||
) : Parcelable {
|
||||
|
||||
sealed class DialogState : Parcelable {
|
||||
@Parcelize
|
||||
data object Loading : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ExampleEvent {
|
||||
data class NavigateToNextScreen(
|
||||
val completionData: CompletionData,
|
||||
) : ExampleEvent()
|
||||
}
|
||||
|
||||
sealed class ExampleAction {
|
||||
data object ContinueButtonClick : ExampleAction()
|
||||
|
||||
data class ToggleValueUpdate(
|
||||
val isToggleEnabled: Boolean,
|
||||
) : ExampleAction()
|
||||
|
||||
sealed class Internal : ExampleAction() {
|
||||
data class CompletionDataReceive(
|
||||
val completionData: CompletionData,
|
||||
) : Internal()
|
||||
|
||||
data class ExampleDataReceive(
|
||||
val exampleData: String,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
### Screens / Compose
|
||||
|
||||
Each unique screen destination is represented by a composable `...Screen` function annotated with `@Composable`. The responsibilities of this layer include:
|
||||
|
||||
- Receiving "state" data from the associated `ViewModel` and rendering the UI accordingly.
|
||||
- Receiving "events" from the associated `ViewModel` and taking the corresponding action, which is typically to trigger a passed-in callback function for navigation purposes. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should be used to simplify this process.
|
||||
- Sending "actions" to the `ViewModel` indicating user interactions or UI-layer events as necessary.
|
||||
- Interacting with purely UI-layer "manager" classes that are not appropriate or possible to be injected into the `ViewModel` (such as those concerning permissions or camera access).
|
||||
|
||||
In order to both provide consistency to the app and to simplify the development of new screens, an extensive system of reusable composable components has been developed and may be found in the [components package](../app/src/main/java/com/x8bit/bitwarden/ui/platform/components). These tend to bear the prefix `Bitwarden...` to easily distinguish them from similar OS-level components. If there is any new unique UI component that is added, it should always be considered if it should be made a shareable component.
|
||||
|
||||
Refer to the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--jetpack-compose) for additional information on best practices when using Compose.
|
||||
|
||||
#### State-hoisting
|
||||
|
||||
Jetpack Compose is built around the idea that the state required to render any given composable function can be "hoisted" to higher levels that may need access to that state. This means that the responsibility for keeping track of the current state of a component may not typically reside with the component itself. It is important, therefore, to understand where the best place is to manage any given piece of UI state.
|
||||
|
||||
This is discussed in detail in [Google's state-hoisting documentation](https://developer.android.com/develop/ui/compose/state-hoisting) and can be summarized roughly as follows: **state should be hoisted as high as is necessary to perform any relevant logic and no higher.** This is a pattern that is followed in the Bitwarden app and tends to lead to the following:
|
||||
|
||||
- Any state that will eventually need to be used by the VM should be hoisted to the VM. For example, any text input, toggle values, etc. that may be updated by user interactions should be completely controlled by the VM. These state changes are communicated via the "actions" that the Compose layer sends, which trigger corresponding updates the VM's total "state".
|
||||
- Any UI state that will not be used by logic inside the VM should remain in the UI layer. For example, visibility toggle states for password inputs should typically remain out of the VM.
|
||||
|
||||
These rules can lead to some notable differences in how certain dialogs are handled. For example, loading dialogs are controlled by events that occur in the VM, such as when a network request is started and when it completes. This requires the VM to be in charge of managing the visibility state of the dialog. However, some dialogs appear as the result of clicking on an item in the UI in order to simply display information or to ask for the user's confirmation before some action is taken. Because there is no logic in the VM that depends on the visibility of these particular dialogs, their visibility state can be controlled by the UI. Note, however, that any user interaction that results in a navigation to a new screen should always be routed to the VM first, as the VM should always be in charge of triggering changes at the per-screen level.
|
||||
|
||||
#### Example
|
||||
|
||||
The following shows off the basic structure of a composable `...Screen` implementation. Note that:
|
||||
|
||||
- The VM is "injected" using the `hiltViewModel()` helper function. This will correctly create, cache, and scope the VM in production code while making it easier to pass in mock/fake implementations in test code.
|
||||
- An `onNavigateToNextScreen` function is also passed in, which can be called to trigger a navigation via a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) in the outer layers of the navigation graph. Passing in a function rather than the `NavController` itself decouples the screen code from the navigation framework and greatly simplifies the testing of screens.
|
||||
- The VM "state" is consumed using `viewModel.stateFlow.collectAsStateWithLifecycle()`. This will cause the composable to "recompose" and update whenever updates are pushed to the `viewModel.stateFlow`.
|
||||
- The VM "events" are consumed using an [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) and demonstrate how the `onNavigateToNextScreen` may be triggered.
|
||||
- The current state of the text, switch, and button are hoisted to the VM and pulled out from the `state`. User interactions with the switch and button result in "actions" being sent to the VM using `viewModel.trySendAction`.
|
||||
- Reusable components (`BitwardenLoadingDialog`, `BitwardenSwitch`, and `BitwardenFilledButton`) are used where possible in order to build the screen using the correct theming and reduce code duplication. When this is not possible (such as when rending the `Text` composable) all colors and styles are pulled from the BitwardenTheme object.
|
||||
|
||||
<details>
|
||||
<summary>Show example</summary>
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ExampleScreen(
|
||||
onNavigateToNextScreen: (CompletionData) -> Unit,
|
||||
viewModel: ExampleViewModel = hiltViewModel(),
|
||||
) {
|
||||
// Collect state
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
// Handle events
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is ExampleEvent.NavigateToNextScreen -> {
|
||||
onNavigateToNextScreen(event.completionData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show state-based dialogs if necessary
|
||||
when (state.dialogState) {
|
||||
ExampleState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = R.string.loading.asText(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
// Render the remaining state
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
text = state.exampleData,
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.textColors.primary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.wrapContentHeight(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.toggle_label),
|
||||
isChecked = state.isToggleEnabled,
|
||||
// Use remember(viewModel) to ensure the unstable lambda doesn't trigger unnecessary
|
||||
// recompositions.
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.ToggleValueUpdate(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.ContinueButtonClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Navigation
|
||||
|
||||
Navigation in the app is achieved via the [Compose Navigation](https://developer.android.com/develop/ui/compose/navigation) component. This involves specifying "destinations" within a [NavHost](https://developer.android.com/reference/androidx/navigation/NavHost) and using a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) to trigger actual navigation events. The app uses a mix of state-based navigation at the highest level (in order to determine the overall app flow the user is in) and event-based navigation (to handle navigation within a flow based on user interactions).
|
||||
|
||||
#### State-based Navigation
|
||||
|
||||
State-based navigation is handled by the [RootNavViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt) and [RootNavScreen](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt). The current navigation state is determined within the `RootNavViewModel` by monitoring the overall [UserState](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt) along with any [SpecialCircumstance](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt). The `UserState` encodes whether there are any user accounts, whether they are logged in, etc., while the `SpecialCircumstance` describes a particular circumstance under which the app may have been launched (such as being opened in order to manually select an autofill value). The `RootNavState` calculated by the `RootNavViewModel` is then used by the `RootNavScreen` in order to trigger a navigation only if the overall state has changed.
|
||||
|
||||
The benefit of state-based navigation at this level is that it allows for a variety of state changes that may happen deep within the data layer to automatically trigger navigation to the correct top-level flow without the rest of the UI needing to be concerned with these details. For example, when logging out the current user, the data layer can decide the new state and the `RootNavViewModel` can decide where to go next, rather than having the screen that triggered the logout action choose the next navigation state (which would then need to be redundantly handled in multiple places in the app).
|
||||
|
||||
State-based navigation should be limited to specific cases that affect the overall flow of the app and are driven by data emitted from the data layer. All other navigation should use event-based navigation.
|
||||
|
||||
#### Event-based Navigation
|
||||
|
||||
Event-based navigation is ultimately the result of some "action" sent to a VM, whether that comes directly from a user-interaction with the current screen or from some internal action of the VM sent as the result of some asynchronous call. The VM will send an "event" to the Compose layer, which will trigger a callback that will eventually make a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController).
|
||||
|
||||
#### Navigation Implementation
|
||||
|
||||
At its most basic level, Compose Navigation relies on constructing String-based URIs with a root path to specify a destination and both path and query parameters to handle the passing of required and optional parameters. This process can typically be very error prone. The Bitwarden app borrows a pattern best exemplified by the [Now In Android sample project](https://github.com/android/nowinandroid) in order to make this process more type-safe. This consists of:
|
||||
|
||||
- A type-safe `...Destination` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any single screen destination or a type-safe `...Graph` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any nested graph destination.
|
||||
- A type-safe `navigateTo...` extension function on [NavController](https://developer.android.com/reference/androidx/navigation/NavController) for any destination that is not simply the root of a graph.
|
||||
- A type-safe `...Args` class that pulls data from a `SavedStateHandle` for consumption by a VM.
|
||||
|
||||
These are all then grouped into a single `...Navigation.kt` file where all of the raw String-based details can be hidden and only the exposed type-safe functions must be used by the rest of the app.
|
||||
|
||||
#### Example
|
||||
|
||||
The following example demonstrates a sample `ExampleNavigation.kt` file with:
|
||||
|
||||
- An `ExampleArgs` class that wraps `SavedStateHandle` and allows for the `isToggleEnabledInitialValue` to be extracted in a type-safe way.
|
||||
- A `NavGraphBuilder.exampleDestination` extension function that can be used to safely add the `ExampleScreen` to the navigation graph.
|
||||
- A `NavController.navigateToExample` extension function that can be called to navigate to the `ExampleScreen` in a type-safe way.
|
||||
|
||||
Note in particular how consumers of the above **do not need to know the details of the actual route or path parameter**.
|
||||
|
||||
<details>
|
||||
<summary>Show example</summary>
|
||||
|
||||
```kotlin
|
||||
private const val IS_TOGGLE_ENABLED: String = "is_toggle_enabled"
|
||||
private const val EXAMPLE_ROUTE_PREFIX = "example"
|
||||
private const val EXAMPLE_ROUTE = "$EXAMPLE_ROUTE_PREFIX/{$IS_TOGGLE_ENABLED}"
|
||||
|
||||
data class ExampleArgs(
|
||||
val isToggleEnabledInitialValue: Boolean,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
isToggleEnabledInitialValue = checkNotNull(savedStateHandle[IS_TOGGLE_ENABLED]) as Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.exampleDestination(
|
||||
onNavigateToNextScreen: (CompletionData) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = EXAMPLE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(IS_TOGGLE_ENABLED) { type = NavType.BoolType }
|
||||
),
|
||||
) {
|
||||
ExampleScreen(onNavigateToNextScreen = onNavigateToNextScreen)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToExample(
|
||||
isToggleEnabled: Boolean,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(
|
||||
"$EXAMPLE_ROUTE_PREFIX/$isToggleEnabled",
|
||||
navOptions,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -1,775 +0,0 @@
|
||||
# Code Style Guidelines
|
||||
|
||||
## Contents
|
||||
|
||||
* [Style : Kotlin](#style--kotlin)
|
||||
* [Style : ViewModels](#style--viewmodels)
|
||||
* [Best Practices : Kotlin](#best-practices--kotlin)
|
||||
* [Best Practices : Jetpack Compose](#best-practices--jetpack-compose)
|
||||
|
||||
The following outlines the general code style and best practices for Android development. It is expected that all developers are familiar with the code style documents referenced here and have them bookmarked for quick reference when necessary.
|
||||
|
||||
Rather than repeating the rules listed in those documents, this guide only mentions rules that **override** or are **in addition to** the ones present in them. Also note that while the [bitwarden-style.xml](/docs/bitwarden-style.xml) formatter and [detekt tool](https://github.com/detekt/detekt) will enforce many of the rules (such as the 100 character line limit) there are many it can not or will not enforce and it is therefore extremely important that all developers **read and follow this document.** For more details on what the formatter _does_ handle, refer directly to the settings in Android Studio under `Preferences > Editor > Code Style` and select the language of interest.
|
||||
|
||||
## Style : Kotlin
|
||||
|
||||
The library's Kotlin code style adheres closely to both the [code style documentation for the Kotlin language itself](https://kotlinlang.org/docs/reference/coding-conventions.html) and the [code style documentation for the Android Open Source Project](https://android.github.io/kotlin-guides/style.html). The former provides the "base" reference while the AOSP docs should be viewed as tweaks to that document. In the rare cases where the two documents disagree, the AOSP rules should apply.
|
||||
|
||||
### Disagreements between the Kotlin language and Android code style documents
|
||||
|
||||
Some notable disagreements between the two documents include:
|
||||
|
||||
- AOSP rules do not allow for two-letter acronyms to be capitalized separately when part of a member name. For example
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
val fetcher: IDFetcher
|
||||
|
||||
// Good
|
||||
val fetcher: IdFetcher
|
||||
```
|
||||
|
||||
### Custom library style
|
||||
|
||||
In general, the code style documents referenced above cover most of the basic cases of interest. There will always be situations, however, where the documents are unclear, vague, or simply do not offer an opinion. Furthermore, there are some cases where we will directly break with the code style documents. Below are some examples of these cases (though it should be noted that no single document can ever fully classify our complete style and existing code should always be sought for a reference in questionable cases.)
|
||||
|
||||
#### Class and file layout
|
||||
|
||||
The documents specify that a class layout should use the following order:
|
||||
|
||||
```
|
||||
- Property declarations and initializer blocks
|
||||
- Secondary constructors
|
||||
- Method declarations
|
||||
- Companion object
|
||||
```
|
||||
|
||||
We will adhere to this order, but there are a few additions / modifications :
|
||||
|
||||
- Nested class definitions should be placed _after_ the Companion object. In cases where the nested
|
||||
class is only used internally, it is also valid to simply place the class in the same file after
|
||||
the main source class.
|
||||
- The code style documents say to use "logical ordering" for method declarations. We will adhere to this principle while also noting that does not mean new code in a file should simply be added to the end; be thoughtful about placement! In the absence of any compelling logistical ordering, alphabetization is a valid choice (though not required).
|
||||
|
||||
Also please note the following:
|
||||
- Methods with certain "special modifiers" (`override`, `operator`, `abstact`, etc.) should come first and methods with the same modifiers should be grouped together. Certain types of annotations may also qualify as a "special modifier" (refer to existing library code).
|
||||
- If any kind of method group described above requires "sub-groups", these can be denoted by the use of the `//region <Name> ... //endregion <Name>` markers. These regions should be used for a very small set of methods, such as for the overrides to a single interface.
|
||||
|
||||
#### Type omission
|
||||
|
||||
We generally prefer to omit types whenever the compiler allows it **and** the return type is _unambiguous_. This is true for functions, properties, and variable declarations.
|
||||
|
||||
```kotlin
|
||||
// Good: This is clearly an integer value.
|
||||
val durationInSeconds = 100
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad: The return type can only be inferred by looking at the signature of another function, which
|
||||
// might itself be omitting a type.
|
||||
fun requestData() = apiService.getData()
|
||||
```
|
||||
|
||||
When omitting a type for a function, it is very important that it is done _thoughtfully_; a function's type should not be the _accidental consequence_ of overly concise code.
|
||||
|
||||
#### Expression functions
|
||||
|
||||
We generally prefer using expression functions whenever possible to keep the code concise and free of unnecessary boilerplate. This should only be done when a call chain naturally suggests such a form, though; complicated functions should not be forced into a single expression simply to take advantage of this feature.
|
||||
|
||||
#### Functions as parameters
|
||||
|
||||
We strongly encourage the direct use of functions as parameters rather than defining one-off listener classes. For example
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
fun waitForEvent(handler: (Event) -> Unit) {
|
||||
val event = produceEvent()
|
||||
handler(event)
|
||||
}
|
||||
```
|
||||
|
||||
is preferable to
|
||||
|
||||
```kotlin
|
||||
interface EventHandler {
|
||||
fun handleEvent(event: Event): Unit
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
// Bad
|
||||
fun waitForEvent(handler: EventHandler) {
|
||||
val event = produceEvent()
|
||||
handler.handleEvent(event)
|
||||
}
|
||||
```
|
||||
|
||||
This not only reduces the amount of boilerplate code necessary, but it also ensures lambdas can be used properly from within Kotlin. When an interface is preferred for some reason, strongly consider the user of [functional interfaces](https://kotlinlang.org/docs/fun-interfaces.html) to continue to take advantage of SAM conversions at the callsite.
|
||||
|
||||
|
||||
#### Long function calls
|
||||
|
||||
As discussed [in the code style document](https://kotlinlang.org/docs/coding-conventions.html#method-calls) long function calls should place each argument on their own line. The closing parenthesis should be on its own line as well:
|
||||
|
||||
```kotlin
|
||||
drawSquare(
|
||||
x = 10,
|
||||
y = 10,
|
||||
width = 100,
|
||||
height = 100,
|
||||
fill = true,
|
||||
)
|
||||
```
|
||||
|
||||
Unlike those rules, however, we don't allow grouping "multiple closely related arguments on the same line".
|
||||
|
||||
When there is only one argument but the function call must wrap to multiple lines, that argument should still be placed on its own line(s).
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
.register(
|
||||
object : Callback<DeviceEvent>() {
|
||||
override fun onSuccess() = ...
|
||||
override fun onError() = ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
.register(object : Callback() {
|
||||
override fun onSuccess() = ...
|
||||
override fun onError() = ...
|
||||
})
|
||||
```
|
||||
|
||||
#### Annotation-related formatting
|
||||
|
||||
When annotating properties, the annotations should be on their own lines and there must be a space between each annotated property. The following is correct:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
@Inject
|
||||
val propertyA: PropertyA
|
||||
|
||||
@Inject
|
||||
val propertyB: PropertyB
|
||||
|
||||
@Inject
|
||||
@CustomAnnotation
|
||||
val propertyC: PropertyC
|
||||
|
||||
@Inject
|
||||
val propertyD: PropertyD
|
||||
```
|
||||
|
||||
while this is not:
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
@Inject val propertyA: PropertyA
|
||||
@Inject val propertyB: PropertyB
|
||||
@Inject @CustomAnnotation
|
||||
val propertyC: PropertyC
|
||||
@Inject val propertyD: PropertyD
|
||||
```
|
||||
|
||||
#### Chained call formatting
|
||||
|
||||
As mentioned in the main documents, for long call chains it is preferred to put all method calls on their own lines, even the first:
|
||||
|
||||
```kotlin
|
||||
val anchor = owner
|
||||
?.firstChild!!
|
||||
.siblings(forward = true)
|
||||
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
|
||||
```
|
||||
|
||||
The goal should always be to keep the start of function calls and the closing parenthesis vertically aligned. Consider the following example:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
fragmentComponent
|
||||
.customFragmentComponent(
|
||||
CustomFragmentModule(
|
||||
this,
|
||||
argument,
|
||||
)
|
||||
)
|
||||
.inject(this)
|
||||
```
|
||||
|
||||
Notice what happens if the first method call to `.customFragmentComponent` is placed on the first line:
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
fragmentComponent.customFragmentComponent(
|
||||
CustomFragmentModule(
|
||||
this,
|
||||
argument,
|
||||
)
|
||||
)
|
||||
.inject(this)
|
||||
```
|
||||
|
||||
The closing parenthesis for `.customFragmentComponent()` does not align with either it nor with `.inject`.
|
||||
|
||||
Note that in cases where a single method is being called, it is valid for it to be on the same line as the instance:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
return Bundle().apply {
|
||||
putParcelable(KEY_ARGUMENTS, arguments)
|
||||
}
|
||||
```
|
||||
|
||||
If a chain is created here, however, the method must be moved down:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
return Bundle()
|
||||
.apply {
|
||||
putParcelable(KEY_ARGUMENTS, arguments)
|
||||
}
|
||||
.also { ... }
|
||||
```
|
||||
|
||||
Failure to do so will result in the following incorrect formatting:
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
return Bundle().apply {
|
||||
putParcelable(KEY_ARGUMENTS, arguments)
|
||||
}
|
||||
.also { ... }
|
||||
```
|
||||
|
||||
In some cases proper formatting will be impossible. Each of the following---while not ideal---is in the "correct" format because there is no good alternative:
|
||||
|
||||
```kotlin
|
||||
// Acceptable (no better alternative)
|
||||
topLevelFunction(
|
||||
...long argument list...
|
||||
)
|
||||
.apply { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Acceptable (no better alternative)
|
||||
ObjectConstructor(
|
||||
...long argument list...
|
||||
)
|
||||
.apply { ... }
|
||||
```
|
||||
|
||||
These types of scenarios should be avoided when possible (as described above) but are allowed when necessary.
|
||||
|
||||
#### Miscellaneous Code Formatting
|
||||
|
||||
Whenever questions about code formatting arise in which multiple options are valid according to all previously described rules, [the Rectangle Rule](https://github.com/google/google-java-format/wiki/The-Rectangle-Rule) is a good test to use to prefer one style over the other.
|
||||
|
||||
#### Documentation
|
||||
|
||||
All public classes, functions, and properties should include documentation in the [KDoc style](https://kotlinlang.org/docs/kotlin-doc.html). Private classes, functions, and properties may optionally be documented as needed.
|
||||
|
||||
##### Class Documentation
|
||||
|
||||
Classes with more than 2 constructor properties should document each individually using the `@property` label; otherwise the property descriptions can be incorporated into the class description:
|
||||
|
||||
```kotlin
|
||||
// Good. The class contains a single constructor property that is included in the class's own
|
||||
// documentation
|
||||
|
||||
/**
|
||||
* A wrapper class for a unique idenfitier, [id].
|
||||
*/
|
||||
data class IdWrapper(
|
||||
val id: String,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
```kotlin
|
||||
// Good. The class contains more than two properties and each are documented separately.
|
||||
|
||||
/**
|
||||
* A class containing various data.
|
||||
*
|
||||
* @property id The unique identifier for the data.
|
||||
* @property name The name of the data.
|
||||
* @property value The value associated with the data.
|
||||
*/
|
||||
data class Data(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad. The constructor properties are not documented.
|
||||
|
||||
/**
|
||||
* A class containing various data.
|
||||
*/
|
||||
data class Data(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad. The constructor properties are not documented individually.
|
||||
|
||||
/**
|
||||
* A class containing various data ([id], [name], [value]).
|
||||
*/
|
||||
data class Data(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
```kotlin
|
||||
// Bad. Not using KDoc style.
|
||||
|
||||
// A class containing various data ([id], [name], [value]).
|
||||
data class Data(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
```
|
||||
|
||||
##### Functions
|
||||
|
||||
Functions are typically allowed to include documentation for parameters within the body of the function documentation itself independent of their number:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
|
||||
/**
|
||||
* Gets the data for the given [id].
|
||||
*/
|
||||
fun getData(id: String): Data
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
|
||||
/**
|
||||
* Gets a list of data items from the [startDateInMillis] to the [endDateInMillis]. This number of
|
||||
* items in the list will not exceed [maxCount].
|
||||
*/
|
||||
fun getDataList(
|
||||
startDateInMillis: Long,
|
||||
endDateInMillis: Long,
|
||||
maxCount: Long
|
||||
): List<Data>
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad. Not in KDoc style.
|
||||
|
||||
// Gets the data for the given [id].
|
||||
fun getData(id: String): Data
|
||||
```
|
||||
|
||||
When each parameter appears to require more focused documentation, `@param` may be used;
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
|
||||
/**
|
||||
* Gets a list of data items.
|
||||
*
|
||||
* @param startDateInMillis The beginning data (in epoch time as milliseconds) to begin searching
|
||||
* for data items.
|
||||
* @param endDateInMillis The end data (in epoch time as milliseconds) to stop searching
|
||||
* for data items.
|
||||
* @param maxCount The maximum number of items to return.
|
||||
*/
|
||||
fun getDataList(
|
||||
startDateInMillis: Long,
|
||||
endDateInMillis: Long,
|
||||
maxCount: Long
|
||||
): List<Data>
|
||||
```
|
||||
|
||||
#### Inline comments
|
||||
|
||||
Inline comments are encouraged, particularly when the logic being described is not self-explanatory. The comments should:
|
||||
|
||||
- begin with `//`.
|
||||
- include a space before the first word.
|
||||
- capitalize the first word.
|
||||
- optionally include punctuation for sentence fragments or single sentences.
|
||||
- include punctuation for multiple sentences.
|
||||
- prefer the "imperative" voice.
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
|
||||
// Get the data from the database
|
||||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
|
||||
// Get the data from the database. This will happen synchronously.
|
||||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// OK. Not in the imperative voice.
|
||||
|
||||
// Gets the data from the database
|
||||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad. Missing space before first word and missing capitalization on first word.
|
||||
|
||||
//get the data from the database
|
||||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Bad. Missing punctuation for multiple sentences.
|
||||
|
||||
// Get the data from the database
|
||||
// This will happen synchronously
|
||||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
## Style : ViewModels
|
||||
|
||||
- Private functions that handle actions should be prefixed with "handle" and suffixed with the name of the action. (ex: `handleSubmitClick`)
|
||||
|
||||
## Best Practices : Kotlin
|
||||
|
||||
The following contains general tips and best practices that apply for Kotlin code (unless otherwise specified) that has not already been mentioned in their specific sections above.
|
||||
|
||||
- We will be adhering to the 100 character line limit. This will be enforced in most places by the auto-formatter.
|
||||
|
||||
- Avoid unnecessary `if` / `else` nesting and keep code as left-aligned as possible by using early return statements. For example
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
fun someMethod() {
|
||||
if (someCondition) {
|
||||
// ...many lines of code...
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// Bad
|
||||
fun someMethod() {
|
||||
if (someCondition) {
|
||||
// ...many lines of code...
|
||||
}
|
||||
}
|
||||
|
||||
// Good
|
||||
fun someMethod() {
|
||||
if (!someCondition) {
|
||||
return
|
||||
}
|
||||
// ...many lines of code...
|
||||
}
|
||||
```
|
||||
|
||||
When an early return is not possible because additional code is required to run after the `if`, consider moving the `if` to its own method.
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
fun someMethod() {
|
||||
if (someCondition) {
|
||||
// ...many lines of code...
|
||||
}
|
||||
// ...more code...
|
||||
}
|
||||
|
||||
// Good
|
||||
fun someMethod() {
|
||||
internalHelperMethod()
|
||||
// ...more code...
|
||||
}
|
||||
|
||||
// where...
|
||||
fun internalHelperMethod() {
|
||||
if (!someCondition) {
|
||||
return
|
||||
}
|
||||
// ...many lines of code...
|
||||
}
|
||||
```
|
||||
|
||||
- Using an expression like `true == someBoolean` is acceptable only when `someBoolean` is nullable (i.e. `Boolean?`) and requires unwrapping:
|
||||
|
||||
```kotlin
|
||||
// Bad
|
||||
val nonNullBoolean: Boolean = true
|
||||
if (nonNullBoolean == true) ...
|
||||
|
||||
// Good
|
||||
val nonNullBoolean: Boolean = true
|
||||
if (nonNullBoolean) ...
|
||||
|
||||
// Bad (does not compile)
|
||||
val nullableBoolean: Boolean? = true
|
||||
if (nullableBoolean) ...
|
||||
|
||||
// Good
|
||||
val nullableBoolean: Boolean? = true
|
||||
if (nullableBoolean == true) ...
|
||||
```
|
||||
|
||||
- Any method or constructor left blank deliberately (such as an unneeded interface method) should be formatted as a one-line expression function with the explicit return type of `Unit` :
|
||||
|
||||
```kotlin
|
||||
override fun unusedMethod() = Unit
|
||||
```
|
||||
|
||||
- Nullability should be very clearly communicated. In Kotlin, **only objects expected to be null should be given nullable types**:
|
||||
|
||||
```kotlin
|
||||
// Bad: Are these all *really* nullable? An network object with an "optional" ID does not make
|
||||
// much sense.
|
||||
data class NetworkData {
|
||||
val id: String?
|
||||
val name: String?
|
||||
val iconUrl: String?
|
||||
}
|
||||
|
||||
// Good: The API docs for this class communicate that "id" and "name" will never be null, so
|
||||
// they should not be given nullable types. "iconUrl" is not guaranteed, so it may be made
|
||||
// nullable.
|
||||
data class NetworkData {
|
||||
val id: String
|
||||
val name: String
|
||||
val iconUrl: String?
|
||||
}
|
||||
```
|
||||
|
||||
- Whenever possible, pass and return _interfaces_ rather than _implementations_. For example:
|
||||
|
||||
```kotlin
|
||||
// Bad: Why should the method care what type of List is passed to it and why should the caller
|
||||
// care what type of List is passed back?
|
||||
fun filter(input: ArrayList<String>): ArrayList<String> { ... }
|
||||
|
||||
// Good
|
||||
fun filter(input: List<String>): List<String> { ... }
|
||||
```
|
||||
|
||||
- With very few exceptions, static variables should never be used. Their use is typically a symptom of a failure to properly declare and pass dependencies from one area of the library to another. It is better to explicitly pass the dependencies when possible. When "static" behavior is absolutely needed, an injected singleton should be used to provided the data instead:
|
||||
|
||||
```kotlin
|
||||
// Bad: This class should not depend on the static data held by another. Not only does it make
|
||||
// it hard to understand what the dependencies are here, this code is potentially unstable and
|
||||
// unpredictable and hard to control during tests.
|
||||
class NeedsExternalData {
|
||||
fun someMethod() {
|
||||
val data = SomeOtherClass.staticData
|
||||
// ...code requiring Data instance...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good: This class requires a Data object to function properly so it is instantiated with it.
|
||||
class NeedsExternalData(
|
||||
private val data: Data,
|
||||
) {
|
||||
fun someMethod() {
|
||||
// ...code requiring Data instance...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good: This class requires a Data object for someMethod to function properly, so it is passed
|
||||
// in.
|
||||
class NeedsExternalData {
|
||||
fun someMethod(data: Data) {
|
||||
// ...code requiring Data instance...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good: This class requires a Data object for someMethod to function properly, so we inject
|
||||
// an instance of DataProvider.
|
||||
class NeedsExternalData @Inject constructor(
|
||||
private val dataProvider: DataProvider
|
||||
) {
|
||||
fun someMethod() {
|
||||
val data = dataProvider.getData()
|
||||
// ...code requiring Data instance...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Functions should not intentionally throw exceptions! Any function that needs to represent the possibility of both a success and an error should either:
|
||||
- Return the [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type.
|
||||
- Return a custom sealed class to model the possibilities.
|
||||
- Return a nullable value and clearly indicate a `null` represents an empty/failure state of some kind.
|
||||
|
||||
- When it is absolutely required (such as when dealing with external libraries that throw) exception handling should be done _properly_ and _only when necessary_. This means that (except for rare cases):
|
||||
|
||||
- Never catch `Exception` generically. Always catch the specific errors that are _known to be possible_:
|
||||
|
||||
```kotlin
|
||||
// Bad: What exception is being thrown here and why? Is it something we can fix by trying
|
||||
// again or is it unrecoverable?
|
||||
try {
|
||||
service.makeServiceCall()
|
||||
} catch (e: Exception) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Good
|
||||
try {
|
||||
service.makeServiceCall()
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
- Never catch a `RuntimeException` that _can't happen_:
|
||||
|
||||
```kotlin
|
||||
// Bad: A NumberFormatException is not possible here based on what we know about the value
|
||||
// we're using, so we're adding code that isn't necessary.
|
||||
val definitelyANumber = "1234"
|
||||
val value = try {
|
||||
Integer.parseInt(definitelyANumber)
|
||||
} catch (e: NumberFormatException) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
val definitelyANumber = "1234"
|
||||
val value = Integer.parseInt(definitlyANumber)
|
||||
```
|
||||
|
||||
- Never use `e.printStackTrace()`. Instead, use a configurable logging class:
|
||||
|
||||
```kotlin
|
||||
// Bad: This will print directly to the system, even in production.
|
||||
try {
|
||||
service.makeServiceCall()
|
||||
} catch (e: RemoteException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
try {
|
||||
service.makeServiceCall()
|
||||
} catch (e: RemoteException) {
|
||||
Timber.e(e, "Error making a service call.")
|
||||
}
|
||||
```
|
||||
|
||||
- Never catch exceptions to "solve" bugs that should be managed elsewhere.
|
||||
|
||||
```kotlin
|
||||
// Bad: Why does the method throw the NPE? Is it our fault or a problem in the method itself?
|
||||
// It is impossible to tell here.
|
||||
val locationID = methodReturningNullableString()
|
||||
try {
|
||||
methodRequiringNonNullArguments(locationId)
|
||||
} catch (e: NullPointerException) {
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
val locationId = methodReturningNullableString()
|
||||
if (locationId == null) {
|
||||
return
|
||||
}
|
||||
methodRequiringNonNullArguments(locationId)
|
||||
```
|
||||
|
||||
- Never wrap huge chunks of code in a `try` / `catch`; only wrap the lines that throw an `Exception`:
|
||||
|
||||
```kotlin
|
||||
// Bad: It is difficult to follow the flow here. Which lines throw? At what point in the
|
||||
// code might we be forced to the catch block?
|
||||
try {
|
||||
val value = lineThatThrows()
|
||||
// ...many more lines of code...
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
val value = try {
|
||||
lineThatThrows()
|
||||
} catch (e: RemoteException) {
|
||||
// ...handle the error case...
|
||||
return
|
||||
}
|
||||
// ...many more lines of code...
|
||||
```
|
||||
|
||||
When multiple lines throw the same exception, they may all be placed in the same `try` block:
|
||||
|
||||
```kotlin
|
||||
val value1: String
|
||||
val value2: String
|
||||
val value3: String
|
||||
try {
|
||||
value1 = lineThatThrows1()
|
||||
value2 = lineThatThrows2()
|
||||
value3 = lineThatThrows3()
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
When there is code between them that does _not_ throw, then each call should be wrapped separately:
|
||||
|
||||
```kotlin
|
||||
val value1 = try {
|
||||
lineThatThrows1()
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
// ...code that does not throw...
|
||||
val value2 = try {
|
||||
lineThatThrows2()
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
// ...code that does not throw...
|
||||
val value3 = try {
|
||||
lineThatThrows3()
|
||||
} catch (e: RemoteException) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices : Jetpack Compose
|
||||
|
||||
When writing UI layer code using Jetpack Compose, the [Compose API guidelines](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md) should be considered the go-to reference for style and best practices.
|
||||
|
||||
Special consideration should be taken to avoid unnecessary recompositions. There are now numerous sources that address the topic, including the following:
|
||||
|
||||
- https://developer.android.com/jetpack/compose/performance/bestpractices
|
||||
- https://getstream.io/blog/jetpack-compose-guidelines/
|
||||
- https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/
|
||||
Reference in New Issue
Block a user