Kotlin

modern android development. concise and safe.


basics

// variables
val name = "abhishek"    // immutable (like const)
var age = 20             // mutable

// explicit type
val name: String = "abhishek"
var count: Int = 0

// nullable
var email: String? = null      // can be null
var email: String = "a@b.com"  // cannot be null

// null safety
email?.length              // safe call, null if email is null
email ?: "no email"        // elvis operator, default if null
email!!.length             // force unwrap, throws if null (avoid)

data types

val i: Int = 42
val l: Long = 42L
val f: Float = 3.14f
val d: Double = 3.14
val b: Boolean = true
val c: Char = 'A'
val s: String = "hello"
val bytes: ByteArray = byteArrayOf(1, 2, 3)

// type conversion
42.toString()
"42".toInt()
"3.14".toDouble()
42.toLong()

strings

val name = "abhishek"
val greeting = "hello $name"
val expr = "age is ${age + 1}"

// raw string
val text = """
    line one
    line two
""".trimIndent()

// methods
name.length
name.uppercase()
name.lowercase()
name.trim()
name.contains("abhi")
name.startsWith("ab")
name.endsWith("ek")
name.replace("abhi", "xyz")
name.split(",")
name.substring(0, 4)
name.isEmpty()
name.isBlank()

collections

// list
val list = listOf(1, 2, 3)          // immutable
val mList = mutableListOf(1, 2, 3)  // mutable

mList.add(4)
mList.remove(1)
mList.removeAt(0)
mList[0] = 10

// map
val map = mapOf("name" to "abhi", "age" to 20)
val mMap = mutableMapOf("name" to "abhi")

mMap["email"] = "a@b.com"
mMap.remove("name")
map["name"]            // "abhi"
map.getOrDefault("x", "default")
map.containsKey("name")

// set
val set = setOf(1, 2, 3)
val mSet = mutableSetOf(1, 2, 3)
mSet.add(4)

// collection operations
list.filter { it > 2 }       // [3]
list.map { it * 2 }          // [2, 4, 6]
list.find { it > 2 }         // 3
list.any { it > 2 }          // true
list.all { it > 0 }          // true
list.none { it < 0 }         // true
list.count { it > 1 }        // 2
list.sum()                   // 6
list.sortedBy { it }
list.sortedByDescending { it }
list.forEach { println(it) }
list.reduce { acc, i -> acc + i }
list.groupBy { it % 2 }
list.first()
list.last()
list.firstOrNull()
list.take(2)
list.drop(2)
list.distinct()
list.flatten()

functions

// basic
fun greet(name: String): String {
    return "hello $name"
}

// single expression
fun greet(name: String) = "hello $name"

// default params
fun greet(name: String = "stranger") = "hello $name"

// named args
greet(name = "abhishek")

// vararg
fun sum(vararg numbers: Int) = numbers.sum()
sum(1, 2, 3)

// extension function
fun String.shout() = uppercase() + "!!!"
"hello".shout()  // "HELLO!!!"

// higher order function
fun apply(value: Int, operation: (Int) -> Int): Int {
    return operation(value)
}
apply(5) { it * 2 }  // 10

// lambda
val double = { x: Int -> x * 2 }
val add = { a: Int, b: Int -> a + b }

classes

// basic class
class User(val name: String, var age: Int) {
    val displayName get() = name.uppercase()

    fun greet() = "hello, i'm $name"
}

val user = User("abhishek", 20)
user.name    // "abhishek"
user.age = 21
user.greet()

// data class (for models)
data class User(
    val id: Int,
    val name: String,
    val email: String
)
// auto generates: equals, hashCode, toString, copy

val user = User(1, "abhishek", "a@b.com")
val updated = user.copy(age = 21)

// sealed class (for state)
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// object (singleton)
object Database {
    val connection = "connected"
    fun query(sql: String) = "result"
}
Database.query("SELECT *")

// companion object (static methods)
class User {
    companion object {
        fun create(name: String) = User(name)
    }
}
User.create("abhishek")

// interface
interface Clickable {
    fun onClick()
    fun onLongClick() {}  // default implementation
}

// inheritance
open class Animal(val name: String) {
    open fun sound() = "..."
}

class Dog(name: String) : Animal(name) {
    override fun sound() = "woof"
}

control flow

// if/else (expression)
val label = if (age > 18) "adult" else "minor"

// when (like switch but better)
when (status) {
    "active" -> println("active")
    "inactive" -> println("inactive")
    else -> println("unknown")
}

// when as expression
val label = when {
    age > 18 -> "adult"
    age > 13 -> "teen"
    else -> "child"
}

// when with type check
when (obj) {
    is String -> println("string: $obj")
    is Int -> println("int: $obj")
    else -> println("other")
}

loops

for (i in 1..5) {}        // 1 to 5 inclusive
for (i in 1 until 5) {}   // 1 to 4
for (i in 5 downTo 1) {}  // 5 to 1
for (i in 1..10 step 2) {} // 1,3,5,7,9

for (item in list) {}
for ((index, item) in list.withIndex()) {}
for ((key, value) in map) {}

while (condition) {}
do {} while (condition)

repeat(5) { println(it) }

