Kotlin - Android Basics

Overview

Kotlin is the preferred language for Android development, offering concise syntax, null safety, and seamless Java interoperability. This tutorial covers Android app development with Kotlin, including Activities, Views, layouts, navigation, and modern Android development practices.

🎯 Learning Objectives:
  • Understand Android app structure and Kotlin integration
  • Learn Activities, Fragments, and lifecycle management
  • Master Views, layouts, and UI development
  • Implement navigation and data binding
  • Apply modern Android development patterns

Android Project Setup

Project Configuration

// build.gradle (Module: app)
android {
    compileSdk 34
    
    defaultConfig {
        applicationId "com.example.kotlinandroidapp"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"
        
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    
    buildTypes {
        release {
            isMinifyEnabled false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    kotlinOptions {
        jvmTarget = "1.8"
    }
    
    buildFeatures {
        viewBinding true
        dataBinding true
    }
}

dependencies {
    implementation "androidx.core:core-ktx:1.12.0"
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.11.0"
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"
    
    // Navigation
    implementation "androidx.navigation:navigation-fragment-ktx:2.7.6"
    implementation "androidx.navigation:navigation-ui-ktx:2.7.6"
    
    // Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
    
    // RecyclerView
    implementation "androidx.recyclerview:recyclerview:1.3.2"
    
    // Testing
    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}

Application Class

// MyApplication.kt
package com.example.kotlinandroidapp

import android.app.Application
import android.util.Log

class MyApplication : Application() {
    
    companion object {
        private const val TAG = "MyApplication"
    }
    
    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Application created")
        
        // Initialize global components here
        // e.g., Dependency injection, crash reporting, analytics
    }
}

// AndroidManifest.xml
<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.KotlinAndroidApp">
    
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

Activities and Lifecycle

MainActivity with Kotlin

// MainActivity.kt
package com.example.kotlinandroidapp

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.example.kotlinandroidapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    
    companion object {
        private const val TAG = "MainActivity"
        private const val KEY_COUNTER = "counter_key"
    }
    
    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MainViewModel
    private var counter = 0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "onCreate called")
        
        // Initialize view binding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        // Initialize ViewModel
        viewModel = ViewModelProvider(this)[MainViewModel::class.java]
        
        // Restore state
        savedInstanceState?.let { bundle ->
            counter = bundle.getInt(KEY_COUNTER, 0)
        }
        
        setupUI()
        observeViewModel()
    }
    
    private fun setupUI() {
        // Set up click listeners using view binding
        binding.buttonIncrement.setOnClickListener {
            counter++
            updateCounterDisplay()
            viewModel.incrementCounter()
        }
        
        binding.buttonDecrement.setOnClickListener {
            counter--
            updateCounterDisplay()
            viewModel.decrementCounter()
        }
        
        binding.buttonShowToast.setOnClickListener {
            showToast("Counter value: $counter")
        }
        
        binding.buttonOpenSecondActivity.setOnClickListener {
            openSecondActivity()
        }
        
        updateCounterDisplay()
    }
    
    private fun observeViewModel() {
        viewModel.counterValue.observe(this) { value ->
            binding.textViewModelCounter.text = "ViewModel Counter: $value"
        }
        
        viewModel.message.observe(this) { message ->
            if (message.isNotEmpty()) {
                showToast(message)
                viewModel.clearMessage()
            }
        }
    }
    
    private fun updateCounterDisplay() {
        binding.textViewCounter.text = "Count: $counter"
    }
    
    private fun showToast(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
    
    private fun openSecondActivity() {
        val intent = Intent(this, SecondActivity::class.java).apply {
            putExtra("counter_value", counter)
            putExtra("message", "Hello from MainActivity")
        }
        startActivity(intent)
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(KEY_COUNTER, counter)
        Log.d(TAG, "State saved: counter = $counter")
    }
    
    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart called")
    }
    
    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume called")
    }
    
    override fun onPause() {
        super.onPause()
        Log.d(TAG, "onPause called")
    }
    
    override fun onStop() {
        super.onStop()
        Log.d(TAG, "onStop called")
    }
    
    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy called")
    }
}

ViewModel with LiveData

