Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion docs/android/best_practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,92 @@ Tie your coroutines to an Android lifecycle (e.g., `viewModelScope` or `lifecycl

For more details on race conditions, see [Race Condition](https://en.wikipedia.org/wiki/Race_condition#In_software).

## Use strong types instead of strings for logic

Use strings for storing and displaying text, not for controlling logic or behavior in your code. Relying on strings for logic-such as passing a string to determine a destination or behavior-can introduce errors like typos and make it harder to track or refactor your code. Instead, use strong types, such as a `sealed` class or, if necessary, an `enum`, to represent these concepts. Reserve strings for raw values from third-party sources or for UI display. If you must use strings, define them as `const val` (following our [codestyle](/docs/android/codestyle#avoid-magic-numbers-and-strings)) or wrap them in a strong type, such as an [inline value class](https://kotlinlang.org/docs/inline-classes.html).

:::note[Example]

#### ❌ Avoid this pattern

```kotlin
fun newInstance(destination: String): Intent {
// Logic based on string value
return Intent().apply {
putExtra("destination", destination)
}
}
```

#### ✅ Prefer this approach

```kotlin
private const val DESTINATION_KEY = "destination"

@Parcelize
sealed interface Destination : Parcelable {
data object General : Destination
data object Notifications : Destination
data object Privacy : Destination
}

fun newInstance(destination: Destination): Intent {
return Intent().apply {
putExtra(DESTINATION_KEY, destination)
}
}

fun onIntent(intent: Intent) {
val destination = intent.getParcelableExtra<Destination>(DESTINATION_KEY, Destination::class.java)
when (destination) {
Destination.General -> // Handle General
Destination.Notifications -> // Handle Notifications
Destination.Privacy -> // Handle Privacy
null -> // Handle missing destination
}
}
```

:::

Using strong types for destinations helps prevent errors, improves code navigation, and makes refactoring more reliable. When you use `sealed` classes with `when`, the compiler can catch missing cases, and your IDE can quickly locate all usages of a specific destination, making updates and maintenance easier.

### Why sealed classes are better than enums

Sealed classes provide more flexibility and safety than enums. With sealed classes, you can define subclasses with their own properties, allowing you to pass additional data as needed for each type. This makes your APIs more expressive and adaptable.

For example, if the `Notifications` destination needs a `title` parameter, define it like this:

```kotlin
@Parcelize
sealed interface Destination : Parcelable {
data object General : Destination
data class Notifications(val title: String) : Destination
data object Privacy : Destination
}

fun onIntent(intent: Intent) {
val destination = intent.getParcelableExtra<Destination>(DESTINATION_KEY, Destination::class.java)
when (destination) {
Destination.General -> // Handle General
is Destination.Notifications -> {
val title = destination.title
// Handle Notifications with title
}
Destination.Privacy -> // Handle Privacy
null -> // Handle missing destination
}
}
```

:::note
When you use `when` with a sealed class, avoid adding an `else` branch. This ensures that if you add a new case, the compiler will require you to handle it, making your code safer and easier to maintain.
:::

By using sealed classes, you can safely add new destination types with their own required fields, and the compiler will enforce handling all cases. This approach makes your code more robust, maintainable, and less error-prone than using enums or strings for logic control.

Read more about sealed modifier on the [Kotlin documentation](https://kotlinlang.org/docs/sealed-classes.html).

## Code organization

### Keep your classes small
Expand Down Expand Up @@ -173,7 +259,11 @@ The further you progress in development, the more difficult it becomes to debug

### Leverage Kotlin compiler

The Kotlin compiler can help you catch issues early. For example, using the `when` operator with sealed classes/interfaces ensures that all cases are handled.
The Kotlin compiler can help you catch issues early. For example, using the `when` operator with `sealed` classes/interfaces ensures that all cases are handled.

:::note
Favor [composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance) when designing your classes. Composition leads to more flexible, maintainable, and testable code by allowing you to build complex behavior from simpler, reusable components, rather than relying on rigid class hierarchies.
:::

**Example:**

Expand Down