val vs var vs const val, and core syntax
val vs var
In Kotlin, val and var both declare variables but differ in mutability: val = read-only reference (cannot be reassigned), var = mutable (can be reassigned).
const val
const val is a compile-time constant β its value must be known at compile time and can only be a top-level or object-level property of a primitive type or String (not inside a function, not a class instance).
const val ek aisa constant hai jiski value compile time par hi known honi chahiye aur baad mein change nahi ho sakti.
const val BASE_URL = "https://api.example.com" β compiler ko pehle se value pata hai.fun main() {
val name = "Alice" // cannot be reassigned
var age = 25
age = 26 // OK
println("Name: $name")
println("Age: $age")
}Name: Alice
Age: 26Type Inference & Type Conversion
Kotlin infers a variable's type from its assigned value. Convert between types with toInt(), toDouble(), toString(), etc.
Default & Named Arguments
fun createUser(name: String, city: String = "Delhi") β call as createUser("John") or createUser("John", city = "LA").Vararg parameters
Examplefun greetAll(greeting: String, vararg names: String) {
for (name in names) println("$greeting, $name!")
}
fun main() = greetAll("Hello", "Alice", "Bob", "Charlie")Hello, Alice!
Hello, Bob!
Hello, Charlie!Tail-recursive functions
A tail-recursive function's recursive call is the last operation before returning. Marked tailrec, Kotlin compiles it into a loop, avoiding StackOverflowError on deep recursion.
tailrec fun countDown(n: Int) {
if (n == 0) return
println(n)
countDown(n - 1) // last operation β tail call
}Inline Function
A higher-order function call has overhead: each lambda is an object, each call creates a stack frame. inline copies the function body to the call site at compile time, removing that overhead β used heavily for small utility HOFs like let, run, forEach.
inline fun myInlineFunc(action: () -> Unit) {
println("Before action")
action()
println("After action")
}Function Types & it
Functions can be stored, passed, and returned like data. When a lambda has a single parameter, Kotlin auto-names it it.
val sum: (Int, Int) -> Int = { a, b -> a + b }
listOf(1, 2, 3).forEach { println(it) }Late Initialization (lateinit)
lateinit declares a non-null var without initializing it immediately β useful for Android Views, dependency injection. Accessing it before init throws UninitializedPropertyAccessException. Works only with non-null var of a class/reference type (not primitives, not val).
lateinit var recyclerView: RecyclerView== vs ===
== checks structural equality (calls equals()). === checks referential equality (same object in memory).
Null Safety in Kotlin
Kotlin's type system distinguishes nullable and non-nullable references at compile time, preventing most NullPointerExceptions.
?β Nullable Type βval age: Int? = null?.β Safe Call βval length = name?.length(returns null instead of crashing if name is null)?:β Elvis Operator βval displayName = name ?: "Guest"(fallback if left side is null)!!β Not-Null Assertion β forces a NPE if the value is null; use only when you're certain it can't be nullletβ commonly combined with?.for null-safe scoped operations:name?.let { println(it) }
Lambda & Higher-Order Functions
A lambda is an anonymous function you can store in a variable or pass as an argument.
Higher-Order Function
A function that takes another function as a parameter, returns a function, or both β e.g. map, filter, forEach.
Unit
Unit is Kotlin's equivalent of Java's void β it means the function returns no meaningful value.
fun calculate(x: Int, op: (Int) -> Int) = op(x)
fun main() = println(calculate(10) { it * 2 }) // trailing lambda20Extension Functions
Lets you add a new function to an existing class without modifying or inheriting from it β resolved statically at compile time based on the declared type.
Existing class mein bina modify/inherit kiye naya function add karna. Code ko clean, reusable, readable banane ke liye Android dev mein bahut use hota hai.
fun String.isValidEmail(): Boolean =
android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun main() = println("test@mail.com".isValidEmail())trueGenerics new
Generics let you write classes/functions that work with any type while keeping compile-time type safety, avoiding unchecked casts.
class Box(val item: T)
fun printItem(item: T) = println(item)
fun main() {
val intBox = Box(10)
val strBox = Box("Hello")
println(intBox.item)
println(strBox.item)
} 10
HelloVariance: out / in
- out (covariant) β the type can only be produced/returned, not consumed.
List<out T>meansList<Dog>is aList<Animal>. - in (contravariant) β the type can only be consumed, not produced.
Comparable<in T>.
out = "produce only" (returns), in = "consume only" (takes as parameter). Ye Java ke ? extends / ? super jaisa hai.
Reified type parameters
Normally generic type info is erased at runtime (type erasure). Marking an inline function's type parameter reified keeps the type available at runtime.
inline fun isInstance(value: Any) = value is T
fun main() = println(isInstance("hello")) // true Sequences vs Collections new
Collection operations (map, filter on List) are eager β each operator processes the whole list and creates an intermediate list. A Sequence is lazy β elements are processed one at a time, only computed when a terminal operation (toList(), first(), sum()) is called.
List operations turant poora list process karte hain har step pe. Sequence lazy hai β element-by-element chalti hai, sirf jab terminal operation call ho tab kaam hota hai. Bade datasets ya chained operations (map+filter+...) mein Sequence zyada efficient hai.
val result = listOf(1, 2, 3, 4, 5)
.asSequence()
.map { println("map $it"); it * 2 }
.filter { println("filter $it"); it > 4 }
.first()
println("Result: $result")map 1
filter 2
map 2
filter 4
map 3
filter 6
Result: 6first() finds a match β with a regular List, every element would go through map first, then every result through filter.OOPs Concepts β the 4 pillars
1. Class & Object
A class is a blueprint defining properties and functions. An object is a real instance of that class.
Class = ghar ka naksha (blueprint). Object = naksha se bana actual ghar.
2. Inheritance
A child/derived class inherits properties and functions from a parent/base class.
3. Encapsulation
Internal state is hidden from outside and accessible only through defined methods (getters/setters).
Data = ATM ke andar ka paisa. Methods = ATM screen β sirf PIN daal ke paisa nikaal sakte ho.
4. Polymorphism
Objects of different classes can be treated via a common supertype, and each class responds to the same call differently.
Ek naam, multiple kaam β draw() Circle alag draw karega, Rectangle alag.
5. Abstraction
Hiding unnecessary implementation details, exposing only essential features.
Inheritance (final / open) & the object keyword
- final (default): All Kotlin classes/members are final by default β cannot be extended/overridden.
- open: Add
opento a class or member to allow it to be extended/overridden.
- object declaration: a lazily-created, thread-safe singleton.
- companion object: a named object tied to a class, used like Java's static members.
- object expression: an anonymous, one-off object, often implementing an interface inline.
companion object Factory { }, accessed as User.Factory; unnamed it's accessed as User.Companion.object, companion object & Singleton β in depth
object AppConstants {
const val BASE_URL = "https://api.example.com"
const val TIMEOUT = 30
}
class User {
companion object {
const val TYPE = "USER"
fun create(name: String) = User().also { it.name = name }
}
var name: String = ""
}| Concept | Meaning |
|---|---|
| object | Standalone, lazily-initialized singleton |
| companion object | Class-level members inside a class β can hold functions & properties |
| static (Java) | No direct equivalent in Kotlin β use object / companion object / top-level functions |
object for every singleton isn't automatically "clean architecture" β it makes testing and lifecycle control harder. Modern Android apps prefer DI (Hilt/Dagger) to manage singleton scope instead of hardcoding it via object.data class, enum class & sealed class
1) data class
Used to hold data. The compiler auto-generates equals(), hashCode(), toString(), copy(), and componentN() (for destructuring) from the primary-constructor properties.
val/var.data class User(val name: String, val age: Int)
fun main() {
val u1 = User("Zulfikar", 25)
val u2 = u1.copy(age = 26)
println(u1)
println(u2)
println(u1 == u2)
}User(name=Zulfikar, age=25)
User(name=Zulfikar, age=26)
false2) enum class
A fixed, closed set of predefined constants β can't be extended at runtime.
enum class PaymentStatus { PENDING, SUCCESS, FAILED }3) sealed class
A restricted class hierarchy β all direct subclasses are known at compile time, so a when over them can be exhaustive without an else branch. Perfect for modelling UI/result states where each state carries different data.
sealed class PaymentResult {
object Loading : PaymentResult()
data class Success(val transactionId: String) : PaymentResult()
data class Error(val message: String) : PaymentResult()
}
fun handle(result: PaymentResult) = when (result) {
is PaymentResult.Loading -> "Loading..."
is PaymentResult.Success -> "Success: ${result.transactionId}"
is PaymentResult.Error -> "Error: ${result.message}"
}Collections in Kotlin
Kotlin has three core collection types: List (ordered, duplicates allowed), Set (unique elements), Map (key-value pairs).
val fruits = listOf("Apple", "Banana", "Orange") // immutable
val mutableFruits = mutableListOf("Apple", "Banana")
mutableFruits.add("Grapes")
val numbers = setOf(1, 2, 2, 3) // duplicates removed
val user = mapOf("name" to "Zulfikar", "city" to "Delhi")
println(fruits); println(mutableFruits); println(numbers); println(user["name"])[Apple, Banana, Orange]
[Apple, Banana, Grapes]
[1, 2, 3]
ZulfikarCommon operations
forEachβ loop;filterβ condition-based subset;mapβ transform each element;containsβ membership check.
Scope Functions
Execute a block of code within an object's context β five functions differing in receiver (this/it) and return value (object/result).
| Function | Receiver | Returns | Typical use |
|---|---|---|---|
| let | it | lambda result | null-checks, transforming |
| run | this | lambda result | object config + compute a result |
| with | this | lambda result | grouping calls on an existing object |
| apply | this | the object itself | configuring an object (builder-style) |
| also | it | the object itself | side effects: logging, debugging |
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("USER_ID", 101)
}.also { println("Intent created: $it") }
val name = "Zulfikar"
name.let { println(it) }Delegation in Kotlin
One object hands responsibility for a task to another object, using the by keyword β favouring composition over inheritance.
| Problem | Delegation solves it by |
|---|---|
| Inheritance = tight coupling | Composition (loose coupling) |
| No multiple inheritance | Delegate to multiple objects |
| Code duplication | Reuse a single delegate implementation |
Class Delegation
interface Printer { fun print() }
class ConsolePrinter : Printer { override fun print() = println("Printing...") }
class Document(printer: Printer) : Printer by printerProperty Delegation: lazy, observable, vetoable
val lazyValue: String by lazy {
println("Computing lazy value")
"Hello, Kotlin!"
}
fun main() {
println(lazyValue) // "Computing..." printed, then value
println(lazyValue) // cached, no recompute
}Computing lazy value
Hello, Kotlin!
Hello, Kotlin!Annotations
Metadata attached to code that the compiler or a runtime/annotation-processor can read, without changing the code's own logic.
@Deprecatedβ flags an API as outdated, with a replacement suggestion.@Targetβ restricts where a custom annotation can be applied.@JvmStaticβ exposes a companion-object member as a true static method for Java callers.@JvmOverloadsβ generates Java-callable overloads for a function with default parameters.@Volatileβ ensures a property's writes are immediately visible to other threads.
Reflection
Inspecting classes, functions, and properties at runtime via kotlin.reflect.
::classβ aKClassreference to a type at runtime.KClassβ exposes a class's properties/functions/constructors.KFunctionβ lets you inspect and dynamically invoke a function.
DSL (Domain-Specific Languages)
A small, expressive mini-language built with normal Kotlin syntax (lambdas with receivers) for a specific domain β configuration, UI building, scripting.
Example β a simple HTML DSLclass Body { private val children = mutableListOf()
fun p(text: String) { children.add("$text
") }
fun content() = children.joinToString("") }
class HTML { private val elements = mutableListOf()
fun body(block: Body.() -> Unit) {
val body = Body(); body.block()
elements.add("${body.content()}")
}
fun render() = "${elements.joinToString("")}" }
fun html(block: HTML.() -> Unit): HTML { val h = HTML(); h.block(); return h }
fun main() {
val page = html { body { p("Hello from Kotlin DSL!"); p("Second paragraph.") } }
println(page.render())
} <html><body><p>Hello from Kotlin DSL!</p><p>Second paragraph.</p></body></html>Real-life DSL examples
- Gradle Kotlin DSL β
android {},dependencies {} - Jetpack Compose β
Column {},Text() - Kotlinx HTML, SQL DSL libraries (Exposed)
Coroutines β the basics
Lightweight, non-blocking concurrency: coroutines can suspend without blocking the underlying thread, so thousands can run on a handful of OS threads.
Ek thread mein bahut saare tasks non-blocking tarike se chalte hain β CPU time save hota hai, app responsive rehta hai.
Suspend Functions
suspend marks a function that can pause (e.g. during delay or a network call) and resume later, without blocking the thread. Callable only from a coroutine or another suspend function.
suspend fun doLongTask() {
println("Task start hua")
delay(1000)
println("Task complete hua")
}Coroutine Builders
fun main() = runBlocking {
val deferred = async { delay(1000); "Result mil gaya" }
println("Wait kar raha hoon")
println(deferred.await())
}Wait kar raha hoon
Result mil gaya- launch β fire-and-forget, returns a
Job, no result. - async/await β returns a
Deferred<T>, use when you need a result. - runBlocking β blocks the current thread until its coroutine finishes (mainly for
main()/ tests β never use in production Android UI code).
viewModelScope, lifecycleScope) instead.Coroutine Context & Dispatchers
A CoroutineContext is an immutable, indexed set of elements defining how/where a coroutine runs β includes a Job, a Dispatcher, and optionally a CoroutineName.
- Dispatchers.Main β the UI thread; needed for touching Views.
- Dispatchers.IO β optimized, elastic pool for blocking I/O (network, disk, DB).
- Dispatchers.Default β CPU-bound work, sized to the number of cores (sorting, parsing, heavy computation).
- Dispatchers.Unconfined β starts on the caller's thread, resumes on whatever thread the suspension point returns on; mostly for internal/testing use.
suspend fun loadData() {
val data = withContext(Dispatchers.IO) { fetchFromNetwork() }
withContext(Dispatchers.Main) { updateUI(data) }
}withContext is a suspend function that switches the coroutine's dispatcher for a block, then switches back automatically.
Job, Cancellation & Structured Concurrency
A Job is a cancellable handle to a coroutine's lifecycle.
cancel()β requests cancellation (cooperative β the coroutine must checkisActiveor hit a suspension point to actually stop).join()β suspends the caller until the coroutine completes.isActiveβ whether the coroutine is still running.
Structured Concurrency
A parent coroutine scope doesn't complete until all its children complete, and cancelling the parent automatically cancels all children β this prevents leaked/orphaned coroutines.
fun main() = runBlocking {
val job = launch {
repeat(5) { i ->
if (!isActive) return@launch
println("Working: $i")
delay(500)
}
}
delay(1300)
job.cancel()
job.join()
println("Completed: ${!job.isActive}")
}Working: 0
Working: 1
Working: 2
Completed: trueFlow, StateFlow & SharedFlow
Flow represents an asynchronous stream of values, built on coroutines.
A water tap dripping slowly β each drop is a value. Nothing flows until you put your cup under it (collect).
- Cold stream β code only runs when a collector subscribes; each collector gets its own independent run.
- Hot stream β emits regardless of listeners (StateFlow, SharedFlow, Channel).
fun simpleFlow(): Flow = flow {
for (i in 1..5) { delay(100); emit(i) }
}
fun main() = runBlocking {
simpleFlow().collect { println("Received $it") }
} Received 1
Received 2
Received 3
Received 4
Received 5| Feature | StateFlow | SharedFlow |
|---|---|---|
| Holds a value? | Yes β always has a current value | No β pure event stream by default |
| Replay | Always replays the latest value | Configurable (0 or more) |
| Use case | UI state (loading, form data) | One-off events (toast, navigation) |
Flow Operators
mapβ transform;filterβ condition;catchβ handle upstream exceptions;onEachβ side effects like logging.
Room
Room is Jetpack's persistence library β an abstraction over SQLite with compile-time query validation, less boilerplate, and Coroutine/Flow/LiveData support.
| Component | Description | Annotation |
|---|---|---|
| Entity | Represents a table | @Entity |
| DAO | Contains SQL queries | @Dao |
| Database | Connects app to SQLite | @Database |
@Entity(tableName = "user")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String, val email: String
)
@Dao
interface UserDao {
@Insert suspend fun insertUser(user: User)
@Query("SELECT * FROM user") fun getAllUsers(): Flow>
@Delete suspend fun deleteUser(user: User)
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Singleton database instance
object RoomDB {
@Volatile private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext, AppDatabase::class.java, "my_database"
).build().also { INSTANCE = it }
}
}Migration
Safely updates the schema without wiping data.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0 NOT NULL")
}
}Dagger 2 & Hilt (Dependency Injection)
@Injectβ tells Dagger where to inject a dependency (constructor/field).@Moduleβ a class that provides dependencies.@Providesβ a method describing how to create an object.@Componentβ connects providers to consumers, builds the dependency graph.
Manual DI means creating and wiring objects yourself β messy at scale. Dagger builds a compile-time dependency graph, catching wiring errors at build time instead of runtime.
Hilt new
Hilt is Google's opinionated layer on top of Dagger, built specifically for Android. It removes most of Dagger's boilerplate (components, modules for Android classes) via annotations.
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()LiveData & Data Binding
LiveData is a lifecycle-aware observable holder β it only delivers updates to active (STARTED/RESUMED) observers, avoiding crashes/leaks from updating a destroyed UI.
- Lifecycle-aware β auto unsubscribes on destroy.
- Thread-safe writes from a background thread via
postValue()(vssetValue()on the main thread).
Data Binding
Binds UI components directly to data sources in XML layouts, removing findViewById boilerplate and keeping view logic declarative.
RxJava 2
A Reactive Extensions library for composing asynchronous, event-based programs using observable streams β predates widespread coroutine adoption, still common in legacy codebases.
Observable,Observer,subscribe(), Operators, Schedulers (thread management)
Observable observable = Observable.just(1, 2, 3);
observable.subscribe(new Observer() {
public void onSubscribe(Disposable d) { System.out.println("Subscribed!"); }
public void onNext(Integer n) { System.out.println("Received: " + n); }
public void onError(Throwable e) { System.out.println("Error: " + e.getMessage()); }
public void onComplete() { System.out.println("Completed!"); }
}); Subscribed!
Received: 1
Received: 2
Received: 3
Completed!Multi-Module apps
Splitting a big app into smaller, independently-buildable modules by responsibility β improves build times (parallel/incremental compilation), enforces boundaries, and enables code reuse.
- App Module β assembles everything into the final APK.
- Feature Module β a specific feature (login, profile).
- Library/Core Module β shared utilities, network, DI, base classes.
MyApp/
βββ app/
βββ core/
βββ network/
βββ feature_login/
βββ feature_profile/MVVM & ViewModel new
MVVM (Model-View-ViewModel) separates UI (View) from business/UI logic (ViewModel) and data (Model/Repository). The View observes state exposed by the ViewModel; it never talks to the Model directly.
- View (Activity/Fragment/Compose) β renders state, forwards user actions.
- ViewModel β holds UI state, survives configuration changes (rotation), exposes state via LiveData/StateFlow.
- Model/Repository β single source of truth, abstracts data sources (Room, network).
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _state = MutableStateFlow(UiState.Loading)
val state: StateFlow = _state
fun loadUser(id: Int) = viewModelScope.launch {
_state.value = UiState.Loading
_state.value = try {
UiState.Success(repo.getUser(id))
} catch (e: Exception) {
UiState.Error(e.message ?: "Unknown error")
}
}
} viewModelScope is automatically cancelled when the ViewModel is cleared β no manual cleanup needed.
Retrofit & networking new
Retrofit is a type-safe HTTP client for Android/Java β it turns an interface with annotations into a working REST API client, backed by OkHttp.
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): User
}
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(ApiService::class.java)A suspend Retrofit function runs on Dispatchers.IO internally and can be called directly from a coroutine, without callbacks.
Jetpack Compose basics new
Compose is Android's modern, declarative UI toolkit β you describe what the UI should look like for a given state, and Compose handles updating the actual UI when that state changes (instead of manually mutating Views).
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) { Text("Increment") }
}
}- @Composable β marks a function as UI-emitting.
- remember β keeps a value alive across recompositions.
- mutableStateOf β an observable value; changing it triggers recomposition of anything reading it.
- Recomposition β Compose re-runs only the composables that read changed state, not the whole tree.
Memory Leaks, ANR & Performance new
Memory leaks
Happens when an object (often an Activity/Fragment/View) is kept referenced longer than its intended lifecycle, so the garbage collector can't reclaim it.
- Common cause: a static field, singleton, or long-lived listener holding an Activity/Context reference.
- Common cause: a coroutine on
GlobalScopeor an unmanaged callback still holding a UI reference after the screen is destroyed. - Fix: use
applicationContextwhere a long-lived reference is needed, use lifecycle-scoped coroutines, and unregister listeners inonDestroy.
ANR (Application Not Responding)
Triggered when the main thread is blocked for too long (~5s for input events) β usually from heavy work (I/O, big loops, synchronous network calls) running on the main thread instead of a background dispatcher.
Dispatchers.IO/Default, keep the main thread only for UI updates via withContext(Dispatchers.Main).WorkManager & background work new
WorkManager is Jetpack's API for deferrable, guaranteed background work that should survive app restarts and device reboots (e.g. syncing data, uploading logs) β it picks the best underlying mechanism (JobScheduler, AlarmManager, etc.) for the OS version automatically.
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
return try {
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
val request = OneTimeWorkRequestBuilder()
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(context).enqueue(request)