// MainViewModel.kt
package com.example.kotlinandroidapp

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    
    private val _counterValue = MutableLiveData().apply { value = 0 }
    val counterValue: LiveData = _counterValue
    
    private val _message = MutableLiveData().apply { value = "" }
    val message: LiveData = _message
    
    fun incrementCounter() {
        val currentValue = _counterValue.value ?: 0
        _counterValue.value = currentValue + 1
        
        if (currentValue + 1 == 10) {
            _message.value = "Counter reached 10!"
        }
    }
    
    fun decrementCounter() {
        val currentValue = _counterValue.value ?: 0
        _counterValue.value = currentValue - 1
        
        if (currentValue - 1 == 0) {
            _message.value = "Counter is back to zero!"
        }
    }
    
    fun clearMessage() {
        _message.value = ""
    }
    
    override fun onCleared() {
        super.onCleared()
        // Clean up resources here
    }
}

Layouts and Views

XML Layout with Material Design

<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textViewTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textSize="24sp"
        android:textStyle="bold"
        android:gravity="center"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/textViewCounter" />

    <TextView
        android:id="@+id/textViewCounter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Count: 0"
        android:textSize="20sp"
        android:layout_marginTop="32dp"
        app:layout_constraintTop_toBottomOf="@+id/textViewTitle"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/textViewModelCounter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ViewModel Counter: 0"
        android:textSize="16sp"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@+id/textViewCounter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <LinearLayout
        android:id="@+id/buttonContainer"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="32dp"
        app:layout_constraintTop_toBottomOf="@+id/textViewModelCounter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/buttonDecrement"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:text="@string/decrement"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/buttonIncrement"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="8dp"
            android:text="@string/increment"
            style="@style/Widget.MaterialComponents.Button" />

    </LinearLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/buttonShowToast"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/show_toast"
        app:layout_constraintTop_toBottomOf="@+id/buttonContainer"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        style="@style/Widget.MaterialComponents.Button.UnelevatedButton" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/buttonOpenSecondActivity"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/open_second_activity"
        app:layout_constraintTop_toBottomOf="@+id/buttonShowToast"  
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Custom Views and Components

// CustomCounterView.kt
package com.example.kotlinandroidapp.views

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import com.example.kotlinandroidapp.databinding.ViewCustomCounterBinding

class CustomCounterView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
    
    private val binding: ViewCustomCounterBinding
    private var counter = 0
    private var onCounterChangedListener: ((Int) -> Unit)? = null
    
    init {
        binding = ViewCustomCounterBinding.inflate(LayoutInflater.from(context), this, true)
        setupClickListeners()
        updateDisplay()
    }
    
    private fun setupClickListeners() {
        binding.buttonMinus.setOnClickListener {
            counter--
            updateDisplay()
            onCounterChangedListener?.invoke(counter)
        }
        
        binding.buttonPlus.setOnClickListener {
            counter++
            updateDisplay()
            onCounterChangedListener?.invoke(counter)
        }
    }
    
    private fun updateDisplay() {
        binding.textCounter.text = counter.toString()
    }
    
    fun setCounter(value: Int) {
        counter = value
        updateDisplay()
    }
    
    fun getCounter(): Int = counter
    
    fun setOnCounterChangedListener(listener: (Int) -> Unit) {
        onCounterChangedListener = listener
    }
}

Fragments and Navigation

Fragment Implementation

// HomeFragment.kt
package com.example.kotlinandroidapp.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.example.kotlinandroidapp.R
import com.example.kotlinandroidapp.databinding.FragmentHomeBinding
import com.example.kotlinandroidapp.viewmodels.HomeViewModel

class HomeFragment : Fragment() {
    
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    
    private lateinit var viewModel: HomeViewModel
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewModel = ViewModelProvider(this)[HomeViewModel::class.java]
        