coroutines (async)

// add to build.gradle
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"

import kotlinx.coroutines.*

// launch (fire and forget)
CoroutineScope(Dispatchers.IO).launch {
    val data = fetchData()
    withContext(Dispatchers.Main) {
        // update UI on main thread
        updateUI(data)
    }
}

// async/await (get result)
val result = async { fetchData() }
val data = result.await()

// in viewmodel
viewModelScope.launch {
    try {
        val data = repository.getData()
        _uiState.value = UiState.Success(data)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e.message)
    }
}

// dispatchers
Dispatchers.Main    // UI thread
Dispatchers.IO      // network/disk
Dispatchers.Default // CPU intensive

android basics

// Activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() { super.onStart() }
    override fun onResume() { super.onResume() }
    override fun onPause() { super.onPause() }
    override fun onStop() { super.onStop() }
    override fun onDestroy() { super.onDestroy() }
}

// Fragment
class HomeFragment : Fragment(R.layout.fragment_home) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // setup views here
    }
}

// Intent (navigate between activities)
val intent = Intent(this, ProfileActivity::class.java)
intent.putExtra("userId", 123)
startActivity(intent)

// get extra
val userId = intent.getIntExtra("userId", 0)

// implicit intent
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
startActivity(intent)

Jetpack Compose basics

// composable function
@Composable
fun Greeting(name: String) {
    Text(text = "hello $name")
}

// state
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// layout
Column(
    modifier = Modifier.fillMaxSize().padding(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("Title", style = MaterialTheme.typography.headlineMedium)
    Spacer(modifier = Modifier.height(8.dp))
    Button(onClick = {}) { Text("Click me") }
}

Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween
) {}

Box(modifier = Modifier.fillMaxSize()) {
    // overlapping elements
}

// modifier
Modifier
    .fillMaxSize()
    .fillMaxWidth()
    .fillMaxHeight()
    .size(100.dp)
    .width(100.dp)
    .height(100.dp)
    .padding(16.dp)
    .background(Color.Blue)
    .clip(RoundedCornerShape(8.dp))
    .border(1.dp, Color.Gray)
    .clickable {}
    .alpha(0.5f)

// lazy list (like RecyclerView)
LazyColumn {
    items(userList) { user ->
        UserCard(user)
    }
}

LazyRow {
    items(userList) { user ->
        UserCard(user)
    }
}

ViewModel

class HomeViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    private val _loading = MutableStateFlow(false)
    val loading: StateFlow<Boolean> = _loading.asStateFlow()

    init {
        loadUsers()
    }

    private fun loadUsers() {
        viewModelScope.launch {
            _loading.value = true
            try {
                _users.value = repository.getUsers()
            } catch (e: Exception) {
                // handle error
            } finally {
                _loading.value = false
            }
        }
    }
}

// in composable
@Composable
fun HomeScreen(viewModel: HomeViewModel = viewModel()) {
    val users by viewModel.users.collectAsState()
    val loading by viewModel.loading.collectAsState()
}

Room database

// entity
@Entity(tableName = "messages")
data class Message(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val text: String,
    val timestamp: Long = System.currentTimeMillis()
)

// DAO
@Dao
interface MessageDao {
    @Query("SELECT * FROM messages ORDER BY timestamp DESC")
    fun getAllMessages(): Flow<List<Message>>

    @Insert
    suspend fun insert(message: Message)

    @Delete
    suspend fun delete(message: Message)

    @Update
    suspend fun update(message: Message)
}

// database
@Database(entities = [Message::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun messageDao(): MessageDao
}

// create
val db = Room.databaseBuilder(
    context,
    AppDatabase::class.java,
    "app-database"
).build()

encryption (for private chat app)

// add dependency
// implementation "androidx.security:security-crypto:1.1.0-alpha06"

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import javax.crypto.Cipher
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties

// encrypted shared preferences
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val prefs = EncryptedSharedPreferences.create(
    context,
    "secret_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

prefs.edit().putString("token", "secret").apply()
prefs.getString("token", null)

// AES encryption
fun encrypt(data: String, key: SecretKey): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    return cipher.doFinal(data.toByteArray())
}

fun decrypt(data: ByteArray, key: SecretKey, iv: ByteArray): String {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
    return String(cipher.doFinal(data))
}

build.gradle (app level)

android {
    compileSdk 34
    defaultConfig {
        minSdk 21   // Android 5.0
        targetSdk 34
    }
}

dependencies {
    // core
    implementation "androidx.core:core-ktx:1.12.0"
    implementation "androidx.appcompat:appcompat:1.6.1"

    // compose
    implementation platform("androidx.compose:compose-bom:2024.01.00")
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.activity:activity-compose:1.8.2"

    // viewmodel
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"

    // coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"

    // room
    implementation "androidx.room:room-runtime:2.6.1"
    implementation "androidx.room:room-ktx:2.6.1"
    kapt "androidx.room:room-compiler:2.6.1"

    // network
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation "com.squareup.okhttp3:okhttp:4.12.0"

    // encrypted storage
    implementation "androidx.security:security-crypto:1.1.0-alpha06"
}

=^._.^= kotlin makes android fun