Kotlin Advanced Features


1. Coroutines & Structured Concurrency

Q: What are Kotlin Coroutines and why are they important for Android?

Coroutines are lightweight, suspendable computations that allow you to write asynchronous code in a sequential, readable style. Unlike threads, coroutines are extremely cheap to create—you can launch thousands without significant overhead.

The key insight is that coroutines suspend rather than block. When a coroutine hits a suspension point (like a network call), it releases the thread to do other work, then resumes when the result is ready.

Q: Explain structured concurrency.

Structured concurrency means coroutines follow a parent-child hierarchy. When you launch a coroutine within a scope:

  • The parent waits for all children to complete
  • If the parent is cancelled, all children are cancelled
  • If a child fails, the parent (and siblings) are cancelled by default

In Android, viewModelScope and lifecycleScope provide built-in structured concurrency—coroutines are automatically cancelled when the ViewModel clears or the lifecycle ends, preventing memory leaks and wasted work.

Q: What are the different Dispatchers and when do you use each?

Dispatcher Thread Pool Use Case
Main Android main thread UI updates, light work
IO Shared pool (64+ threads) Network, database, file I/O
Default CPU cores Heavy computation, sorting, parsing
Unconfined Inherits caller's thread Testing only, avoid in production

Q: How do Dispatchers work under the hood?

Dispatchers are CoroutineContext elements that determine which thread(s) execute the coroutine. When you call withContext(Dispatchers.IO), the coroutine suspends, moves to an IO thread, executes the block, then resumes on the original dispatcher.

  • Main uses Android's main Looper—there's only one main thread
  • IO and Default share threads but have different policies. IO allows many concurrent operations (blocking calls), while Default is sized to CPU cores (compute-bound work)
  • IO can grow beyond 64 threads if needed, but reuses threads from Default when possible

Q: What is withContext vs switching dispatchers with launch?

withContext suspends the current coroutine and runs the block on another dispatcher, then returns the result. It's for sequential switching within a coroutine.

launch(Dispatchers.IO) creates a new child coroutine on that dispatcher. Use this for parallel/concurrent work.

Q: What's the difference between launch and async?

  • launch: Fire-and-forget. Returns a Job. Use when you don't need a result.
  • async: Returns a Deferred<T>. Use when you need to compute a value, especially for parallel operations where you await() multiple results.

Q: How does exception handling work in coroutines?

Regular launch propagates exceptions to the parent, which cancels siblings. Use SupervisorJob when you want child failures to be independent—one child's exception won't cancel others.

For async, exceptions are thrown when you call await(), so wrap that in try-catch.


2. Flow & Reactive Streams

Q: What is Kotlin Flow?

Flow is a cold asynchronous stream that emits multiple values over time. "Cold" means it doesn't produce values until someone collects it, and each collector gets its own independent stream.

Q: Compare Flow, StateFlow, and SharedFlow.

Type Hot/Cold Initial Value Replay Use Case
Flow Cold No No One-shot data, transformations
StateFlow Hot Required Latest value UI state, always has current value
SharedFlow Hot Optional Configurable Events, can have multiple subscribers

Q: What are the key Flow operators?

  • map/filter: Transform or filter emissions
  • flatMapLatest: Cancel previous work when new value arrives (great for search)
  • combine: Merge multiple flows, emit when any changes
  • debounce: Wait for pause in emissions (search input)
  • catch: Handle upstream errors
  • flowOn: Change dispatcher for upstream operations
  • stateIn/shareIn: Convert cold Flow to hot StateFlow/SharedFlow

Q: How does backpressure work in Flow?

Backpressure occurs when the producer emits faster than the consumer can process. Flow handles this naturally because it's sequential by default—the producer suspends until the collector processes each emission.

For cases where you need different behavior:

  • buffer(): Runs collector in separate coroutine, emissions don't wait for processing
  • conflate(): Skip intermediate values, only process the latest when collector is ready
  • collectLatest(): Cancel previous collection when new value arrives (similar to flatMapLatest)

Use buffer() when you want to decouple producer/consumer speeds. Use conflate() or collectLatest() when only the latest value matters (UI updates, search queries).

Q: How do you safely collect Flow in Android?