        setupUI()
        observeViewModel()
    }
    
    private fun setupUI() {
        binding.buttonNavigateToDetail.setOnClickListener {
            val action = HomeFragmentDirections.actionHomeToDetail("Hello from Home")
            findNavController().navigate(action)
        }
        
        binding.buttonShowDialog.setOnClickListener {
            showCustomDialog()
        }
    }
    
    private fun observeViewModel() {
        viewModel.items.observe(viewLifecycleOwner) { items ->
            // Update RecyclerView adapter
            updateItemsList(items)
        }
        
        viewModel.loading.observe(viewLifecycleOwner) { isLoading ->
            binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
        }
    }
    
    private fun updateItemsList(items: List) {
        // Update RecyclerView with new items
        binding.textViewItemCount.text = "Items: ${items.size}"
    }
    
    private fun showCustomDialog() {
        val dialog = CustomDialogFragment.newInstance("Dialog Title", "Dialog message")
        dialog.show(parentFragmentManager, "CustomDialog")
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Navigation Component Setup

<!-- navigation/nav_graph.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.kotlinandroidapp.fragments.HomeFragment"
        android:label="Home"
        tools:layout="@layout/fragment_home">
        
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.kotlinandroidapp.fragments.DetailFragment"
        android:label="Detail"
        tools:layout="@layout/fragment_detail">
        
        <argument
            android:name="message"
            app:argType="string"
            android:defaultValue="Default message" />
    </fragment>

</navigation>

RecyclerView and Adapters

RecyclerView Adapter with Kotlin

// UserAdapter.kt
package com.example.kotlinandroidapp.adapters

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlinandroidapp.databinding.ItemUserBinding
import com.example.kotlinandroidapp.models.User

class UserAdapter(
    private val onUserClick: (User) -> Unit
) : ListAdapter(UserDiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding, onUserClick)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    class UserViewHolder(
        private val binding: ItemUserBinding,
        private val onUserClick: (User) -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {
        
        fun bind(user: User) {
            binding.apply {
                textViewName.text = user.name
                textViewEmail.text = user.email
                textViewAge.text = "Age: ${user.age}"
                
                // Set click listener
                root.setOnClickListener {
                    onUserClick(user)
                }
                
                // Load user avatar (using placeholder)
                imageViewAvatar.setImageResource(
                    if (user.isActive) android.R.drawable.presence_online 
                    else android.R.drawable.presence_offline
                )
            }
        }
    }
    
    class UserDiffCallback : DiffUtil.ItemCallback() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }
    }
}

// Usage in Fragment
class UsersFragment : Fragment() {
    private lateinit var adapter: UserAdapter
    
    private fun setupRecyclerView() {
        adapter = UserAdapter { user ->
            // Handle user click
            findNavController().navigate(
                UsersFragmentDirections.actionUsersToUserDetail(user.id)
            )
        }
        
        binding.recyclerViewUsers.adapter = adapter
        
        // Observe users from ViewModel
        viewModel.users.observe(viewLifecycleOwner) { users ->
            adapter.submitList(users)
        }
    }
}

Data Binding and View Binding

Data Binding Example

<!-- fragment_user_profile.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="user"
            type="com.example.kotlinandroidapp.models.User" />
        
        <variable
            name="viewModel"
            type="com.example.kotlinandroidapp.viewmodels.UserProfileViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">

        <TextView
            android:id="@+id/textViewName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{user.name}"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <TextView
            android:id="@+id/textViewEmail"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{user.email}"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/textViewName"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <Button
            android:id="@+id/buttonEdit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="Edit Profile"
            android:layout_marginTop="32dp"
            android:onClick="@{() -> viewModel.editProfile()}"
            app:layout_constraintTop_toBottomOf="@+id/textViewEmail"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
// UserProfileFragment.kt
class UserProfileFragment : Fragment() {
    
    private lateinit var binding: FragmentUserProfileBinding
    private lateinit var viewModel: UserProfileViewModel
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(
            inflater, 
            R.layout.fragment_user_profile, 
            container, 
            false
        )
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewModel = ViewModelProvider(this)[UserProfileViewModel::class.java]
        
        // Set up data binding
        binding.apply {
            lifecycleOwner = viewLifecycleOwner
            viewModel = [email protected]
        }
        
        // Observe user data
        viewModel.user.observe(viewLifecycleOwner) { user ->
            binding.user = user
        }
    }
}

Intent Handling and Data Passing

Activity Communication

// SecondActivity.kt
class SecondActivity : AppCompatActivity() {
    
    companion object {
        const val EXTRA_COUNTER_VALUE = "counter_value"
        const val EXTRA_MESSAGE = "message"
        const val RESULT_NEW_COUNTER = "new_counter"
        
        fun createIntent(context: Context, counterValue: Int, message: String): Intent {
            return Intent(context, SecondActivity::class.java).apply {
                putExtra(EXTRA_COUNTER_VALUE, counterValue)
                putExtra(EXTRA_MESSAGE, message)
            }
        }
    }
    
