Do you and your team want to develop better Kotlin apps? Getting through an independent code review will give you a serious advantage. Code reviews will help you catch bugs early and boost performance while creating applications that users will love and rate positively. This checklist covers the basics of building Kotlin apps and will simplify Kotlin code reviews.
Scope and Objectives
Pre-review setup is very important. It means checking that everything is in place before the code analysis begins. First, make sure there’s a well-defined scope and objectives to focus on. This way, you’ll ensure your review is targeted and precise.
Key Best Practices:
- State the review goals clearly—focus on security, performance, readability, and more
- Define the scope precisely—one module, a specific feature, or the whole app
- Set measurable success criteria—number of fixed bugs, performance benchmarks, code coverage percentage, etc.
- Communicate the scope to your team beforehand
Testing Environment Setup
The next crucial step at the pre-review stage is setting up your testing environment. A properly configured environment makes the future review process smooth and efficient. It also will save you from redoing everything in case of a mistake.
Key Best Practices:
- Check if an IDE is appropriately configured with Kotlin and all dependencies
- Verify necessary plugins like Detekt or Gradle are installed and functioning
- Set up a comprehensive test suite, including unit integration and end-to-end tests
- Confirm that the test suite runs smoothly and that the codebase is ready for testing
Code Documentation
Now it’s time for the next critical stage, checking if your Kotlin code speaks clearly for itself through documentation. Well-documented code isn’t just a courtesy—it’s a superpower for efficient reviews and long-term project health.
Key Best Practices:
- Write meaningful KDoc comments for classes, functions, and properties
- Check for clear and concise explanations of complex logic
- Make sure examples work for public APIs and functions
- Verify that documentation is up-to-date with the code
- Encourage documentation generation tools like Dokka
// Good practice of Code Documentation (KDoc)
/**
* A class representing a user profile.
*
* It holds user's personal information and provides methods to manage their profile.
*
* @property userId Unique identifier for the user.
* @property username The user's chosen username.
*/
class UserProfile(val userId: Int, val username: String) {
/**
* Validates personal information.
*
* @param firstName The user's first name. Can be null.
* @param lastName The user's last name. Can be null.
* @return `true` if both the first name and last name are non-null and non-blank, `false` otherwise.
*/
fun validatePersonalInfo(firstName: String?, lastName: String?) =
!firstName.isNullOrBlank() && !lastName.isNullOrBlank()
}
// Bad practice of Code Documentation (KDoc)
/**
* UserProfile class.
*/
class UserProfile(val userId: Int, val username: String) {
/**
* Function to get full name.
*/
fun getFullName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
}
Up-to-Date Dependencies
Checking for up-to-date dependencies determines whether your project’s foundation is solid. Utilizing the most current libraries and tools isn’t just an accolade of having the latest features—it’s a vital step for security and stability in the long run.
Key Best Practices:
- Review dependency updates using Gradle or Maven commands
- Verify that all dependencies are from trusted and reputable sources
- Look for dependencies with known security vulnerabilities
- Check if dependency versions are consistent across the project
- Use dependency management tools to automate updates and vulnerability checks
Static Analysis Tools
Don’t hesitate to bring in the automated eyes of Static Analysis Tools to your Kotlin code reviews. These tools act as your tireless assistants, catching potential issues early and consistently, freeing up human reviewers for more nuanced aspects of the code.
Key Best Practices:
- Add static analysis tools to your project’s CI/CD pipeline
- Use linters like Detekt and Ktlint for Kotlin code-style enforcement
- Configure static analysis rules to match your project’s specific needs and coding standards
- Review and address discoveries from static analysis reports
- Introduce new static analysis rules to other developers
Test Data and Scenarios
Well-conducted tests are your safety net, and reviewing them is just as vital as reviewing the code itself. Solid test data and scenarios ensure your Kotlin code’s reliability, both now and in the future.
- Review test data for realism and edge cases
- Check for comprehensive scenario coverage, including positive and negative cases
- Confirm that tests are independent and don’t rely on external states or dependencies
- Look for clear and descriptive test names that explain the scenario being tested
- Promote data-driven testing for scenarios with multiple input variations
Consistent Naming and Comments
Naming, formatting, and commenting can sometimes seem minor, but they’re the bedrock of readability and maintainability in any Kotlin project. They allow for smoother code reviews and more effective alignment for everyone.
Key Best Practices:
- Confirm that the code sticks to Kotlin coding conventions and style guides
- Check for consistent naming conventions across the codebase
- Ensure comments explain non-obvious logic or complex sections
- Look for opportunities to improve code clarity through better naming or formatting instead of just adding comments
- Verify that comments are accurate and up-to-date with the code
// Good practice of Naming, Formatting, and Comments
class OrderProcessor {
fun calculateTotalPrice(orderItems: List, discountPercentage: Double): Double {
val subtotal = orderItems.sumOf { it.price * it.quantity }
return subtotal * (1 - (discountPercentage / 100))
}
}
data class OrderItem(
val productId: Int,
val productName: String,
val quantity: Int,
val price: Double
)
// Bad practice of Naming, Formatting, and Comments
class order_processor {
fun calc_price(items: List, disc: Double): Double {
// calculate sum
val s = items.sumOf { it.price * it.quantity }
// discount
val discount = s * (disc / 100)
val finalPrice = s - discount
return finalPrice
}
}
data class OrderItem(
val productID: Int,
val item_name: String,
val qty: Int,
val itemPrice: Double
)
Organization of Imports
Keeping the code simple and any imports organized will ultimately speed up reviews, reduce the chances of errors creeping in, and make the codebase understandable for everyone. Code that’s easy to grasp and navigate is a joy to review and maintain. It sets the stage for a healthier and more robust project.
Key Best Practices:
- Break down complex functions
- Reduce nesting to improve code navigability
- Organize imports to clarify dependencies
- Remove unused imports to reduce clutter
- Avoid wildcard imports
// Good practice of Import Organization
import com.example.model.User
import com.example.repository.UserRepository
import com.example.service.UserService
import com.example.view.UserView
class UserProcessor {
private val userRepository = UserRepository()
private val userService = UserService(userRepository)
fun displayUserDetails(userId: Int) {
val user = userService.getUserById(userId) ?: return // Ранній вихід, якщо користувача не знайдено
val formattedName = user.username.capitalize() // Використання вбудованої функції Kotlin
val userView = createUserView(user, formattedName)
userView.display()
}
private fun createUserView(user: User, formattedName: String): UserView {
return UserView(
name = formattedName,
email = user.email.orEmpty() // Безпечна обробка null
)
}
}
// Bad practice of Import Organization
import com.example.utils.*
import com.example.view.UserView
import com.example.service.UserService
import com.example.model.User
import com.example.repository.UserRepository
class UserProcessor {
private val userRepository = UserRepository()
private val userService = UserService(userRepository)
fun displayUserDetails(userId: Int) {
val user = userService.getUserById(userId)
if (user != null) {
val name = user.username
val formattedName = StringUtils.capitalize(name) // Utility function somewhere
val email = if (user.email != null) user.email else "" // Null check inline
val view = UserView(formattedName, email)
view.display()
}
}
}
Logical Grouping and Function Length
Well-organized code is fundamentally more straightforward to understand, test, and evolve. By logically grouping functions and keeping them concise, we create a codebase that reviewers can quickly grasp. Changes become less error-prone, and the overall project becomes more maintainable.
Key Best Practices:
- Group related code into logical units like classes or packages
- Stick to the Single Responsibility Principle (SRP)
- Limit function length
- Refractor overly long functions or poorly grouped code
Proper Feature Utilization
Writing good Kotlin code is more than just having functional code. It’s about utilizing Kotlin’s tools to make apps stronger, readable, and faster. When Kotlin features are correctly used, code reviews become much smoother. Reviewers can quickly understand the important stuff—how the app works and is built—instead of getting lost in complicated or unusual code.
Key Best Practices:
- Leverage Kotlin’s null safety features (e.g., ?, !!, let, run, also, apply) to minimize NullPointerExceptions and simplify code reviews
- Use Kotlin data classes for data-centric entities
- Utilize Kotlin extension functions, higher-order functions, and lambdas
- Check for idiomatic Kotlin usage, steering away from wordy Java-style patterns
// Good practice of Feature Utilization (Idiomatic Kotlin)
import java.util.Locale
data class Address(val street: String?, val city: String, val zipCode: String?)
fun formatAddress(address: Address?) = address?.run {
"""${street?.capitalizeFirst() ?: "Unknown Street"}, ${city.uppercase()}, ${zipCode ?: "N/A"}"""
} ?: "No Address Provided"
fun String.capitalizeFirst(): String =
replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
fun main() {
val validAddress = Address("Main Street", "Kyiv", "01001")
val partialAddress = Address(null, "Lviv", null)
val noAddress: Address? = null
println(formatAddress(validAddress))
println(formatAddress(partialAddress))
println(formatAddress(noAddress))
}
// Bad practice of Feature Utilization (More Java-style/Verbose)
class Address {
var street: String? = null
var city: String = ""
var zipCode: String? = null
constructor(street: String?, city: String, zipCode: String?) {
this.street = street
this.city = city
this.zipCode = zipCode
}
}
fun getFormattedAddress(address: Address?): String {
if (address != null) {
var streetName = address.street
if (streetName == null) {
streetName = "Unknown Street"
} else {
streetName = streetName.capitalize()
}
var zip = address.zipCode
if (zip == null) {
zip = "N/A"
}
return streetName + ", " + address.city.toUpperCase() + ", " + zip
} else {
return "No Address Provided"
}
}
fun main() {
val validAddress = Address("Main Street", "Kyiv", "01001")
val partialAddress = Address(null, "Lviv", null)
val noAddress: Address? = null
println(getFormattedAddress(validAddress))
println(getFormattedAddress(partialAddress))
println(getFormattedAddress(noAddress))
}
Handling Nulls Safely
Null Pointer Exceptions are the bane of many developers’ existence, and Kotlin provides powerful tools to avoid them. By precisely reviewing how nulls are handled in Kotlin code, we reduce the risk of crashes, improve app stability, and make the codebase more straightforward to maintain.
Key Best Practices:
- Use non-null types (?) by default
- Utilize nullable types (?) when null is valid and expected
- Promote safe call (?.) and elvis operator (?:) for concise and safe null handling
- Don’t employ the not-null assertion operator (!!) unless it’s unavoidable
- Test null handling scenarios, especially for nullable types and external data interactions
Managing Exceptions
No code is perfect, and things will inevitably go wrong. What’s important is that your code knows how to react when these accidents happen and can clearly state what went wrong. Checking for good error handling in code reviews ensures that apps are strong and won’t break easily. Plus, clear error messages provide a helpful guide for developers to find and fix problems quickly.
Key Best Practices:
- Use Kotlin try-catch blocks to handle potential exceptions and prevent crashes
- Generate specific and custom exception types to provide richer error context
- Include informative error messages in exceptions
- Don’t catch generic Exception unless necessary
- Review error logging practices
// Good practice of Exception Management
import java.io.File
import java.io.IOException
class FileProcessor {
fun readFileContent(filePath: String): String = try {
File(filePath).readText()
} catch (e: IOException) {
throw FileProcessingException("Error reading file at path: $filePath", e)
} catch (e: SecurityException) {
throw FileProcessingException("Insufficient permissions to read file: $filePath", e)
}
}
class FileProcessingException(message: String, cause: Throwable? = null) : Exception(message, cause)
fun main() {
val processor = FileProcessor()
val filePath = "data.txt"
try {
val content = processor.readFileContent(filePath)
println("File content:\n$content")
} catch (e: FileProcessingException) {
println("Error processing file: ${e.message}")
e.cause?.message?.let { println("Caused by: $it") } // Simplified cause message printing
}
}
// Bad practice of Exception Management
import java.io.File
import java.io.IOException
class FileProcessor {
fun readFileContent(filePath: String): String {
try {
return File(filePath).readText()
} catch (e: Exception) { // Catching generic Exception
throw Exception("File error") // Generic error message
}
}
}
fun main() {
val processor = FileProcessor()
val filePath = "data.txt"
try {
val content = processor.readFileContent(filePath)
println("File content:\n$content")
} catch (e: Exception) {
println("Error: Something went wrong") // Uninformative error message
}
}
Efficient Collections
Reviewing code is vital to making apps faster and lighter. You can ensure there aren’t too many things being created and you’re using the best ways to store data. Such apps are quicker and more responsive, and they consume less charge. Paying attention to these details in code reviews makes a significant difference for app users.
Key Best Practices:
- Minimize object creation, especially in loops or frequently called functions
- Use efficient Kotlin collections like List, Set, and Map appropriately
- Utilize immutable collections where possible
- Use collection operations like map, filter, reduce, and fold
- Review code for potential boxing/unboxing overhead when using primitive types and collections, especially in performance-critical sections
// Good practice of Object Creation and Collection Usage
object StringCache { // Using object declaration for singleton
private val cache = hashSetOf() // Efficient HashSet for lookups
fun isCached(text: String) = cache.contains(text)
fun addToCache(text: String) {
cache.add(text) // HashSet add() already handles duplicates efficiently
}
}
fun List.processUniqueToUpperCase() = this
.filterNot { StringCache.isCached(it) } // Using filterNot for better readability
.map { it.uppercase() }
fun main() {
StringCache.addToCache("apple")
StringCache.addToCache("banana")
val inputList = listOf("apple", "orange", "banana", "grape")
val uniqueUpperCaseStrings = inputList.processUniqueToUpperCase() // Using the extension function
println("Unique Uppercase Strings: $uniqueUpperCaseStrings") // Output: [ORANGE, GRAPE] - Corrected output
}
// Bad practice of Object Creation and Collection Usage
class StringCache { // Class, not object - new instance created every time
private val cache = ArrayList() // Inefficient ArrayList for lookups
fun isCached(text: String): Boolean {
for (item in cache) { // Manual loop for checking existence - slow in ArrayList
if (item == text) {
return true
}
}
return false
}
fun addToCache(text: String) {
if (!isCached(text)) {
cache.add(text)
}
}
}
fun processStrings(stringList: List): List {
val resultList = ArrayList() // Creating a mutable list
for (str in stringList) { // Manual loop for filtering and transforming
if (!StringCache().isCached(str)) { // New StringCache instance created unnecessarily!
resultList.add(str.toUpperCase())
}
}
return resultList
}
fun main() {
val cache = StringCache() // Instance creation
cache.addToCache("apple")
cache.addToCache("banana")
val inputList = listOf("apple", "orange", "banana", "grape")
val uniqueUpperCaseStrings = processStrings(inputList)
println("Unique Uppercase Strings: $uniqueUpperCaseStrings") // Output: [ORANGE, GRAPE]
}
Your Kotlin Development & Audit Services
The opportunity of our modern mobile age is that a global audience of 5 billion smartphone users is waiting for your app to solve their tasks! Make the most of this blessing by utilizing Redwerk’s 20 years of experience in mobile app development. We’ll implement your vision to create a top-rated native Android app, a flawless native iOS app, or even a fully functional hybrid cross-platform app.
Forget project stress and burnt deadlines. Redwerk’s expert developers, designers, QA engineers, and project managers thrive on challenges, transforming them into wins. We’ll help you achieve every business goal on your path to success—take a glance:
- Idea to Solution. We build custom software solutions from scratch. See how we developed a performant and intuitive Android (Kotlin) testing toolkit that can be used by QA engineers, developers, product specialists, and creatives with equal effectiveness.
- Legacy Code Modernization. Refactoring isn’t just about tidying up—it’s about breathing new life into your existing system. Our code refactoring specialists inject modern best practices into your project’s DNA. With our experience developing over 20 Android apps across 7 countries, we know what it takes to improve your system’s performance and management.
- Supercharging Your Product’s Appeal. Our developers don’t just add features—they build within the context of your goals, allowing your solution to grow and adapt effortlessly. Read about how we developed a bike parking system connected to a cloud app, native Android app, and iOS apps for My Bike Valet. We designed their MVP architecture, which provided a strong foundation and led to a straightforward expansion.
- Native or Cross-Platform? Unlock the Ideal Mobile Strategy. At Redwerk, we are masters of both native and cross-platform. Native is the ultimate choice for peak performance and cutting-edge features, while cross-platform development offers lower prices and a broader reach. Don’t navigate this decision alone—Redwerk’s experts will help you choose the right path.
Whether you’re facing a critical code review, need expert Kotlin guidance, want full-cycle development, or anything in between, Redwerk’s Kotlin experts are here for you. We’re eager to hear about your vision and how we can help. Get in touch with our team whenever you’re ready.