1. Setup
Objective
Set up the development environment to write and run Kotlin code for Android applications.
Steps
- Install Java Development Kit (JDK):
- Configure Kotlin in Android Studio:
- Create a new Android project and select Kotlin as the programming language.
- Alternatively, add Kotlin support to an existing project by adding the Kotlin plugin in build.gradle:
gradle// Project-level build.gradlebuildscript {dependencies {classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0"}}// App-level build.gradleplugins {id 'kotlin-android'}
- Set Up an Emulator or Physical Device:
- Configure an Android Virtual Device (AVD) in Android Studio or connect a physical Android device for testing.
Key Notes
- Ensure your system meets Android Studio’s requirements (e.g., 8GB RAM minimum, 16GB recommended).
- Keep the Kotlin plugin updated in Android Studio for the latest features and bug fixes.
2. Basics of Kotlin
Objective
Understand Kotlin’s fundamental concepts, syntax, and features, with a focus on Android development.
2.1 Data Types
Primitive (Built-in) Data Types
Kotlin provides the following primitive types, which are similar to Java but optimized for null safety:
- Byte: 8-bit signed integer (-128 to 127)
- Short: 16-bit signed integer (-32768 to 32767)
- Int: 32-bit signed integer (-2^31 to 2^31-1)
- Long: 64-bit signed integer (-2^63 to 2^63-1)
- Float: 32-bit floating-point number
- Double: 64-bit floating-point number
- Char: 16-bit Unicode character
- Boolean: true or false
Non-Primitive (User-Generated) Data Types
- String: Immutable sequence of characters
- Array: Fixed-size collection of elements
- List, Set, Map: Kotlin collection types
- Classes and Objects: User-defined types
Code Example
val myInt: Int = 42
val myDouble: Double = 3.14
val myChar: Char = 'A'
val myString: String = "Hello, Kotlin!"
val myArray: Array<Int> = arrayOf(1, 2, 3)
val myList: List<String> = listOf("Apple", "Banana", "Orange")2.2 Variables
Kotlin uses val and var for variable declarations, promoting immutability and type inference.
- val: Immutable (read-only) variable, cannot be reassigned after initialization.
- var: Mutable variable, can be reassigned.
- Type Inference: Kotlin infers the type if not explicitly specified.
- Local Variables: Declared within a function or block, scoped to that block.
- Instance Variables: Declared inside a class, associated with an instance.
- No Static Variables: Use companion object for static-like behavior.
Code Example
class Example {
var instanceVar: String = "I am an instance variable"
companion object {
val staticLikeVar: String = "I am like a static variable"
}
}
fun main() {
val immutable = 10 // Cannot be reassigned
var mutable = 20
mutable = 30 // Valid
// immutable = 15 // Error: Val cannot be reassigned
println(Example.staticLikeVar) // Accessing companion object variable
}2.3 Null Safety
Kotlin eliminates null pointer exceptions (NPEs) by design through its type system.
- Nullable Types: Use ? to declare a variable that can hold null.
- Safe Call Operator (?. ): Access properties or methods only if the object is non-null.
- Elvis Operator (?: ): Provide a default value if the expression is null.
- Non-null Assertion Operator (!!): Assert that a value is non-null (use cautiously, as it can throw NPE).
Code Example
fun main() {
val nullableString: String? = null
// Safe Call Operator
println(nullableString?.length) // Prints: null
// Elvis Operator
val length = nullableString?.length ?: 0
println(length) // Prints: 0
// Non-null Assertion Operator (use with caution)
val nonNullString: String = "Kotlin"
println(nonNullString!!.length) // Prints: 6
}2.4 Control Flow
If-Else Statement
Kotlin’s if-else can be used as an expression, returning a value.
val max = if (a > b) a else bWhen Statement
Similar to Java’s switch, but more powerful. It supports ranges, multiple conditions, and can be used as an expression.
fun describe(number: Int): String {
return when (number) {
1 -> "One"
in 2..5 -> "Between 2 and 5"
else -> "Other"
}
}Loops
- For Loop: Iterates over ranges, collections, or anything with an iterator.
- While Loop: Executes as long as a condition is true.
- Do-While Loop: Executes at least once, then checks the condition.
- Break and Continue with Labeled Loops: Use labels to control flow in nested loops.
Code Example
fun main() {
// For Loop
for (i in 1..5) {
println(i) // Prints: 1, 2, 3, 4, 5
}
// While Loop
var i = 0
while (i < 3) {
println(i) // Prints: 0, 1, 2
i++
}
// Labeled Loop
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) break@outer
println("i: $i, j: $j")
}
}
}2.5 Operators
Unary Operators
- +, -, !, ++, --
Binary Operators
- Arithmetic: +, -, *, /, %
- Comparison: ==, !=, <, >, <=, >=
- Logical: &&, ||
- Assignment: =, +=, -=, *=, /=, %=
Code Example
fun main() {
val a = 10
val b = 5
println(a + b) // Prints: 15
println(a > b) // Prints: true
println(a == b) // Prints: false
}2.6 Methods (Functions)
Function as Expression
Functions can return values directly, making them concise.
fun square(num: Int): Int = num * numNamed Parameters, Default Parameters, and Vararg
- Named Parameters: Improve readability by specifying parameter names.
- Default Parameters: Provide default values for parameters.
- Vararg: Allow a variable number of arguments.
Code Example
fun greet(name: String = "Guest", prefix: String = "Hello"): String {
return "$prefix, $name!"
}
fun sum(vararg numbers: Int): Int {
return numbers.sum()
}
fun main() {
println(greet(name = "Alice", prefix = "Hi")) // Prints: Hi, Alice!
println(greet()) // Prints: Hello, Guest!
println(sum(1, 2, 3, 4)) // Prints: 10
}Lambda and Higher-Order Functions
- Lambda: Anonymous functions defined using {}.
- Higher-Order Functions: Functions that take other functions as parameters or return them.
Code Example
fun main() {
val lambda: (Int) -> Int = { x -> x * 2 }
println(lambda(5)) // Prints: 10
fun operateOnNumber(num: Int, operation: (Int) -> Int): Int {
return operation(num)
}
println(operateOnNumber(5, lambda)) // Prints: 10
}Extension Functions
Allow adding functionality to existing classes without modifying their source code.
Code Example
fun String.addExclamation(): String {
return "$this!"
}
fun main() {
println("Hello".addExclamation()) // Prints: Hello!
}3. Practical Use Cases for Android Development
Use Case 1: Android UI with Kotlin
Use Kotlin to handle button clicks and update UI elements.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button: Button = findViewById(R.id.myButton)
val textView: TextView = findViewById(R.id.myTextView)
button.setOnClickListener {
textView.text = "Button Clicked!"
}
}
}Use Case 2: Null Safety in Android
Handle nullable views safely to prevent crashes.
class MainActivity : AppCompatActivity() {
private var textView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.myTextView)
textView?.text = "Safe Text Update"
}
}Use Case 3: Using Collections
Filter and manipulate data for a RecyclerView.
val fruits = listOf("Apple", "Banana", "Orange")
val filtered = fruits.filter { it.startsWith("A") }
println(filtered) // Prints: [Apple]3.1 Object
### Definition
An **object** is an instance of a class, representing a specific entity with attributes (properties) and behaviors (methods). In Kotlin, objects are created from classes and can also be declared as singleton instances using the `object` keyword.
### Use Case
- In Android, objects are used to represent UI elements (e.g., a `Button` object) or data entities (e.g., a `User` object).
### Code Example
```kotlin
class User(val name: String, val age: Int)
fun main() {
val user = User("Alice", 25) // Object creation
println("Name: ${user.name}, Age: ${user.age}") // Prints: Name: Alice, Age: 25
}3.2 Class
Definition
A class is a blueprint for creating objects, defining their properties and methods. In Kotlin, classes are concise and support features like primary constructors and default visibility.
Use Case
- Classes in Android are used to define data models (e.g., User) or UI components (e.g., Activity).
Code Example
class Car {
var brand: String = ""
var speed: Int = 0
fun accelerate() {
speed += 10
}
}
fun main() {
val car = Car()
car.brand = "Toyota"
car.accelerate()
println("Brand: ${car.brand}, Speed: ${car.speed}") // Prints: Brand: Toyota, Speed: 10
}3.3 Constructor
Definition
A constructor is a special function used to initialize objects of a class. Kotlin supports two types:
- Primary Constructor: Defined in the class header, used for initializing properties directly.
- Secondary Constructor: Defined inside the class body using the constructor keyword, allowing additional initialization logic.
Use Case
- In Android, constructors are used to initialize data models or configure UI components with initial values.
Code Example
class Person(val name: String, var age: Int) { // Primary constructor
var address: String = ""
// Secondary constructor
constructor(name: String, age: Int, address: String) : this(name, age) {
this.address = address
}
fun display() {
println("Name: $name, Age: $age, Address: $address")
}
}
fun main() {
val person1 = Person("Bob", 30)
val person2 = Person("Alice", 25, "123 Main St")
person1.display() // Prints: Name: Bob, Age: 30, Address:
person2.display() // Prints: Name: Alice, Age: 25, Address: 123 Main St
}3.4 Inheritance
Definition
Inheritance allows a class (subclass) to inherit properties and methods from another class (superclass). In Kotlin, classes are final by default (cannot be inherited). Use the open keyword to allow inheritance.
Use Case
- In Android, inheritance is used when extending base classes like AppCompatActivity for activities or Fragment for UI components.
Code Example
open class Animal(val name: String) {
open fun makeSound() {
println("$name makes a sound")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("$name barks")
}
}
fun main() {
val dog = Dog("Rex")
dog.makeSound() // Prints: Rex barks
}3.5 Polymorphism
Definition
Polymorphism allows objects of different classes to be treated as objects of a common superclass. Kotlin supports:
- Overloading: Defining multiple functions with the same name but different parameters.
- Overriding: Redefining a superclass’s method in a subclass.
Use Case
- In Android, polymorphism is used to handle different UI components (e.g., View subclasses like Button, TextView) uniformly or to customize behavior in subclasses.
3.5.1 Overloading
Definition
Method overloading allows multiple methods with the same name but different parameter lists (number, type, or order).
Code Example
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
fun add(a: Double, b: Double): Double {
return a + b
}
fun add(a: Int, b: Int, c: Int): Int {
return a + b + c
}
}
fun main() {
val calc = Calculator()
println(calc.add(2, 3)) // Prints: 5
println(calc.add(2.5, 3.5)) // Prints: 6.0
println(calc.add(1, 2, 3)) // Prints: 6
}3.5.2 Overriding
Definition
Method overriding allows a subclass to provide a specific implementation of a method defined in its superclass. The superclass method must be marked open, and the subclass method must use override.
Code Example
open class Vehicle {
open fun start() {
println("Vehicle started")
}
}
class Car : Vehicle() {
override fun start() {
println("Car started with key")
}
}
fun main() {
val vehicle: Vehicle = Car()
vehicle.start() // Prints: Car started with key
}3.6 Abstraction
Definition
Abstraction hides implementation details and exposes only the necessary functionality. In Kotlin, abstraction is achieved using:
- Abstract Classes: Cannot be instantiated; used to define common behavior.
- Interfaces: Define contracts that classes can implement, supporting multiple inheritance.
Use Case
- In Android, abstraction is used to define reusable components (e.g., an interface for click listeners) or abstract base classes for activities.
Code Example
interface Clickable {
fun onClick()
}
abstract class Shape {
abstract fun draw()
}
class Button : Shape(), Clickable {
override fun draw() {
println("Drawing a button")
}
override fun onClick() {
println("Button clicked")
}
}
fun main() {
val button = Button()
button.draw() // Prints: Drawing a button
button.onClick() // Prints: Button clicked
}3.7 Access Modifiers
Definition
Access modifiers control the visibility of classes, properties, and methods. Kotlin provides:
- private: Visible only within the same class or file.
- protected: Visible within the same class and its subclasses.
- public: Visible everywhere (default in Kotlin).
- internal: Visible within the same module (a set of Kotlin files compiled together).
Use Case
- In Android, access modifiers ensure encapsulation, such as making view references private in an Activity or using internal for module-specific utilities.
Code Example
class Example {
private val privateVar = "Private"
protected val protectedVar = "Protected"
internal val internalVar = "Internal"
val publicVar = "Public"
}
class SubExample : Example() {
fun display() {
println(protectedVar) // Accessible
println(publicVar) // Accessible
// println(privateVar) // Error: privateVar is not accessible
}
}
fun main() {
val example = Example()
println(example.publicVar) // Prints: Public
println(example.internalVar) // Prints: Internal
// println(example.privateVar) // Error: privateVar is not accessible
}3.8 Data Class
Definition
A data class is a special class designed to hold data, automatically providing implementations for toString(), equals(), hashCode(), and copy(). It is defined using the data keyword.
Use Case
- In Android, data classes are used for data models, such as API responses or database entities, simplifying data manipulation.
Code Example
data class User(val id: Int, val name: String, val email: String)
fun main() {
val user1 = User(1, "Alice", "alice@example.com")
val user2 = user1.copy(email = "alice@new.com")
println(user1) // Prints: User(id=1, name=Alice, email=alice@example.com)
println(user2) // Prints: User(id=1, name=Alice, email=alice@new.com)
println(user1 == user2) // Prints: false
}4. Practical Use Cases for Android Development
Use Case 1: Data Class for API Response
Use a data class to model a JSON response from an API.
data class Product(val id: Int, val name: String, val price: Double)
class ProductActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_product)
val product = Product(1, "Laptop", 999.99)
val textView: TextView = findViewById(R.id.productTextView)
textView.text = product.toString()
}
}Use Case 2: Inheritance and Polymorphism in Android
Extend AppCompatActivity and override lifecycle methods.
open class BaseActivity : AppCompatActivity() {
open fun setupUI() {
// Default implementation
}
}
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
}
override fun setupUI() {
val textView: TextView = findViewById(R.id.textView)
textView.text = "MainActivity UI"
}
}Use Case 3: Interface for Click Handling
Define a reusable click listener interface.
interface OnItemClickListener {
fun onItemClick(item: String)
}
class ItemAdapter(private val listener: OnItemClickListener) {
fun onItemSelected(item: String) {
listener.onItemClick(item)
}
}
class MainActivity : AppCompatActivity(), OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ItemAdapter(this)
adapter.onItemSelected("Item 1")
}
override fun onItemClick(item: String) {
Toast.makeText(this, "Clicked: $item", Toast.LENGTH_SHORT).show()
}
}5. Interview Preparation Tips
- Explain OOP Concepts Clearly:
- Define each concept (e.g., inheritance, polymorphism) and provide Kotlin-specific examples.
- Highlight differences from Java (e.g., open keyword, data classes).
- Demonstrate Android Relevance:
- Show how OOP is applied in Android (e.g., extending Activity, using interfaces for event handling).
- Null Safety Integration:
- Explain how Kotlin’s null safety complements OOP (e.g., using nullable types in data classes).
- Practical Scenarios:
- Be prepared to write code for common Android tasks, such as creating a data class for a REST API response or implementing a custom View with polymorphism.
- Common Questions:
- Difference between open and final classes.
- When to use a data class vs. a regular class.
- How to implement multiple inheritance using interfaces.
4. Kotlin Collections
Kotlin provides a powerful and flexible collection framework for handling groups of data. Collections in Kotlin are divided into **immutable** (read-only) and **mutable** (modifiable) types. This section covers the main collection types (`List`, `Set`, `Map`) and common operations (`all`, `any`, `count`, `find`, `filter`, `map`), with a focus on their use in Android development.
---
## 4.1 List and MutableList
### Definition
- **List**: An immutable, ordered collection of elements that allows duplicates. Use `listOf()` to create.
- **MutableList**: A mutable version of `List`, allowing additions, removals, and modifications. Use `mutableListOf()` to create.
### Key Features
- Elements are accessed by index (0-based).
- Maintains insertion order.
- Suitable for ordered data, such as a list of items in a RecyclerView.
### Use Case
- In Android, `List` is used to store static data (e.g., predefined menu items), while `MutableList` is used for dynamic data (e.g., a list of tasks in a to-do app).
### Code Example
```kotlin
fun main() {
// Immutable List
val fruits: List<String> = listOf("Apple", "Banana", "Orange")
println(fruits[1]) // Prints: Banana
// fruits.add("Mango") // Error: Immutable list
// Mutable List
val mutableFruits: MutableList<String> = mutableListOf("Apple", "Banana")
mutableFruits.add("Mango")
mutableFruits[1] = "Grape"
println(mutableFruits) // Prints: [Apple, Grape, Mango]
}Android Example
Populating a RecyclerView with a MutableList.
class MainActivity : AppCompatActivity() {
private val items: MutableList<String> = mutableListOf("Item 1", "Item 2")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
recyclerView.adapter = ItemAdapter(items)
recyclerView.layoutManager = LinearLayoutManager(this)
// Add item dynamically
val button: Button = findViewById(R.id.addButton)
button.setOnClickListener {
items.add("Item ${items.size + 1}")
recyclerView.adapter?.notifyDataSetChanged()
}
}
}4.2 Set and MutableSet
Definition
- Set: An immutable, unordered collection of unique elements. Use setOf() to create.
- MutableSet: A mutable version of Set, allowing additions and removals. Use mutableSetOf() to create.
Key Features
- No duplicate elements (duplicates are automatically removed).
- No index-based access due to unordered nature.
- Ideal for scenarios requiring unique items, such as a collection of unique tags.
Use Case
- In Android, Set is used for unique data (e.g., unique user IDs), while MutableSet is used when items need to be added or removed dynamically (e.g., selected filter options).
Code Example
fun main() {
// Immutable Set
val colors: Set<String> = setOf("Red", "Blue", "Red") // Duplicate "Red" ignored
println(colors) // Prints: [Red, Blue]
// colors.add("Green") // Error: Immutable set
// Mutable Set
val mutableColors: MutableSet<String> = mutableSetOf("Red", "Blue")
mutableColors.add("Green")
mutableColors.remove("Red")
println(mutableColors) // Prints: [Blue, Green]
}Android Example
Managing unique tags in a filter system.
class FilterActivity : AppCompatActivity() {
private val selectedTags: MutableSet<String> = mutableSetOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter)
val addTagButton: Button = findViewById(R.id.addTagButton)
val tagInput: EditText = findViewById(R.id.tagInput)
val tagTextView: TextView = findViewById(R.id.tagTextView)
addTagButton.setOnClickListener {
val tag = tagInput.text.toString()
if (tag.isNotEmpty()) {
selectedTags.add(tag)
tagTextView.text = "Tags: $selectedTags"
tagInput.text.clear()
}
}
}
}4.3 Map and MutableMap
Definition
- Map: An immutable collection of key-value pairs, where keys are unique. Use mapOf() to create.
- MutableMap: A mutable version of Map, allowing modifications to key-value pairs. Use mutableMapOf() to create.
Key Features
- Keys are unique; values can be duplicated.
- Access values using keys (not indices).
- Suitable for key-based data, such as user preferences or configuration settings.
Use Case
- In Android, Map is used for static configurations (e.g., settings), while MutableMap is used for dynamic key-value data (e.g., form input data).
Code Example
fun main() {
// Immutable Map
val userInfo: Map<String, String> = mapOf("name" to "Alice", "email" to "alice@example.com")
println(userInfo["name"]) // Prints: Alice
// userInfo["name"] = "Bob" // Error: Immutable map
// Mutable Map
val mutableUserInfo: MutableMap<String, String> = mutableMapOf("name" to "Bob")
mutableUserInfo["email"] = "bob@example.com"
mutableUserInfo.remove("name")
println(mutableUserInfo) // Prints: {email=bob@example.com}
}Android Example
Storing form data in a MutableMap.
class FormActivity : AppCompatActivity() {
private val formData: MutableMap<String, String> = mutableMapOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_form)
val nameInput: EditText = findViewById(R.id.nameInput)
val emailInput: EditText = findViewById(R.id.emailInput)
val submitButton: Button = findViewById(R.id.submitButton)
val resultTextView: TextView = findViewById(R.id.resultTextView)
submitButton.setOnClickListener {
formData["name"] = nameInput.text.toString()
formData["email"] = emailInput.text.toString()
resultTextView.text = "Form Data: $formData"
}
}
}4.4 Predicates: all, any, count, find
Definition
Predicates are functions that test elements in a collection and return a boolean or a count. Common predicates include:
- all: Returns true if all elements satisfy the condition.
- any: Returns true if at least one element satisfies the condition.
- count: Returns the number of elements that satisfy the condition.
- find: Returns the first element that satisfies the condition or null if none found.
Use Case
- In Android, predicates are used to validate data (e.g., checking if all form fields are filled) or to search collections (e.g., finding a specific item in a list).
Code Example
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// all: Check if all numbers are positive
val allPositive = numbers.all { it > 0 }
println(allPositive) // Prints: true
// any: Check if any number is even
val hasEven = numbers.any { it % 2 == 0 }
println(hasEven) // Prints: true
// count: Count numbers greater than 3
val countGreaterThanThree = numbers.count { it > 3 }
println(countGreaterThanThree) // Prints: 2
// find: Find the first number divisible by 3
val firstDivisibleByThree = numbers.find { it % 3 == 0 }
println(firstDivisibleByThree) // Prints: 3
}Android Example
Validating a list of form inputs.
class ValidationActivity : AppCompatActivity() {
private val inputs: List<String> = listOf("Alice", "alice@example.com", "")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_validation)
val resultTextView: TextView = findViewById(R.id.resultTextView)
// Check if all inputs are non-empty
val allValid = inputs.all { it.isNotEmpty() }
resultTextView.text = if (allValid) "All inputs valid" else "Some inputs empty"
}
}4.5 filter and map
Definition
- filter: Creates a new collection containing only elements that satisfy a condition.
- map: Transforms each element in a collection into a new value, creating a new collection.
Use Case
- In Android, filter is used to display a subset of data (e.g., filtering a list of products by category), and map is used to transform data (e.g., extracting names from a list of users).
Code Example
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// filter: Get even numbers
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // Prints: [2, 4]
// map: Square each number
val squaredNumbers = numbers.map { it * it }
println(squaredNumbers) // Prints: [1, 4, 9, 16, 25]
// Combined: Filter even numbers and square them
val evenSquared = numbers.filter { it % 2 == 0 }.map { it * it }
println(evenSquared) // Prints: [4, 16]
}Android Example
Filtering and mapping data for a RecyclerView.
data class Product(val id: Int, val name: String, val category: String)
class ProductActivity : AppCompatActivity() {
private val products = listOf(
Product(1, "Laptop", "Electronics"),
Product(2, "Shirt", "Clothing"),
Product(3, "Phone", "Electronics")
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_product)
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
val electronics = products.filter { it.category == "Electronics" }
.map { it.name }
recyclerView.adapter = ItemAdapter(electronics)
recyclerView.layoutManager = LinearLayoutManager(this)
}
}5. Interview Preparation Tips
- Understand Immutable vs. Mutable Collections:
- Explain the difference between List and MutableList, Set and MutableSet, Map and MutableMap.
- Highlight when to use immutable collections for safety and mutable collections for flexibility.
- Master Collection Operations:
- Be prepared to write code using filter, map, all, any, count, and find.
- Explain how these operations improve code readability and efficiency.
- Android-Specific Applications:
- Demonstrate how collections are used in Android (e.g., populating a RecyclerView with a filtered List or storing user preferences in a Map).
- Performance Considerations:
- Discuss the performance implications of large collections (e.g., Set for unique elements is faster than checking duplicates in a List).
- Common Questions:
- How do you filter a list of objects in Kotlin?
- What’s the difference between find and firstOrNull?
- How would you use a MutableMap to store form data in an Android app?
5.1 Kotlin Coroutines
### Definition
Kotlin **Coroutines** are a concurrency design pattern used to simplify asynchronous programming, such as network calls or database operations, by allowing code to run in a non-blocking manner. They provide a way to write asynchronous code that looks sequential.
### Key Features
- **Suspend Functions**: Functions marked with `suspend` can pause execution without blocking the thread.
- **Coroutine Scope**: Defines the lifecycle of coroutines (e.g., `GlobalScope`, `viewModelScope` in Android).
- **Dispatchers**: Control the thread where coroutines run (e.g., `Dispatchers.Main` for UI, `Dispatchers.IO` for I/O operations).
### Use Case
- In Android, coroutines are used for tasks like fetching data from an API, updating the UI, or performing database operations without freezing the app.
### Code Example
```kotlin
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
// Launch coroutine in Main scope
CoroutineScope(Dispatchers.Main).launch {
val result = fetchData() // Suspend function
textView.text = result
}
}
private suspend fun fetchData(): String {
// Simulate network call on IO thread
return withContext(Dispatchers.IO) {
delay(1000) // Simulate delay
"Data fetched!"
}
}
}Interview Tip
- Explain the difference between launch and async, and when to use Dispatchers.Main vs. Dispatchers.IO.
- Highlight how coroutines replace older Android patterns like AsyncTask.
5.2 Java Interoperability
Definition
Kotlin is fully interoperable with Java, allowing seamless use of Java libraries and classes in Kotlin code and vice versa. Kotlin code compiles to JVM bytecode, making it compatible with Java-based Android APIs.
Key Features
- Call Java methods from Kotlin and Kotlin functions from Java.
- Kotlin’s null safety works with Java’s nullable types using annotations (e.g., @Nullable, @NotNull).
- Kotlin properties can be accessed as getters/setters in Java.
Use Case
- In Android, Java interoperability allows using Android’s Java-based APIs (e.g., Activity, View) or third-party Java libraries in Kotlin projects.
Code Example
// Kotlin class
class KotlinClass {
fun greet(name: String): String {
return "Hello, $name!"
}
}
// Java class calling Kotlin
public class JavaClass {
public static void main(String[] args) {
KotlinClass kotlinClass = new KotlinClass();
System.out.println(kotlinClass.greet("Alice")); // Prints: Hello, Alice!
}
}Android Example
Using Java-based Android API in Kotlin.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Using Java-based Toast API
Toast.makeText(this, "Welcome!", Toast.LENGTH_SHORT).show()
}
}Interview Tip
- Explain how Kotlin handles Java’s lack of null safety (e.g., using platform types like String!).
- Discuss converting a Java Android project to Kotlin incrementally.
5.3 Sealed Class
Definition
A sealed class is a restricted class hierarchy where all subclasses must be defined within the same file. It is used to represent a fixed set of types, often in conjunction with when expressions for type-safe handling.
Use Case
- In Android, sealed classes are used to model UI states (e.g., loading, success, error) or API response types.
Code Example
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
fun main() {
handleResult(Result.Success("User data"))
handleResult(Result.Error("Failed to load"))
handleResult(Result.Loading)
}Android Example
Managing UI state in an Activity.
sealed class UiState {
data class Success(val data: String) : UiState()
data class Error(val message: String) : UiState()
object Loading : UiState()
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
val state: UiState = UiState.Success("Profile loaded")
when (state) {
is UiState.Success -> textView.text = state.data
is UiState.Error -> textView.text = state.message
UiState.Loading -> textView.text = "Loading..."
}
}
}Interview Tip
- Explain why sealed classes are preferred over enums for complex type hierarchies.
- Discuss their use with when for exhaustive type checking.
5.4 Enums
Definition
An enum class defines a fixed set of constants, each representing an instance of the enum class. Enums can have properties and methods.
Use Case
- In Android, enums are used for fixed categories, such as user roles, screen orientations, or button states.
Code Example
enum class Direction(val degrees: Int) {
NORTH(0),
EAST(90),
SOUTH(180),
WEST(270);
fun getDescription(): String {
return "$name is at $degrees degrees"
}
}
fun main() {
val direction = Direction.NORTH
println(direction.getDescription()) // Prints: NORTH is at 0 degrees
}Android Example
Using enums for button states.
enum class ButtonState {
ENABLED, DISABLED
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button: Button = findViewById(R.id.button)
val state = ButtonState.DISABLED
button.isEnabled = state == ButtonState.ENABLED
}
}Interview Tip
- Compare enums and sealed classes (e.g., enums for simple constants, sealed classes for complex hierarchies).
5.5 Exception Handling
Definition
Exception handling in Kotlin uses try, catch, and finally blocks to handle runtime errors gracefully. Kotlin does not have checked exceptions, unlike Java.
Use Case
- In Android, exception handling is used for network failures, file operations, or invalid user inputs.
Code Example
fun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("Error: ${e.message}")
0
} finally {
println("Operation completed")
}
}
fun main() {
println(divide(10, 2)) // Prints: 5, Operation completed
println(divide(10, 0)) // Prints: Error: / by zero, 0, Operation completed
}Android Example
Handling network call exceptions.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
CoroutineScope(Dispatchers.Main).launch {
try {
val data = fetchData()
textView.text = data
} catch (e: Exception) {
textView.text = "Error: ${e.message}"
}
}
}
private suspend fun fetchData(): String = withContext(Dispatchers.IO) {
throw Exception("Network failure") // Simulated error
}
}Interview Tip
- Explain the absence of checked exceptions in Kotlin and its benefits in Android.
- Discuss combining coroutines with exception handling.
5.6 Companion Object
Definition
A companion object is a singleton object declared inside a class using the companion object keyword. It is used to define static-like members (properties or methods) in Kotlin, as there are no static members.
Use Case
- In Android, companion objects are used for constants, utility functions, or factory methods (e.g., creating an Intent).
Code Example
class DatabaseHelper {
companion object {
const val DATABASE_NAME = "app.db"
fun getInstance(): DatabaseHelper {
return DatabaseHelper()
}
}
}
fun main() {
println(DatabaseHelper.DATABASE_NAME) // Prints: app.db
val db = DatabaseHelper.getInstance()
}Android Example
Using companion object for Intent creation.
class MainActivity : AppCompatActivity() {
companion object {
fun createIntent(context: Context): Intent {
return Intent(context, MainActivity::class.java)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
// Starting activity from another context
fun startMainActivity(context: Context) {
context.startActivity(MainActivity.createIntent(context))
}Interview Tip
- Explain why Kotlin uses companion objects instead of static.
- Discuss their use in Android for utility methods or constants.
5.7 Interfaces
Definition
An interface defines a contract of methods and properties that implementing classes must provide. Kotlin interfaces can have default implementations and support multiple inheritance.
Use Case
- In Android, interfaces are used for event listeners (e.g., click listeners) or defining contracts for components.
Code Example
interface Clickable {
fun onClick()
fun onLongClick() = println("Default long click")
}
class Button : Clickable {
override fun onClick() {
println("Button clicked")
}
}
fun main() {
val button = Button()
button.onClick() // Prints: Button clicked
button.onLongClick() // Prints: Default long click
}Android Example
Implementing a click listener interface.
interface ItemClickListener {
fun onItemClick(item: String)
}
class ItemAdapter(private val listener: ItemClickListener) {
fun selectItem(item: String) {
listener.onItemClick(item)
}
}
class MainActivity : AppCompatActivity(), ItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ItemAdapter(this)
adapter.selectItem("Item 1")
}
override fun onItemClick(item: String) {
Toast.makeText(this, "Selected: $item", Toast.LENGTH_SHORT).show()
}
}Interview Tip
- Compare interfaces with abstract classes in Kotlin.
- Explain their use in Android for decoupling components.
5.8 Scope Functions: apply, also, let, with, and run
Definition
Kotlin’s scope functions (apply, also, let, with, run) provide concise ways to operate on objects within a specific scope, improving code readability and reducing boilerplate.
Key Features
- apply: Configures an object and returns it (this).
- also: Performs additional actions on an object and returns it (it).
- let: Executes a block on a nullable object and returns the block’s result.
- with: Executes a block on a non-null object and returns the block’s result.
- run: Executes a block on an object and returns the block’s result (similar to with but as an extension function).
Use Case
- In Android, scope functions are used to initialize views, handle nullable objects, or simplify object configuration.
Code Example
data class Person(var name: String, var age: Int)
fun main() {
// apply: Configure and return the object
val person1 = Person("Alice", 25).apply {
age = 26
}
println(person1) // Prints: Person(name=Alice, age=26)
// also: Perform side effects and return the object
val person2 = Person("Bob", 30).also {
println("Created person: ${it.name}")
}
println(person2) // Prints: Person(name=Bob, age=30)
// let: Handle nullable objects
val nullableName: String? = "Charlie"
val length = nullableName?.let { it.length } ?: 0
println(length) // Prints: 7
// with: Work with a non-null object
val person3 = Person("Dave", 40)
with(person3) {
name = "David"
age = 41
}
println(person3) // Prints: Person(name=David, age=41)
// run: Execute a block and return its result
val result = person3.run { "$name is $age years old" }
println(result) // Prints: David is 41 years old
}Android Example
Using scope functions for view initialization.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// apply: Configure a view
findViewById<TextView>(R.id.textView).apply {
text = "Welcome!"
textSize = 18f
setTextColor(Color.BLACK)
}
// let: Handle nullable intent data
val data: String? = intent.getStringExtra("key")
data?.let {
findViewById<TextView>(R.id.textView).text = it
}
}
}Interview Tip
- Explain the differences between scope functions and when to use each.
- Demonstrate their use in Android for view setup or nullable handling.
5.9 Type Checking and Smart Casting
Definition
Type checking uses the is operator to check an object’s type, and smart casting automatically casts the object to that type within a safe scope (e.g., if or when).
Use Case
- In Android, type checking and smart casting are used when handling different types in a collection or polymorphic objects.
Code Example
interface Animal {
fun makeSound()
}
class Dog : Animal {
override fun makeSound() = println("Bark")
fun fetch() = println("Fetching ball")
}
class Cat : Animal {
override fun makeSound() = println("Meow")
}
fun main() {
val animals: List<Animal> = listOf(Dog(), Cat())
for (animal in animals) {
if (animal is Dog) {
animal.fetch() // Smart cast to Dog
}
animal.makeSound()
}
}Android Example
Handling different view types in a RecyclerView.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val view: View = findViewById(R.id.button)
if (view is Button) {
view.text = "Click Me" // Smart cast to Button
}
}
}Interview Tip
- Explain how smart casting reduces explicit casting compared to Java.
- Discuss its use with sealed classes or interfaces.
5.10 Getters and Setters in Kotlin (Backing Field)
Definition
Kotlin properties automatically generate getters and setters. A backing field (field) is used to store the property’s value when custom getter/setter logic is needed.
Key Features
- Default getters/setters are generated for var and val properties.
- Use field in custom getters/setters to avoid recursive calls.
Use Case
- In Android, custom getters/setters are used for data validation or UI updates.
Code Example
class Person {
var name: String = ""
get() = field.uppercase()
set(value) {
field = if (value.isNotEmpty()) value else "Unknown"
}
}
fun main() {
val person = Person()
person.name = "Alice"
println(person.name) // Prints: ALICE
person.name = ""
println(person.name) // Prints: UNKNOWN
}Android Example
Validating input in a ViewModel.
class UserViewModel {
var username: String = ""
get() = field
set(value) {
field = if (value.length >= 3) value else "Invalid"
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = UserViewModel()
val textView: TextView = findViewById(R.id.textView)
viewModel.username = "Al"
textView.text = viewModel.username // Prints: Invalid
}
}Interview Tip
- Explain the role of field and how it prevents infinite recursion.
- Discuss when to use custom getters/setters in Android.
5.11 lateinit Keyword for Variables
Definition
The lateinit keyword delays the initialization of a non-null var property until later. It is used only with var (not val) and non-primitive types.
Use Case
- In Android, lateinit is used for variables initialized after object creation, such as views in an Activity or dependencies in dependency injection.
Code Example
class Example {
lateinit var name: String
fun initializeName(value: String) {
name = value
}
fun isInitialized(): Boolean {
return ::name.isInitialized
}
}
fun main() {
val example = Example()
println(example.isInitialized()) // Prints: false
example.initializeName("Alice")
println(example.name) // Prints: Alice
}Android Example
Using lateinit for view initialization.
class MainActivity : AppCompatActivity() {
lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textView)
if (::textView.isInitialized) {
textView.text = "Initialized!"
}
}
}Interview Tip
- Explain why lateinit is used and its risks (e.g., accessing uninitialized properties).
- Discuss alternatives like nullable types or lazy initialization.
5.12 Safe Call and Elvis Operator
Definition
- Safe Call Operator (?. ): Accesses properties or methods of a nullable object only if it is non-null.
- Elvis Operator (?: ): Provides a default value if an expression is null.
Use Case
- In Android, safe calls and the Elvis operator are used to handle nullable views, intent extras, or API responses safely.
Code Example
fun main() {
val nullableString: String? = null
// Safe Call
println(nullableString?.length) // Prints: null
// Elvis Operator
val length = nullableString?.length ?: 0
println(length) // Prints: 0
}Android Example
Handling nullable intent data.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
val data: String? = intent.getStringExtra("key")
textView.text = data ?: "No data provided"
}
}Interview Tip
- Compare safe call/Elvis operator with Java’s null checks.
- Explain their role in preventing NullPointerExceptions in Android.
6. Practical Use Cases for Android Development
Use Case 1: Coroutines with Sealed Classes
Fetch data asynchronously and update UI based on state.
sealed class ApiResult {
data class Success(val data: String) : ApiResult()
data class Error(val message: String) : ApiResult()
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
CoroutineScope(Dispatchers.Main).launch {
val result = fetchData()
textView.text = when (result) {
is ApiResult.Success -> result.data
is ApiResult.Error -> result.message
}
}
}
private suspend fun fetchData(): ApiResult = withContext(Dispatchers.IO) {
try {
delay(1000) // Simulate network call
ApiResult.Success("Data loaded")
} catch (e: Exception) {
ApiResult.Error("Failed to load")
}
}
}Use Case 2: Scope Functions for View Setup
Configure multiple views concisely.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.button).apply {
text = "Submit"
isEnabled = false
setOnClickListener { Toast.makeText(this@MainActivity, "Clicked", Toast.LENGTH_SHORT).show() }
}
}
}Use Case 3: Companion Object for Constants
Define app-wide constants.
class AppConfig {
companion object {
const val API_BASE_URL = "https://api.example.com"
const val TIMEOUT_SECONDS = 30L
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
textView.text = AppConfig.API_BASE_URL
}
}7. Interview Preparation Tips
- Master Coroutines:
- Explain suspend functions, dispatchers, and structured concurrency.
- Demonstrate Android examples (e.g., API calls with Retrofit and coroutines).
- Java Interoperability:
- Discuss calling Java APIs in Kotlin and handling nullability.
- Sealed Classes vs. Enums:
- Explain when to use each and their benefits in Android.
- Scope Functions:
- Provide examples of apply vs. let vs. run in Android contexts.
- Exception Handling:
- Show how to handle errors in coroutines and UI updates.
- Common Questions:
- How do coroutines improve Android development compared to AsyncTask?
- When would you use lateinit vs. a nullable property?
- How do scope functions improve code readability?
8. Additional Resources
- Kotlin Coroutines Documentation: kotlinlang.org/docs/coroutines-guide.html
- Kotlin for Android: developer.android.com/kotlin
- Kotlin Playground: play.kotlinlang.org for practicing concepts
- Books: “Kotlin Coroutines by Tutorials” by Ray Wenderlich for advanced coroutine learning
- Interview Questions


No comments:
Post a Comment