    private lateinit var binding: ActivitySecondBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySecondBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        // Retrieve data from intent
        val counterValue = intent.getIntExtra(EXTRA_COUNTER_VALUE, 0)
        val message = intent.getStringExtra(EXTRA_MESSAGE) ?: "No message"
        
        setupUI(counterValue, message)
    }
    
    private fun setupUI(counterValue: Int, message: String) {
        binding.apply {
            textViewReceivedData.text = "Received: $message\nCounter: $counterValue"
            
            buttonReturnResult.setOnClickListener {
                val resultIntent = Intent().apply {
                    putExtra(RESULT_NEW_COUNTER, counterValue + 10)
                    putExtra("result_message", "Modified from SecondActivity")
                }
                setResult(RESULT_OK, resultIntent)
                finish()
            }
            
            buttonFinish.setOnClickListener {
                finish()
            }
        }
    }
}

// In MainActivity, handle the result
class MainActivity : AppCompatActivity() {
    
    private val secondActivityLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val newCounter = result.data?.getIntExtra(SecondActivity.RESULT_NEW_COUNTER, 0) ?: 0
            val message = result.data?.getStringExtra("result_message") ?: ""
            
            counter = newCounter
            updateCounterDisplay()
            showToast("Result: $message, New counter: $newCounter")
        }
    }
    
    private fun openSecondActivity() {
        val intent = SecondActivity.createIntent(this, counter, "Hello from MainActivity")
        secondActivityLauncher.launch(intent)
    }
}

Permissions and Runtime Requests

Permission Handling

// PermissionUtils.kt
package com.example.kotlinandroidapp.utils

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment

class PermissionHelper(private val fragment: Fragment) {
    
    private var onPermissionResult: ((Boolean) -> Unit)? = null
    
    private val permissionLauncher: ActivityResultLauncher> =
        fragment.registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions()
        ) { permissions ->
            val allGranted = permissions.values.all { it }
            onPermissionResult?.invoke(allGranted)
        }
    
    fun requestCameraPermission(onResult: (Boolean) -> Unit) {
        onPermissionResult = onResult
        
        when {
            hasCameraPermission() -> {
                onResult(true)
            }
            fragment.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
                // Show rationale dialog
                showPermissionRationale {
                    permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA))
                }
            }
            else -> {
                permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA))
            }
        }
    }
    
    fun requestStoragePermissions(onResult: (Boolean) -> Unit) {
        onPermissionResult = onResult
        
        val permissions = arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
        
        if (hasStoragePermissions()) {
            onResult(true)
        } else {
            permissionLauncher.launch(permissions)
        }
    }
    
    private fun hasCameraPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            fragment.requireContext(),
            Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun hasStoragePermissions(): Boolean {
        val context = fragment.requireContext()
        return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
               ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun showPermissionRationale(onAccepted: () -> Unit) {
        // Show custom dialog explaining why permission is needed
        // This is a simplified example
        onAccepted()
    }
}

// Usage in Fragment
class CameraFragment : Fragment() {
    
    private lateinit var permissionHelper: PermissionHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        permissionHelper = PermissionHelper(this)
    }
    
    private fun openCamera() {
        permissionHelper.requestCameraPermission { granted ->
            if (granted) {
                // Open camera
                launchCamera()
            } else {
                // Show error message
                showPermissionDeniedMessage()
            }
        }
    }
}

Modern Android Patterns

Repository Pattern

// UserRepository.kt
package com.example.kotlinandroidapp.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.kotlinandroidapp.models.User
import kotlinx.coroutines.delay

class UserRepository {
    
    private val _users = MutableLiveData>()
    val users: LiveData> = _users
    
    private val userList = mutableListOf()
    
    init {
        // Initialize with sample data
        loadSampleUsers()
    }
    
    suspend fun loadUsers() {
        // Simulate network call
        delay(1000)
        _users.postValue(userList)
    }
    
    suspend fun addUser(user: User) {
        delay(500) // Simulate network call
        userList.add(user.copy(id = generateId()))
        _users.postValue(userList.toList())
    }
    
    suspend fun updateUser(updatedUser: User) {
        delay(500)
        val index = userList.indexOfFirst { it.id == updatedUser.id }
        if (index != -1) {
            userList[index] = updatedUser
            _users.postValue(userList.toList())
        }
    }
    
