Kotlin is the preferred language for Android development, offering modern features, null safety, and excellent Java interoperability. This beginner-friendly tutorial takes you from zero to building your first Android app.
Why Kotlin?
- Official Android language - Recommended by Google
- Concise syntax - Less boilerplate than Java
- Null safety - Prevents null pointer exceptions
- Coroutines - Easy asynchronous programming
- Java interop - Use existing Java libraries
Prerequisites
Before starting, you'll need:
- Computer (Windows, Mac, or Linux)
- Android Studio (free)
- Basic programming concepts understanding
Setting Up Android Studio
1. Download Android Studio
Visit developer.android.com/studio and download the latest version.
2. Create Your First Project
- Open Android Studio
- Click "New Project"
- Select "Empty Activity" (Compose)
- Configure:
- Name: MyFirstApp
- Package: com.example.myfirstapp
- Language: Kotlin
- Minimum SDK: API 24
Kotlin Basics
Variables and Types
// Immutable (val) - cannot be reassigned
val name = "John"
val age = 25
val pi = 3.14159
// Mutable (var) - can be reassigned
var score = 0
score = 100
// Explicit types
val greeting: String = "Hello"
val count: Int = 42
val price: Double = 29.99
val isActive: Boolean = true
// Type inference
val city = "New York" // String inferred
val number = 42 // Int inferred
Basic Types
// Numbers
val byte: Byte = 127
val short: Short = 32000
val int: Int = 2_000_000
val long: Long = 9_000_000_000L
val float: Float = 3.14f
val double: Double = 3.14159265359
// Strings
val message = "Hello, World!"
val multiline = """
This is a
multiline string
""".trimIndent()
// String templates
val name = "Alice"
val greeting = "Hello, $name!"
val calculation = "2 + 2 = ${2 + 2}"
// Characters
val letter: Char = 'A'
// Booleans
val isEnabled = true
val isHidden = false
Null Safety
// Non-nullable (default)
var name: String = "John"
// name = null // Compile error!
// Nullable (with ?)
var middleName: String? = null
middleName = "William"
// Safe call operator (?.)
val length = middleName?.length // null if middleName is null
// Elvis operator (?:)
val displayName = middleName ?: "Unknown"
// Not-null assertion (!!) - use carefully!
val forcedLength = middleName!!.length // Throws if null
// Safe cast
val obj: Any = "Hello"
val str: String? = obj as? String
// Let with null check
middleName?.let { name ->
println("Middle name is $name")
}
Collections
// Lists (immutable)
val fruits = listOf("Apple", "Banana", "Orange")
val first = fruits[0]
val size = fruits.size
// Mutable lists
val mutableFruits = mutableListOf("Apple", "Banana")
mutableFruits.add("Orange")
mutableFruits.removeAt(0)
// Sets
val colors = setOf("Red", "Green", "Blue")
val mutableColors = mutableSetOf("Red", "Green")
mutableColors.add("Blue")
// Maps
val scores = mapOf(
"Alice" to 95,
"Bob" to 87,
"Charlie" to 92
)
val aliceScore = scores["Alice"]
val mutableScores = mutableMapOf<String, Int>()
mutableScores["David"] = 88
// Collection operations
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }
val sum = numbers.reduce { acc, n -> acc + n }
val firstEven = numbers.find { it % 2 == 0 }
val anyEven = numbers.any { it % 2 == 0 }
val allPositive = numbers.all { it > 0 }
Control Flow
// If expression
val max = if (a > b) a else b
// When expression (like switch)
val grade = when (score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
else -> "F"
}
// When with conditions
when {
x.isOdd() -> println("x is odd")
y.isEven() -> println("y is even")
else -> println("neither")
}
// For loops
for (fruit in fruits) {
println(fruit)
}
for (i in 0..4) {
println(i) // 0, 1, 2, 3, 4
}
for (i in 0 until 5) {
println(i) // 0, 1, 2, 3, 4
}
for (i in 5 downTo 0 step 2) {
println(i) // 5, 3, 1
}
for ((index, value) in fruits.withIndex()) {
println("$index: $value")
}
// While loops
while (condition) {
// do something
}
do {
// do something
} while (condition)
Functions
// Basic function
fun sayHello() {
println("Hello!")
}
// Function with parameters and return type
fun add(a: Int, b: Int): Int {
return a + b
}
// Single-expression function
fun multiply(a: Int, b: Int) = a * b
// Default parameters
fun greet(name: String = "World") {
println("Hello, $name!")
}
// Named arguments
fun createUser(name: String, age: Int, isAdmin: Boolean = false) {
// ...
}
createUser(name = "Alice", age = 25, isAdmin = true)
// Vararg parameters
fun sum(vararg numbers: Int): Int {
return numbers.sum()
}
sum(1, 2, 3, 4, 5)
// Extension functions
fun String.addExclamation() = this + "!"
val excited = "Hello".addExclamation() // "Hello!"
// Higher-order functions
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
val result = calculate(10, 5) { x, y -> x + y }
Lambdas
// Lambda syntax
val sum = { a: Int, b: Int -> a + b }
val result = sum(10, 5)
// Type inference
val greet: (String) -> String = { name -> "Hello, $name!" }
// Single parameter uses 'it'
val double: (Int) -> Int = { it * 2 }
// Trailing lambda
listOf(1, 2, 3).map { it * 2 }
// Multiple parameters
listOf(1, 2, 3).fold(0) { acc, num -> acc + num }
Classes
// Basic class
class Person(val name: String, var age: Int) {
// Secondary constructor
constructor(name: String) : this(name, 0)
// Properties
val isAdult: Boolean
get() = age >= 18
// Methods
fun greet() {
println("Hello, I'm $name")
}
// Init block
init {
println("Person created: $name")
}
}
val person = Person("Alice", 25)
person.greet()
println(person.isAdult)
// Data class (auto-generates equals, hashCode, toString, copy)
data class User(
val id: Int,
val name: String,
val email: String
)
val user = User(1, "Alice", "alice@example.com")
val copy = user.copy(name = "Bob")
// Inheritance
open class Animal(val name: String) {
open fun makeSound() {
println("Some sound")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("Woof!")
}
}
// Object (singleton)
object Database {
fun connect() {
println("Connected")
}
}
Database.connect()
// Companion object (static-like)
class MyClass {
companion object {
const val TAG = "MyClass"
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create()
Interfaces and Abstract Classes
// Interface
interface Drawable {
fun draw()
fun erase() {
println("Erasing...") // Default implementation
}
}
interface Colorable {
val color: String
}
// Implementing interfaces
class Circle : Drawable, Colorable {
override val color = "Red"
override fun draw() {
println("Drawing circle")
}
}
// Abstract class
abstract class Shape {
abstract val area: Double
abstract fun draw()
fun describe() {
println("This is a shape with area $area")
}
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override val area = width * height
override fun draw() {
println("Drawing rectangle")
}
}
Coroutines
import kotlinx.coroutines.*
// Basic coroutine
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
// Suspend function
suspend fun fetchData(): String {
delay(1000) // Simulates network call
return "Data loaded"
}
// Async/await
suspend fun loadUserAndPosts() = coroutineScope {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
println("User: ${user.await()}")
println("Posts: ${posts.await()}")
}
// Error handling
suspend fun safeFetch() {
try {
val data = fetchData()
println(data)
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
// With context
suspend fun fetchFromNetwork() = withContext(Dispatchers.IO) {
// Network operation
}
Jetpack Compose Basics
Your First Composable
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Greeting(name: String) {
Text(
text = "Hello, $name!",
style = MaterialTheme.typography.headlineMedium
)
}
@Composable
fun MyApp() {
Column(
modifier = Modifier.padding(16.dp)
) {
Greeting("Android")
Button(
onClick = { println("Clicked!") },
modifier = Modifier.padding(top = 8.dp)
) {
Text("Click Me")
}
}
}
State Management
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.headlineLarge
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { count-- }) {
Text("-")
}
Button(onClick = { count++ }) {
Text("+")
}
}
}
}
// State hoisting
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(
count = count,
onIncrement = { count++ },
onDecrement = { count-- }
)
}
@Composable
fun StatelessCounter(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
Column {
Text("Count: $count")
Row {
Button(onClick = onDecrement) { Text("-") }
Button(onClick = onIncrement) { Text("+") }
}
}
}
Lists and LazyColumn
data class Task(
val id: Int,
val title: String,
val isCompleted: Boolean
)
@Composable
fun TaskList(tasks: List<Task>, onToggle: (Task) -> Unit) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(tasks, key = { it.id }) { task ->
TaskItem(
task = task,
onToggle = { onToggle(task) }
)
}
}
}
@Composable
fun TaskItem(task: Task, onToggle: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = task.isCompleted,
onCheckedChange = { onToggle() }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = task.title,
style = MaterialTheme.typography.bodyLarge,
textDecoration = if (task.isCompleted)
TextDecoration.LineThrough else TextDecoration.None
)
}
}
}
Navigation
import androidx.navigation.compose.*
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetails = { id ->
navController.navigate("details/$id")
}
)
}
composable(
route = "details/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.IntType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailsScreen(
itemId = itemId,
onBack = { navController.popBackStack() }
)
}
}
}
@Composable
fun HomeScreen(onNavigateToDetails: (Int) -> Unit) {
Column {
Text("Home Screen")
Button(onClick = { onNavigateToDetails(123) }) {
Text("Go to Details")
}
}
}
@Composable
fun DetailsScreen(itemId: Int, onBack: () -> Unit) {
Column {
Text("Details for item $itemId")
Button(onClick = onBack) {
Text("Go Back")
}
}
}
Building a Complete App: Todo List
ViewModel
// TodoViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.mutableStateListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
data class TodoItem(
val id: Int,
val title: String,
val isCompleted: Boolean = false
)
class TodoViewModel : ViewModel() {
private val _todos = mutableStateListOf<TodoItem>()
val todos: List<TodoItem> = _todos
private var nextId = 0
fun addTodo(title: String) {
_todos.add(TodoItem(id = nextId++, title = title))
}
fun toggleTodo(id: Int) {
val index = _todos.indexOfFirst { it.id == id }
if (index >= 0) {
_todos[index] = _todos[index].copy(
isCompleted = !_todos[index].isCompleted
)
}
}
fun deleteTodo(id: Int) {
_todos.removeAll { it.id == id }
}
}
UI Components
// TodoScreen.kt
@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
var showDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("My Todos") }
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showDialog = true }
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
) { padding ->
if (viewModel.todos.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text("No todos yet!")
}
} else {
LazyColumn(
modifier = Modifier.padding(padding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(viewModel.todos, key = { it.id }) { todo ->
TodoItemCard(
todo = todo,
onToggle = { viewModel.toggleTodo(todo.id) },
onDelete = { viewModel.deleteTodo(todo.id) }
)
}
}
}
if (showDialog) {
AddTodoDialog(
onDismiss = { showDialog = false },
onAdd = { title ->
viewModel.addTodo(title)
showDialog = false
}
)
}
}
}
@Composable
fun TodoItemCard(
todo: TodoItem,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.isCompleted,
onCheckedChange = { onToggle() }
)
Text(
text = todo.title,
modifier = Modifier.weight(1f),
textDecoration = if (todo.isCompleted)
TextDecoration.LineThrough else null
)
IconButton(onClick = onDelete) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete"
)
}
}
}
}
@Composable
fun AddTodoDialog(
onDismiss: () -> Unit,
onAdd: (String) -> Unit
) {
var title by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add Todo") },
text = {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Title") },
singleLine = true
)
},
confirmButton = {
TextButton(
onClick = { onAdd(title) },
enabled = title.isNotBlank()
) {
Text("Add")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
Networking with Retrofit
// API Interface
interface ApiService {
@GET("posts")
suspend fun getPosts(): List<Post>
@GET("posts/{id}")
suspend fun getPost(@Path("id") id: Int): Post
@POST("posts")
suspend fun createPost(@Body post: Post): Post
}
// Data class
data class Post(
val id: Int,
val title: String,
val body: String,
val userId: Int
)
// Retrofit setup
object RetrofitClient {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
val api: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
// ViewModel usage
class PostsViewModel : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts = _posts.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
fun loadPosts() {
viewModelScope.launch {
_isLoading.value = true
try {
_posts.value = RetrofitClient.api.getPosts()
} catch (e: Exception) {
// Handle error
}
_isLoading.value = false
}
}
}
Next Steps
- Master Jetpack Compose - Modern declarative UI
- Room Database - Local data persistence
- Hilt/Koin - Dependency injection
- Firebase - Backend services
- Testing - Unit and UI testing
- Play Store - Publishing your app
Conclusion
You've learned Kotlin basics and built a functional Android app with Jetpack Compose! Kotlin's modern features and excellent tooling make Android development productive and enjoyable.
Need help with your Android project? Contact Hevcode for professional Android development services. We build native Kotlin applications for all Android devices.