Skip to content

Project Conventions

arthur-mrgt edited this page Nov 13, 2024 · 7 revisions

Project Conventions - Kotlin & Jetpack Compose

This document outlines the conventions to follow in order to maintain clean, readable, and consistent code for this project using Jetpack Compose, in line with Google's recommended conventions and industry standards.

Table of Contents

1. Naming Convention

Classes & Interfaces

  • Classes: Use PascalCase notation. Names should be descriptive, e.g., MainActivity, UserRepository.
  • Interfaces: Don't prefix interfaces with "I" to differentiate from classes, interfaces are named like classes without a prefix, e.g., ToDosRepository.

Variables & Properties

  • Local Variables: Use camelCase, e.g., isLoading, userName.
  • Object Properties: Use camelCase, and make the names descriptive, e.g., currentUser, isVisible.
  • Private Properties: Prefix private properties with an underscore if necessary for clarity, e.g., _userCache.

Constants

  • Constants should be in uppercase letters with underscores, e.g., MAX_CONNECTIONS, TIMEOUT_DURATION.
  • Group related constants into companion objects or top-level object declarations for better organization.

Functions

  • Use camelCase for function names, starting with a verb, e.g., fetchData(), onCreateView().
  • Functions should be named descriptively to reflect their behavior or purpose.

Compose Object

  • Use PascalCase for naming composable functions and objects, e.g., MainScreen, UserCard, SettingsButton.
  • Names should be descriptive to clearly indicate the purpose of the composable, e.g., ProfilePicture, SubmitButton.
  • Avoid abbreviations unless they are well-known and do not reduce clarity.

Package

  • Use lowercase letters and follow the reverse domain name notation, e.g., com.example.myapp.
  • Organize packages by feature or layer, e.g., com.example.myapp.ui, com.example.myapp.data.

2. Code Convention

Indentation

  • Use 4 spaces for indentation. Do not use tabs.

Line Length

  • Limit the length of a line to 100 characters.
  • Break up long lines logically, ensuring readability.

Imports

  • Avoid wildcard imports (import *). Import only what is necessary.
  • Organize imports into standard Kotlin libraries, third-party libraries, and project-specific imports.
  • Use the IDE's auto-import feature to keep imports clean and organized.

Parentheses and Braces

  • Always use braces, even for single-line blocks:
    if (condition) {
        doSomething()
    }

Conditional Expressions

  • Use when expressions instead of multiple if-else conditions when appropriate.
  • Prefer elvis operator (?:) for handling null values concisely.

Spacing

  • Use a single space around operators (=, +, -, etc.).
  • Add a space after commas in function calls and declarations.
  • Avoid unnecessary spaces inside parentheses and brackets.

3. Style Convention

Declaration Order

  • Follow this order of declaration in a Kotlin class or file:
    1. Companion objects
    2. Constant properties
    3. Instance properties
    4. Lifecycle methods (in activities or fragments)
    5. Public methods
    6. Private methods

Explicit Types

  • Always declare return types for public functions.
  • For local variables, let the compiler infer types if the type is obvious from the context.

Lambdas

  • If a lambda function is the last parameter of a function, place it outside of the parentheses.
    button.setOnClickListener {
        doSomething()
    }
  • Use parameter names like it only when the context is clear; otherwise, use descriptive names.

Code Formatting

  • Use the built-in formatter in Android Studio to maintain consistent formatting.
  • Reformat code before committing changes to ensure consistent style : optimize code + reformat code + ktfmtFormat

4. Commenting & Documentation

KDoc

  • Use KDoc to document classes, methods, and public properties. Comments should be clear and concise:
    /**
     * Fetches a list of ToDos from the API.
     *
     * @return A list of ToDo items.
     */
    fun fetchToDos(): List<ToDo> {
        // ...
    }

Inline Comments

  • Add inline comments only when necessary to explain complex or non-trivial logic.
  • Keep inline comments brief and relevant.

TODOs

  • Use // TODO: to indicate incomplete features or areas for improvement.
  • Include the author's name and date for tracking, e.g., // TODO: Refactor this method - John, 2024-10-04.

Documentation Best Practices

  • Keep documentation up to date with code changes.
  • Use clear and consistent language in comments and documentation.
  • Avoid redundant comments that simply restate the code.

5. Kotlin Best Practices

Null Safety

  • Use Kotlin's null safety system wisely. Avoid nullable types when possible.
  • Use ?.let {} to safely operate on nullable objects.
  • Consider using checkNotNull() or requireNotNull() for null checks where appropriate.
  • Use lateinit sparingly and only when you are certain the variable will be initialized before use.

Data Classes

  • Use data class to represent objects that contain only data, such as models or DTOs.
  • Override toString(), equals(), and hashCode() if additional behavior is needed beyond the default.

Collections

  • Prefer functional methods like map, filter, fold over explicit loops, unless performance considerations require otherwise.
  • Use forEach for iteration where side effects are expected.
  • Use groupBy and associateBy for organizing collections efficiently.

Asynchronous Execution

  • Use suspend and coroutines for asynchronous operations instead of callbacks.
  • Use withContext(Dispatchers.IO) for I/O operations to avoid blocking the main thread.
  • Prefer Flow for handling streams of data asynchronously.

Extension Functions

  • Use extension functions to add functionality to existing classes without modifying them.
  • Keep extension functions relevant and maintainable; avoid excessive use.

Sealed Classes

  • Use sealed class for representing restricted class hierarchies, especially for representing different states in a UI or different types in a result.

6. Jetpack Compose Best Practices

Composability

  • Break complex UIs into small, reusable composable components.
    @Composable
    fun Greeting(name: String) {
        Text("Hello, $name!")
    }
  • Each composable should be responsible for a single part of the UI.

Stable References

  • Use immutable data models to avoid issues related to recomposition.
  • Avoid passing mutable state directly to composables; instead, use derived states.

State Management

  • Use remember and mutableStateOf to manage state within components, while adhering to the MVVM architecture for larger-scale state management.
    val isChecked = remember { mutableStateOf(false) }
  • Prefer rememberSaveable for state that needs to survive configuration changes.

Performance

  • Minimize recomposition by using remember whenever possible to store computed values.
  • Use keys with LazyColumn items to maintain consistency and avoid unnecessary recompositions.
    LazyColumn {
        items(items = itemList, key = { it.id }) { item ->
            ItemView(item)
        }
    }
  • Use produceState and derivedStateOf to efficiently handle derived or computed states.

Preview

  • Use @Preview annotations to quickly test UI components without running the entire app.
    @Preview(showBackground = true)
    @Composable
    fun PreviewGreeting() {
        Greeting(name = "Compose")
    }
  • Add parameters to @Preview to test different states, e.g., light and dark themes.

Theming and Styling

  • Use MaterialTheme for consistent theming across the app.
  • Define custom colors, typography, and shapes in a central theme file to maintain consistency.
    @Composable
    fun MyAppTheme(content: @Composable () -> Unit) {
        MaterialTheme(
            colors = myColors,
            typography = myTypography,
            shapes = myShapes,
            content = content
        )
    }
  • Use Modifier to style composables, and chain modifiers for readability.

UI Testing

  • Use ComposeTestRule for testing Jetpack Compose components.
  • Write UI tests that validate the expected behavior of composables, using semantics to assert state or values.
    composeTestRule.onNodeWithText("Hello, Compose!").assertExists()