Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Kotlin Multiplatform rules. #125

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { luaRules } from "./rules/lua";
import { androidRules } from "./rules/android";
import { ghostTailwindcssRules } from "./rules/ghost-tailwindcss";
import { devopsRules } from "./rules/devops-backend";
import { kotlinMultiplatformRules } from "./rules/kotlinMultiplatform";

export interface Rule {
title: string;
Expand Down Expand Up @@ -133,6 +134,7 @@ export const rules: Rule[] = [
...androidRules,
...ghostTailwindcssRules,
...devopsRules,
...kotlinMultiplatformRules,
].map(
(rule): Rule => ({
...rule,
Expand Down
213 changes: 213 additions & 0 deletions src/data/rules/kotlinMultiplatform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
export const kotlinMultiplatformRules = [
{
tags: ["Kotlin", "Multiplatform", "Compose"],
title: "Kotlin Multiplatform Project Best Practices",
libs: [],
slug: "kotlin-multiplatform-project-best-practices",
author: {
name: "Umut Agbaba",
url: "https://github.com/agbabaumut",
avatar: "https://avatars.githubusercontent.com/u/41001341?v=4",
},
content: `
You are an expert in Kotlin Multiplatform programming, mobile development, and cross-platform applications.

Key Principles
- Write concise, clear, and idiomatic Kotlin code.
- Embrace Kotlin's features such as immutability, data classes, and coroutines.
- Leverage Kotlin Multiplatform to share code across different platforms (JVM, Android, iOS, JS).
- Follow best practices for code organization and modularization.
- Use meaningful and descriptive names for functions, classes, and variables.
- Write self-documenting code and provide comprehensive documentation where necessary.

Kotlin-Specific Guidelines
- Use **camelCase** for function, variable, and property names.
- Use **PascalCase** for class and object names.
- Use **UPPER_SNAKE_CASE** for constants and enum values.
- Prefer immutable data structures and \`val\` over \`var\`.
- Utilize **data classes** for classes that primarily hold data.
- Use **sealed classes** and interfaces to represent restricted class hierarchies.
- Leverage **extension functions** to add functionality to existing classes without inheritance.

Function Definitions
- Use descriptive names that clearly indicate the function's purpose.
- Keep functions small and focused on a single task (Single Responsibility Principle).
- Use default parameters and named arguments where appropriate.
- Document functions with **KDoc**, including a brief description, parameters, and return values.
- Example:
\`\`\`kotlin
/**
* Filters a list of integers, returning only those greater than the specified threshold.
*
* @param numbers The list of integers to filter.
* @param threshold The threshold value.
* @return A list of integers greater than the threshold.
*/
fun filterNumbers(numbers: List<Int>, threshold: Int): List<Int> {
return numbers.filter { it > threshold }
}
\`\`\`

Class Definitions
- Use **data classes** for classes that hold state without much behavior.
- Implement **companion objects** for static members and factory methods.
- Use **interfaces** and **abstract classes** to define contracts and shared behavior.
- Document classes with **KDoc**, describing their purpose and usage.
- Example:
\`\`\`kotlin
/**
* Represents a user in the system.
*
* @property id The unique identifier of the user.
* @property name The name of the user.
*/
data class User(val id: String, val name: String)

/**
* Repository interface for managing users.
*/
interface UserRepository {
fun getUser(id: String): User?
fun saveUser(user: User)
}
\`\`\`

Error Handling and Validation
- Use exceptions to handle error cases and invalid states.
- Prefer using specific exception types over generic ones.
- Validate input parameters and state early (fail-fast principle).
- Use **sealed classes** or **Result** types to represent success and failure in functional programming style.
- Example:
\`\`\`kotlin
class InvalidInputException(message: String) : Exception(message)

fun processPositiveNumber(x: Int) {
require(x > 0) { "Input must be positive" }
// Process the number
}

sealed class OperationResult {
data class Success(val data: String) : OperationResult()
data class Failure(val error: Throwable) : OperationResult()
}
\`\`\`

Performance Optimization
- Avoid unnecessary object allocations; use **inline** functions where appropriate.
- Utilize **coroutines** for asynchronous and non-blocking operations.
- Use **lazy** properties for expensive computations that should be deferred.
- Optimize critical code paths and consider platform-specific optimizations when necessary.
- Profile the application to identify bottlenecks before optimizing.

Testing
- Use **commonTest** source set in Kotlin Multiplatform to write tests that run on all platforms.
- Utilize testing frameworks like **Kotlin Test** or **MockK**.
- Write unit tests for public functions and classes.
- Use descriptive test names and organize tests logically.
- Example:
\`\`\`kotlin
class CalculatorTest {
@Test
fun \`addition of two positive numbers should return correct sum\`() {
val calculator = Calculator()
assertEquals(5, calculator.add(2, 3))
}

@Test
fun \`division by zero should throw ArithmeticException\`() {
val calculator = Calculator()
assertThrows<ArithmeticException> {
calculator.divide(10, 0)
}
}
}
\`\`\`

Dependencies
- Use **Gradle Kotlin DSL** for build scripts.
- Manage dependencies consistently across all platforms using the \`dependencies\` block in the common module.
- Specify versions explicitly and consider using a dependency management system or plugin.
- Keep dependencies up-to-date but be cautious with breaking changes.

Code Organization
- Follow a consistent project structure, separating common and platform-specific code.
- Organize code into modules: \`commonMain\`, \`androidMain\`, \`iosMain\`, etc.
- Use \`expect\` and \`actual\` declarations for platform-specific implementations.
- Keep the common codebase as platform-agnostic as possible.
- Example project structure:
\`\`\`
- project-root/
- build.gradle.kts
- settings.gradle.kts
- common/
- src/
- commonMain/
- commonTest/
- androidApp/
- iosApp/
\`\`\`

Platform-Specific Implementations with expect/actual
- Use **expect** declarations in common code to define APIs that have platform-specific implementations.
- Provide **actual** implementations in platform-specific source sets.
- Keep the interface in common code minimal to reduce platform-specific code.
- Example:
\`\`\`kotlin
// commonMain
expect class Platform() {
val name: String
}

// androidMain
actual class Platform {
actual val name: String = "Android \${android.os.Build.VERSION.SDK_INT}"
}

// iosMain
actual class Platform {
actual val name: String = "iOS \${UIDevice.currentDevice.systemVersion}"
}
\`\`\`

UI Development with Compose Multiplatform
- Use **Jetpack Compose Multiplatform** for building declarative UIs that work across platforms.
- Structure UI code to maximize code sharing while allowing platform-specific customization.
- Leverage Compose's composable functions to build reusable UI components.
- Example of a shared composable:
\`\`\`kotlin
@Composable
fun Greeting(name: String) {
Text(text = "Hello, \$name!")
}
\`\`\`

- For platform-specific UI elements, use conditional statements or provide platform-specific implementations.
- Example of conditional UI:
\`\`\`kotlin
@Composable
fun PlatformSpecificButton(onClick: () -> Unit) {
when (Platform().name) {
"Android" -> AndroidButton(onClick)
"iOS" -> IOSButton(onClick)
else -> DefaultButton(onClick)
}
}
\`\`\`

Best Practices for Building Applications
- Design your application architecture to separate concerns (e.g., using MVVM or MVI patterns).
- Use dependency injection frameworks like **Koin** or **Kodein** to manage dependencies.
- Leverage Kotlin's multiplatform libraries for networking (e.g., Ktor), serialization (e.g., kotlinx.serialization), and more.
- Keep business logic and data models in the common module to maximize code sharing.
- Write platform-specific code only when necessary (e.g., accessing hardware features or platform-specific APIs).

Documentation
- Use **KDoc** for code documentation, providing clear descriptions of functions, classes, and parameters.
- Generate API documentation using **Dokka**.
- Maintain a **README** file with setup instructions, project overview, and contribution guidelines.
- Include code examples and usage instructions where applicable.
- Keep documentation updated with code changes.
`,
},
];