    suspend fun deleteUser(userId: Long) {
        delay(500)
        userList.removeAll { it.id == userId }
        _users.postValue(userList.toList())
    }
    
    suspend fun getUserById(id: Long): User? {
        delay(200)
        return userList.find { it.id == id }
    }
    
    private fun loadSampleUsers() {
        userList.addAll(
            listOf(
                User(1, "John Doe", "[email protected]", 30, true),
                User(2, "Jane Smith", "[email protected]", 25, true),
                User(3, "Bob Johnson", "[email protected]", 35, false)
            )
        )
        _users.value = userList
    }
    
    private fun generateId(): Long = System.currentTimeMillis()
}

// UserViewModel with Repository
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    
    val users = repository.users
    
    private val _loading = MutableLiveData()
    val loading: LiveData = _loading
    
    private val _error = MutableLiveData()
    val error: LiveData = _error
    
    fun loadUsers() {
        viewModelScope.launch {
            try {
                _loading.value = true
                repository.loadUsers()
            } catch (e: Exception) {
                _error.value = "Failed to load users: ${e.message}"
            } finally {
                _loading.value = false
            }
        }
    }
    
    fun addUser(name: String, email: String, age: Int) {
        viewModelScope.launch {
            try {
                val user = User(0, name, email, age, true)
                repository.addUser(user)
            } catch (e: Exception) {
                _error.value = "Failed to add user: ${e.message}"
            }
        }
    }
}

Testing Android Components

Unit Testing ViewModels

// UserViewModelTest.kt
class UserViewModelTest {
    
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    
    private lateinit var repository: UserRepository
    private lateinit var viewModel: UserViewModel
    
    @Before
    fun setup() {
        repository = mockk()
        viewModel = UserViewModel(repository)
    }
    
    @Test
    fun `loadUsers should update loading state`() = runTest {
        // Given
        val users = listOf(
            User(1, "John", "[email protected]", 30, true)
        )
        coEvery { repository.loadUsers() } returns Unit
        every { repository.users } returns MutableLiveData(users)
        
        // When
        viewModel.loadUsers()
        
        // Then
        coVerify { repository.loadUsers() }
        assertEquals(false, viewModel.loading.getOrAwaitValue())
    }
    
    @Test
    fun `addUser should call repository addUser`() = runTest {
        // Given
        val name = "John Doe"
        val email = "[email protected]"
        val age = 30
        
        coEvery { repository.addUser(any()) } returns Unit
        
        // When
        viewModel.addUser(name, email, age)
        
        // Then
        coVerify { repository.addUser(match { user ->
            user.name == name && user.email == email && user.age == age
        }) }
    }
}

// Extension function for testing LiveData
fun  LiveData.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer {
        override fun onChanged(o: T) {
            data = o
            latch.countDown()
            [email protected](this)
        }
    }
    
    this.observeForever(observer)
    
    if (!latch.await(time, timeUnit)) {
        throw TimeoutException("LiveData value was never set.")
    }
    
    @Suppress("UNCHECKED_CAST")
    return data as T
}

Key Takeaways

  • Kotlin provides concise, null-safe Android development with excellent Java interoperability
  • Use ViewBinding and DataBinding for type-safe view access
  • Implement MVVM architecture with ViewModels and LiveData
  • Use Navigation Component for fragment navigation
  • Handle permissions properly using ActivityResult APIs
  • Follow Android architectural patterns like Repository pattern
  • Write comprehensive tests for ViewModels and repositories
  • Leverage Kotlin features like extension functions and coroutines

Practice Exercises

  1. Build a complete note-taking app with CRUD operations
  2. Implement a photo gallery app with camera integration
  3. Create a weather app using REST API and location services
  4. Build a chat application with real-time messaging

Quiz

  1. What are the advantages of using Kotlin for Android development?
  2. How do you handle configuration changes in Android with Kotlin?
  3. What's the difference between ViewBinding and DataBinding?
Show Answers
  1. Kotlin offers null safety, concise syntax, coroutines for async operations, excellent Java interoperability, and is Google's preferred language for Android.
  2. Use ViewModels to survive configuration changes, save state in onSaveInstanceState(), and use the saved state handle for fragment arguments.
  3. ViewBinding provides type-safe view references, while DataBinding additionally allows binding data directly to views in XML and supports two-way data binding.