Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
216: Retention Policies for Custom Annotations
Up until now, you've probably been creating annotations and just assuming they'll be there when you need them. But if you've ever tried to check for an annotation using reflection and found that it simply "disappeared," you've run into the retention policy problem. Essentially, you have to tell the Kotlin compiler how long that metadata should survive.
Wait, why do I even need to specify a retention policy?
Think of annotations as sticky notes. Some sticky notes are just for the person writing the code (the developer), some are for the person assembling the parts (the compiler), and some need to stay attached to the product even after it's shipped and running in a customer's environment (the JVM). If you don't specify a policy, Kotlin defaults to AnnotationRetention.RUNTIME, which is helpful, but relying on defaults in a professional codebase is a habit I'd suggest breaking.
By explicitly setting the retention, you're optimizing your compiled bytecode. Why keep metadata in the final binary if you only needed it to trigger a warning in the IDE? It's a small win, but it's the difference between a "it works" project and a production-ready one.
What is the actual difference between SOURCE, BINARY, and RUNTIME?
There are three levels, and the distinction is all about where the annotation "lives" during the lifecycle of your code.
- SOURCE: These are stripped out during compilation. They never even make it into the
.classfiles. I use these mostly for things like@Suppressor custom linting rules where the compiler just needs a hint. - BINARY: These are stored in the
.classfiles, but the JVM ignores them when the class is loaded into memory. You can't find these using reflection, but a bytecode analysis tool (like ProGuard or a custom compiler plugin) can still see them. - RUNTIME: These are the heavy lifters. They stay in the binary and are loaded into the JVM. This is the only way you can use reflection to ask a class, "Hey, do you have this annotation?"
@Retention(AnnotationRetention.SOURCE)
annotation class InternalDraft // Only visible in the editor
@Retention(AnnotationRetention.BINARY)
annotation class OptimizedByCompiler // Visible in bytecode, not at runtime
@Retention(AnnotationRetention.RUNTIME)
annotation class SensitiveData // Visible everywhere, including reflection
How do I actually use a RUNTIME annotation in a real feature?
Let's say you're building a logging system and you want to make sure you don't accidentally leak PII (Personally Identifiable Information) into your logs. You can create a @SensitiveData annotation and use reflection to mask those fields.
If you set this to BINARY or SOURCE, the following logic would simply fail to find the annotation and would print the raw password. You must use RUNTIME here.
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class SensitiveData
data class UserProfile(
val username: String,
@SensitiveData val passwordHash: String,
@SensitiveData val socialSecurityNumber: String
)
fun maskSensitiveInfo(obj: Any): String {
val clazz = obj::class
val fields = clazz.members.filterIsInstance<kotlin.reflect.KProperty1<Any, *>>
return fields.joinToString(", ") { prop ->
val value = prop.get(obj)
val isSensitive = prop.annotations.any { it is SensitiveData }
val displayValue = if (isSensitive) "****" else value
"${prop.name}=$displayValue"
}
}
// Usage
val user = UserProfile("kotlin_dev", "secret_hash_123", "999-00-1111")
println(maskSensitiveInfo(user))
// Output: username=kotlin_dev, passwordHash=****, socialSecurityNumber=****
I've seen developers spend hours debugging why prop.annotations was returning an empty list, only to realize they forgot the @Retention tag or accidentally set it to BINARY. Always double-check that if you're using KClass or javaClass to find an annotation, RUNTIME is the only way to go.
📋 Practical Task
Implementing a Runtime-Based Validation Engine
You are tasked with building a simple validation engine that checks if specific fields in a data class are "required" before a record is saved to a database.
Requirements:
- Create a custom annotation called
@RequiredField. - Set the
RetentionPolicyso that the annotation can be detected at runtime via reflection. - Create a data class
Productwith at least three properties (e.g.,name,sku,description). Mark only some of them as@RequiredField. - Write a function
validateProduct(product: Product): List<String>that uses reflection to inspect the properties of the product. If a property is marked as@RequiredFieldbut its value is null or an empty string, add the name of that property to a list of validation errors. - Test your function with a
Productinstance that has missing required fields and print the resulting list of errors.
There are no comments for now.