This document outlines the standards and best practices for Kotlin development at Bayat.
- Follow the Kotlin Coding Conventions
- Use ktlint or detekt for automated style enforcement
- Maximum line length should be 120 characters
- Use 4 spaces for indentation, not tabs
- Files should be encoded in UTF-8
- Files should end with a newline
- Use trailing commas in multi-line declarations
-
Classes and Interfaces:
- Use PascalCase for class names (e.g.,
UserRepository
,PaymentService
) - Name classes as nouns or noun phrases
- Use adjectives for interfaces that define behavior (e.g.,
Sortable
,Pageable
)
- Use PascalCase for class names (e.g.,
-
Functions and Properties:
- Use camelCase for function and property names (e.g.,
getUserById()
,isActive
) - Name functions as verbs or verb phrases
- Use
is
,has
, orshould
prefixes for Boolean properties - Name top-level constants using SCREAMING_SNAKE_CASE
- Use camelCase for function and property names (e.g.,
-
Parameters and Local Variables:
- Use camelCase for parameters and local variables
- Avoid single-letter names except for obvious cases (e.g., loop indices)
- Prefer descriptive names that convey purpose
-
Packages:
- Use lowercase package names
- Use the reverse domain name pattern (e.g.,
io.bayat.module.feature
) - Keep package names concise
- Group related classes in the same package
-
Companion Objects:
- Use descriptive names for companion objects when they have a specific purpose
- Default companion objects should not have a name
-
File Structure:
- Prefer one class per file
- Name the file after the primary class or interface it contains
- Extensions to an existing class should be in a separate file with a descriptive name
- Group related extension functions in the same file
-
Class Structure:
- Order class contents in a logical manner:
- Properties
- Initializers
- Public methods
- Internal methods
- Private methods
- Companion object
- Nested/inner classes
- Use
//region
and//endregion
comments for logical grouping if needed
- Order class contents in a logical manner:
-
Package Structure:
- Organize packages by feature, not by type
- Keep a clear separation between public API and internal implementation
- Use
internal
visibility modifier appropriately - Group related functionality within packages
-
Null Safety:
- Avoid using
!!
operator unless absolutely necessary - Use nullable types (
T?
) only when a value can legitimately be null - Prefer
?.
(safe call) and?:
(elvis) operators for null handling - Consider using
requireNotNull()
orcheckNotNull()
for mandatory parameters
- Avoid using
-
Extensions:
- Use extension functions to extend functionality of existing classes
- Prefer extension functions over utility classes
- Keep extensions focused and single-purpose
- Document extensions that might not be obvious
-
Scope Functions:
- Use
let
for executing code blocks on non-null objects - Use
apply
for object configuration - Use
run
when you need both the receiver and a result value - Use
also
for side effects outside the object configuration - Use
with
when working with an object multiple times
- Use
-
Smart Casts:
- Leverage type checking with
is
operator for smart casts - Use
as?
for safe casts that might fail - Avoid unnecessary type checking when the compiler can infer the type
- Leverage type checking with
-
Collections:
- Prefer immutable collections (
listOf
,setOf
,mapOf
) when contents won't change - Use mutable collections (
mutableListOf
, etc.) only when necessary - Leverage collection functions like
map
,filter
,reduce
for transformations - Use sequences for large collections to improve performance
- Prefer immutable collections (
-
Higher-Order Functions:
- Use higher-order functions for code reuse
- Keep lambdas concise and readable
- Use function references (
::functionName
) instead of lambdas for simple cases - Document complex higher-order functions
-
Lambda Syntax:
- Use trailing lambda syntax when the lambda is the last parameter
- Use multi-line lambda syntax for complex lambdas
- Use named parameters for clarity when needed
- Move complex lambdas to separate functions when they become unwieldy
-
Immutability:
- Prefer
val
overvar
whenever possible - Use immutable data structures by default
- Make classes immutable when appropriate
- Use data classes for simple data containers
- Prefer
-
Structured Concurrency:
- Follow structured concurrency principles
- Use appropriate coroutine scope for the context
- Always handle exceptions in coroutines
- Use
supervisorScope
when children should fail independently
-
Dispatchers:
- Choose the appropriate dispatcher for the task (IO, Default, Main)
- Document dispatcher requirements for suspending functions
- Be mindful of thread confinement, especially for UI operations
- Don't block the Main dispatcher with long-running operations
-
Flow:
- Use Flow for asynchronous data streams
- Collect flows in the appropriate coroutine scope
- Use cold flows for most use cases
- Be mindful of flow backpressure for large data streams
-
Error Handling:
- Use
try-catch
blocks within coroutines - Consider using
CoroutineExceptionHandler
for top-level error handling - Document how errors are propagated in suspending functions
- Avoid swallowing exceptions without proper handling
- Use
-
Layer Separation:
- Separate code into distinct layers (presentation, domain, data)
- Define clear boundaries between layers
- Use dependency inversion to maintain layer independence
- Avoid leaking implementation details across layer boundaries
-
Domain Layer:
- Keep business logic in the domain layer
- Use entities to represent core business objects
- Define use cases for business operations
- Keep domain layer independent of framework specifics
-
Data Layer:
- Implement repository interfaces from the domain layer
- Handle data sources and transformations
- Use data models specific to the data layer
- Map between data models and domain entities
-
Presentation Layer:
- Use MVVM or MVI pattern for UI logic
- Keep ViewModels focused on presentation logic
- Use LiveData or StateFlow for observable UI state
- Handle UI events appropriately
-
General Principles:
- Use constructor injection as the primary method
- Define clear module boundaries
- Document dependencies and their lifecycles
- Use appropriate scoping for dependencies
-
Frameworks:
- Use Dagger Hilt for Android applications
- Use Koin for simpler projects or non-Android Kotlin applications
- Follow framework-specific best practices
- Configure DI in a centralized location
-
Testing Framework:
- Use JUnit 5 for unit tests
- Use MockK for mocking
- Use appropriate assertion libraries (e.g., AssertJ, Truth)
- Follow the AAA pattern (Arrange, Act, Assert)
-
Test Organization:
- Name tests descriptively
- Group related tests together
- Use nested tests for complex test scenarios
- Keep tests independent of each other
-
Mocking:
- Mock dependencies, not the system under test
- Use relaxed mocks when appropriate
- Verify critical interactions
- Use spies judiciously
-
Test Dispatchers:
- Use
TestCoroutineDispatcher
for controlling coroutine execution - Set the main dispatcher to a test dispatcher during tests
- Use
runBlockingTest
for testing suspending functions - Be aware of test timeouts when testing coroutines
- Use
-
Flow Testing:
- Use
Flow.test()
extension function for testing flows - Verify emissions and their order
- Test error cases and completion
- Use
TestCoroutineScheduler
for time-based operators
- Use
-
KDoc:
- Document all public APIs with KDoc comments
- Document non-obvious behavior
- Include examples for complex functionality
- Document limitations and edge cases
-
Class and Function Documentation:
- Describe the purpose and responsibility of each class
- Document parameters, return values, and exceptions
- Document thread safety considerations
- Keep documentation up-to-date with code changes
-
Code Comments:
- Use comments to explain "why", not "what"
- Comment complex algorithms or business rules
- Avoid redundant comments that just repeat the code
- Use TODO comments for future improvements with tracking information
-
Architecture Components:
- Use ViewModel for UI-related data handling
- Use LiveData or StateFlow for observable data
- Use Room for database operations
- Use Navigation component for app navigation
-
Compose:
- Follow Compose best practices
- Keep composables small and focused
- Use appropriate state management techniques
- Extract reusable composables
-
Work Manager:
- Use WorkManager for background tasks
- Define clear work constraints
- Handle work failures gracefully
- Document work requirements
-
Resource Organization:
- Follow Android resource naming conventions
- Organize resources by type and feature
- Use resource qualifiers appropriately
- Extract dimensions, colors, and strings into resources
-
Performance Considerations:
- Be mindful of resource usage
- Optimize layouts for performance
- Use appropriate view recycling patterns
- Profile and optimize critical paths
-
Project Structure:
- Use Gradle Kotlin DSL for build scripts
- Organize multi-module projects appropriately
- Define dependencies in a centralized location
- Use version catalogs for dependency management
-
Dependency Management:
- Keep dependencies up to date
- Audit dependencies for security vulnerabilities
- Minimize transitive dependencies
- Document dependency purpose and alternatives
-
Tools:
- Use ktlint for style checking
- Use detekt for code quality analysis
- Configure SonarQube for comprehensive analysis
- Include static analysis in CI pipeline
-
Quality Gates:
- Define clear quality thresholds
- Enforce code quality in CI
- Track and improve metrics over time
- Document exceptions to rules