Use repeatOnLifecycle or flowWithLifecycle to collect only when the UI is visible. This prevents wasted work when the app is in the background and avoids crashes from updating views after the Activity is destroyed.


3. Generics & Variance

Q: Explain variance in Kotlin generics.

Variance defines how generic types with inheritance relate to each other.

Invariant (default): Box<Dog> is NOT a subtype of Box<Animal>, even though Dog extends Animal. This is safe because the box could accept writes.

Covariant (out): The type can only be produced/returned, never consumed. List<out T> means you can read items but not add them. A List<Dog> IS a subtype of List<Animal>.

Contravariant (in): The type can only be consumed, never produced. Comparable<in T> means you can pass items in but not get them out. A Comparator<Animal> IS a subtype of Comparator<Dog>.

Q: How do you remember in vs out?

  • out = output position only (return types) → Producer
  • in = input position only (parameter types) → Consumer

Think "PECS" from Java: Producer Extends, Consumer Super.


4. Sealed Classes vs Enums

Q: When should you use sealed classes instead of enums?

Enums are perfect for fixed sets of constants with no varying data—like days of the week or simple status codes.

Sealed classes are better when:

  • Different states carry different data (Success has data, Error has message)
  • You need inheritance hierarchies
  • Subtypes are data classes or objects with properties

The compiler enforces exhaustive when expressions for both, but sealed classes let each subtype have its own structure.

Q: What about sealed interfaces?

Sealed interfaces (Kotlin 1.5+) allow a class to implement multiple sealed hierarchies, which sealed classes can't do. Use them when you need more flexible type modeling.


5. Inline Functions & Reified Types

Q: What does inline do?

The compiler copies the function body directly to every call site, eliminating the function call overhead and lambda object allocation. This matters for higher-order functions called frequently.

Q: What is reified and why is it useful?

Normally, generic type parameters are erased at runtime—you can't access T::class. The reified keyword (only works with inline functions) preserves the type information, letting you:

  • Get the class: T::class.java
  • Check types: value is T
  • Create instances or pass to reflection APIs

Common uses: JSON parsing (gson.fromJson<User>(json)), ViewModel creation, intent extras.

Q: What are the downsides of inline?

  • Increases bytecode size if overused
  • Can't be used with recursive functions
  • Public inline functions can't access private members

6. Extension Functions

Q: How do extension functions work internally?

They're compiled as static methods where the receiver becomes the first parameter. fun String.isEmail() becomes static boolean isEmail(String $this). They're resolved statically at compile time, not dynamically.

Q: What can't extension functions do?

  • Access private/protected members of the class
  • Override existing member functions (members always win)
  • Be truly polymorphic (static dispatch, not virtual)

Q: Explain the scope functions (let, apply, run, also, with).

Function Returns Receiver Access Use Case
let Lambda result it Null checks, transformations
apply Receiver object this Object configuration
run Lambda result this Execute block, compute result
also Receiver object it Side effects, logging
with Lambda result this Multiple calls on same object

7. Data Classes

Q: What does declaring a data class give you automatically?

  • equals() and hashCode() based on constructor properties
  • toString() with property names and values
  • copy() for creating modified copies
  • componentN() functions for destructuring

Q: What are the limitations?

  • Must have at least one primary constructor parameter
  • All constructor parameters must be val or var
  • Cannot be abstract, open, sealed, or inner
  • Only primary constructor properties are used in generated methods

Q: Best practices for data classes?

  • Prefer val for immutability
  • Avoid mutable collections as properties
  • Be careful with properties declared in the body—they're excluded from equals/hashCode
  • Consider using copy() instead of mutable properties

Quick Reference

Topic Key Points
Coroutines Suspend don't block; structured concurrency with scopes; Main/IO/Default dispatchers
Flow Cold streams; StateFlow for state; SharedFlow for events; flatMapLatest for search
Variance out = producer (covariant); in = consumer (contravariant)
Sealed Classes States with data; exhaustive when; better than enum for complex cases
Inline/Reified Inline copies body; reified preserves generic type at runtime
Extensions Static dispatch; can't access private; scope functions for fluent code
Data Classes Auto equals/hashCode/copy/toString; immutable preferred

results matching ""

    No results matching ""