1. Kotlin Mastery
Kotlin is the primary language for modern Android development. A strong Android developer should deeply understand Kotlin syntax, null safety, OOP, functional programming, coroutines, Flow, and clean code practices.
Variables and Constants
Kotlin uses val for read-only values and var for changeable values.
1
2
3
4
5
6
7
8
9
| fun main() {
val appName = "My Android App" // Read-only
var downloadCount = 100 // Changeable
downloadCount = 150
println(appName)
println(downloadCount)
}
|
Data Class
A data class is commonly used for models, API responses, UI state, and database entities.
1
2
3
4
5
6
7
8
9
10
11
| data class User(
val id: Int,
val name: String,
val isPremium: Boolean
)
fun main() {
val user = User(id = 1, name = "Envobyte", isPremium = true)
println(user.name)
}
|
Class
A class is used to create objects with properties and functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class UserManager {
var username: String = "Guest"
fun updateName(name: String) {
username = name
}
}
fun main() {
val manager = UserManager()
manager.updateName("Envobyte")
println(manager.username)
}
|
Object
object creates a singleton class.
1
2
3
4
5
6
7
8
| object AppConfig {
const val BASE_URL = "https://api.example.com"
const val APP_NAME = "My Android App"
}
fun main() {
println(AppConfig.BASE_URL)
}
|
Companion Object
companion object is used for static-like members inside a class.
1
2
3
4
5
6
7
8
9
| class AppConstants {
companion object {
const val MAX_RETRY = 3
}
}
fun main() {
println(AppConstants.MAX_RETRY)
}
|
Null Safety
Kotlin uses nullable types to prevent null pointer crashes.
1
2
3
4
5
6
7
8
9
| fun main() {
val username: String? = "Envobyte"
if (username != null) {
println(username.length)
} else {
println("No username found")
}
}
|
Safe Call Operator
The safe call operator ?. safely accesses nullable values.
1
2
3
4
5
| fun main() {
val username: String? = null
println(username?.length)
}
|
Elvis Operator
The Elvis operator ?: provides a default value when something is null.
1
2
3
4
5
6
7
| fun main() {
val username: String? = null
val finalName = username ?: "Guest"
println(finalName)
}
|
Lateinit
lateinit is used when a non-null variable will be initialized later.
1
2
3
4
5
6
7
8
9
10
11
| class ProfileManager {
lateinit var username: String
fun setup() {
username = "Envobyte"
}
fun printName() {
println(username)
}
}
|
Lazy Initialization
lazy initializes a value only when it is first used.
1
2
3
4
5
6
7
| val apiService by lazy {
"Api Service Created"
}
fun main() {
println(apiService)
}
|
Function
Functions are reusable blocks of code.
1
2
3
4
5
6
7
| fun greetUser(name: String): String {
return "Hello, $name"
}
fun main() {
println(greetUser("Envobyte"))
}
|
Lambda
Lambda is an anonymous function.
1
2
3
4
5
6
7
| fun main() {
val greeting: (String) -> String = { name ->
"Hello, $name"
}
println(greeting("Envobyte"))
}
|
Higher-Order Function
A higher-order function takes another function as a parameter.
1
2
3
4
5
6
7
8
9
| fun performAction(action: () -> Unit) {
action()
}
fun main() {
performAction {
println("Action executed")
}
}
|
Extension Function
Extension functions add new functionality to existing classes.
1
2
3
4
5
6
7
8
| fun String.addWelcomeText(): String {
return "Welcome, $this"
}
fun main() {
val name = "Envobyte"
println(name.addWelcomeText())
}
|
Sealed Class
Sealed classes are useful for representing fixed states such as loading, success, and error.
1
2
3
4
5
6
7
8
9
10
11
12
13
| sealed class UiState {
object Loading : UiState()
data class Success(val data: String) : UiState()
data class Error(val message: String) : UiState()
}
fun handleState(state: UiState) {
when (state) {
is UiState.Loading -> println("Loading...")
is UiState.Success -> println(state.data)
is UiState.Error -> println(state.message)
}
}
|
Enum Class
Enum is used when a value can be one of a fixed set of options.
1
2
3
4
5
6
7
8
9
10
11
| enum class UserType {
FREE,
PREMIUM,
ADMIN
}
fun main() {
val type = UserType.PREMIUM
println(type)
}
|
Interface
Interfaces define rules that classes must follow.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| interface Downloader {
fun download()
}
class VideoDownloader : Downloader {
override fun download() {
println("Video download started")
}
}
fun main() {
val downloader = VideoDownloader()
downloader.download()
}
|
Generics
Generics help write reusable and type-safe code.
1
2
3
4
5
6
7
8
9
| fun <T> printValue(value: T) {
println(value)
}
fun main() {
printValue("Hello")
printValue(100)
printValue(true)
}
|
Collection Functions
Kotlin collection functions make data transformation easier.
1
2
3
4
5
6
7
8
9
| fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
val squaredNumbers = numbers.map { it * it }
println(evenNumbers)
println(squaredNumbers)
}
|
2. Android Fundamentals
Android fundamentals include Activity, Fragment, Lifecycle, Intent, Context, Manifest, permissions, resources, and app components.
Activity
An Activity represents a single screen in an Android app.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = "Hello Android")
}
}
}
|
Activity Lifecycle
The Activity lifecycle controls how a screen behaves when it starts, pauses, resumes, or stops.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Lifecycle", "onCreate called")
}
override fun onStart() {
super.onStart()
Log.d("Lifecycle", "onStart called")
}
override fun onResume() {
super.onResume()
Log.d("Lifecycle", "onResume called")
}
override fun onPause() {
super.onPause()
Log.d("Lifecycle", "onPause called")
}
override fun onStop() {
super.onStop()
Log.d("Lifecycle", "onStop called")
}
override fun onDestroy() {
super.onDestroy()
Log.d("Lifecycle", "onDestroy called")
}
}
|
Context
Context gives access to app resources, system services, files, preferences, and more.
1
2
3
4
| fun showPackageName(context: android.content.Context) {
val packageName = context.packageName
println(packageName)
}
|
Intent
Intent is used to open another Activity, share data, open links, or communicate with Android system components.
1
2
3
4
5
6
7
8
| import android.content.Context
import android.content.Intent
import android.net.Uri
fun openWebsite(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
context.startActivity(intent)
}
|
Explicit Intent
Explicit Intent opens a specific Activity inside your app.
1
2
| val intent = Intent(this, DetailsActivity::class.java)
startActivity(intent)
|
Android Manifest
The Android Manifest declares app components, permissions, package settings, and app metadata.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:theme="@style/Theme.MyApp"
android:label="@string/app_name">
<activity
android:name=".MainActivity"
android:exported="true" />
</application>
</manifest>
|
Resources
Android resources include strings, colors, drawables, dimensions, and themes.
1
2
3
4
5
| <!-- res/values/strings.xml -->
<resources>
<string name="app_name">My Android App</string>
<string name="welcome_text">Welcome to Android</string>
</resources>
|
1
| val appName = context.getString(R.string.app_name)
|
3. Jetpack Compose
Jetpack Compose is Android’s modern declarative UI toolkit. It helps build UI faster with Kotlin code instead of XML layouts.
Basic Composable
A composable function describes part of your UI.
1
2
3
4
5
6
7
| import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun Greeting() {
Text(text = "Hello Jetpack Compose")
}
|
Layout
Jetpack Compose supports layout systems based on Column, Row, and Box. These are similar to VStack, HStack, and ZStack in SwiftUI.
1
2
3
4
5
6
7
8
9
10
11
| import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun LayoutExample() {
Column {
Text(text = "Envobyte")
Text(text = "Android Developer")
}
}
|
Row
Row arranges components horizontally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.Composable
@Composable
fun RowExample() {
Row {
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Premium"
)
Text(text = "Premium User")
}
}
|
Box
Box places components on top of each other.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@Composable
fun BoxExample() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue)
) {
Text(text = "Welcome")
}
}
|
Modifier
Modifier is used to control size, padding, background, click, border, alignment, and more.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ModifierExample() {
Text(
text = "Hello Modifier",
modifier = Modifier
.background(Color.LightGray)
.padding(16.dp)
)
}
|
State with remember
remember stores state inside a composable while it is in composition.
1
2
3
4
5
6
7
8
9
10
11
12
| import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text(text = "Count: $count")
}
}
|
rememberSaveable
rememberSaveable keeps state across configuration changes like screen rotation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.*
@Composable
fun NameInput() {
var name by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = {
Text(text = "Name")
}
)
}
|
State Hoisting
State hoisting means moving state up and passing value plus event callback to child composables.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun ParentCounter() {
var count by remember { mutableStateOf(0) }
CounterButton(
count = count,
onIncrease = {
count++
}
)
}
@Composable
fun CounterButton(
count: Int,
onIncrease: () -> Unit
) {
Button(onClick = onIncrease) {
Text(text = "Count: $count")
}
}
|
LazyColumn
LazyColumn is used for efficient vertical lists.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun UserList() {
val users = listOf("Envobyte", "Alex", "John", "Sarah")
LazyColumn {
items(users) { user ->
Text(text = user)
}
}
}
|
Card
Cards are useful for modern UI sections.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PremiumCard() {
Card(
modifier = Modifier.padding(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Premium Plan")
Text(text = "Unlock all features")
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun PrimaryButton() {
Button(
onClick = {
println("Button clicked")
}
) {
Text(text = "Continue")
}
}
|
TextField
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun EmailField() {
var email by remember { mutableStateOf("") }
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = {
Text(text = "Email")
}
)
}
|
Dialog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun DeleteDialogExample() {
var showDialog by remember { mutableStateOf(false) }
Button(onClick = { showDialog = true }) {
Text(text = "Delete")
}
if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
},
title = {
Text(text = "Delete File?")
},
text = {
Text(text = "Are you sure you want to delete this file?")
},
confirmButton = {
Button(onClick = { showDialog = false }) {
Text(text = "Delete")
}
},
dismissButton = {
Button(onClick = { showDialog = false }) {
Text(text = "Cancel")
}
}
)
}
}
|
Side Effect with LaunchedEffect
LaunchedEffect runs suspend code when a composable enters composition or when its key changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun UserScreen(userId: Int) {
var username by remember { mutableStateOf("Loading...") }
LaunchedEffect(userId) {
username = "User ID: $userId"
}
Text(text = username)
}
|
DisposableEffect
DisposableEffect is useful when you need cleanup logic.
1
2
3
4
5
6
7
8
9
10
11
12
| import androidx.compose.runtime.*
@Composable
fun LifecycleObserverExample() {
DisposableEffect(Unit) {
println("Started observing")
onDispose {
println("Stopped observing")
}
}
}
|
Animation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.background
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun AnimationExample() {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.animateContentSize()
.size(if (expanded) 200.dp else 100.dp)
.background(Color.Blue)
)
Button(onClick = { expanded = !expanded }) {
Text(text = "Animate")
}
}
|
Preview
Preview helps you see UI without running the full app.
1
2
3
4
5
6
7
8
9
| import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material3.Text
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Text(text = "Preview UI")
}
|
4. XML and View System
Even though Compose is modern, many production apps still use XML and the traditional View system. A strong Android developer should understand both Compose and XML.
XML Layout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <!-- res/layout/activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello XML"
android:textSize="24sp" />
</LinearLayout>
|
Using XML in Activity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class XmlActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titleText = findViewById<TextView>(R.id.titleText)
titleText.text = "Updated Text"
}
}
|
ViewBinding
ViewBinding gives type-safe access to XML views.
1
2
3
4
5
6
7
8
9
10
11
12
13
| class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.titleText.text = "Hello ViewBinding"
}
}
|
RecyclerView
RecyclerView is used to show large scrollable lists in XML-based apps.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| data class Product(
val name: String,
val price: String
)
class ProductAdapter(
private val products: List<Product>
) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {
class ProductViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val textView = TextView(parent.context)
return ProductViewHolder(textView)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.textView.text = products[position].name
}
override fun getItemCount(): Int {
return products.size
}
}
|
5. Android Architecture
Architecture helps create scalable, testable, and maintainable apps. The most common professional approach is MVVM with Repository pattern and Clean Architecture.
MVVM
MVVM means Model, View, and ViewModel.
- Model contains data.
- View displays UI.
- ViewModel holds UI state and business logic.
1
2
3
4
5
6
7
8
9
10
11
| data class Product(
val id: Int,
val name: String,
val price: Double
)
data class ProductUiState(
val isLoading: Boolean = false,
val products: List<Product> = emptyList(),
val errorMessage: String? = null
)
|
ViewModel
ViewModel survives configuration changes and manages screen state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ProductViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ProductUiState())
val uiState: StateFlow<ProductUiState> = _uiState
fun loadProducts() {
_uiState.value = ProductUiState(
products = listOf(
Product(1, "Monthly Plan", 4.99),
Product(2, "Yearly Plan", 29.99)
)
)
}
}
|
ViewModel with Compose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ProductScreen(
viewModel: ProductViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isLoading) {
Text(text = "Loading...")
} else {
LazyColumn {
items(uiState.products) { product ->
Text(text = product.name)
}
}
}
}
|
Repository Pattern
Repository separates data logic from ViewModel.
1
2
3
4
5
6
7
8
9
10
11
12
| interface ProductRepository {
suspend fun getProducts(): List<Product>
}
class ProductRepositoryImpl : ProductRepository {
override suspend fun getProducts(): List<Product> {
return listOf(
Product(1, "Monthly Plan", 4.99),
Product(2, "Lifetime Plan", 49.99)
)
}
}
|
Use Case
Use cases keep business logic separate and reusable.
1
2
3
4
5
6
7
| class GetProductsUseCase(
private val repository: ProductRepository
) {
suspend operator fun invoke(): List<Product> {
return repository.getProducts()
}
}
|
Clean Architecture Layers
A professional Android project can be organized like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| app/
data/
repository/
remote/
local/
mapper/
domain/
model/
repository/
usecase/
presentation/
screen/
viewmodel/
state/
event/
common/
utils/
components/
|
UI State and UI Event
1
2
3
4
5
6
7
8
9
10
11
12
| data class LoginUiState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val errorMessage: String? = null
)
sealed class LoginEvent {
data class EmailChanged(val email: String) : LoginEvent()
data class PasswordChanged(val password: String) : LoginEvent()
object SubmitClicked : LoginEvent()
}
|
6. Dependency Injection
Dependency Injection means providing dependencies from outside instead of creating them directly inside a class. It improves testability and scalability.
Manual Dependency Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class ApiService {
fun fetchData(): String {
return "Data from API"
}
}
class UserRepository(
private val apiService: ApiService
) {
fun getUser(): String {
return apiService.fetchData()
}
}
fun main() {
val apiService = ApiService()
val repository = UserRepository(apiService)
println(repository.getUser())
}
|
Hilt Application Class
1
2
3
4
5
| import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()
|
Hilt Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideProductRepository(): ProductRepository {
return ProductRepositoryImpl()
}
}
|
Inject Repository into ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
| import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
fun loadProducts() {
println("Repository injected successfully")
}
}
|
Hilt Activity
1
2
3
4
5
6
7
8
9
10
| import android.os.Bundle
import androidx.activity.ComponentActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
|
7. Networking and APIs
Most Android apps communicate with backend APIs. A professional Android developer should know REST APIs, JSON parsing, authentication, multipart upload, retry logic, pagination, WebSocket, and error handling.
Retrofit API Interface
1
2
3
4
5
6
7
8
9
10
11
| import retrofit2.http.GET
data class PostDto(
val id: Int,
val title: String
)
interface ApiService {
@GET("posts")
suspend fun getPosts(): List<PostDto>
}
|
Retrofit Builder
1
2
3
4
5
6
7
8
9
10
11
12
13
| import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
|
API Call from Repository
1
2
3
4
5
6
7
| class PostRepository(
private val apiService: ApiService
) {
suspend fun getPosts(): List<PostDto> {
return apiService.getPosts()
}
}
|
OkHttp Interceptor
Interceptor is useful for adding headers like authorization token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import okhttp3.Interceptor
import okhttp3.Response
class AuthInterceptor(
private val tokenProvider: () -> String
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${tokenProvider()}")
.build()
return chain.proceed(request)
}
}
|
API Result Wrapper
1
2
3
4
5
| sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String) : ApiResult<Nothing>()
object Loading : ApiResult<Nothing>()
}
|
Safe API Call
1
2
3
4
5
6
7
8
9
| suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): ApiResult<T> {
return try {
ApiResult.Success(apiCall())
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Something went wrong")
}
}
|
Multipart Upload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface UploadApiService {
@Multipart
@POST("upload")
suspend fun uploadImage(
@Part image: MultipartBody.Part,
@Part("user_id") userId: RequestBody
): UploadResponse
}
data class UploadResponse(
val success: Boolean,
val message: String
)
|
8. Local Storage and Database
Android apps use local storage for settings, cache, user sessions, offline data, and downloaded files.
SharedPreferences
SharedPreferences stores small key-value data. It is older but still used in many apps.
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun saveUserName(context: Context, name: String) {
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit()
.putString("username", name)
.apply()
}
fun getUserName(context: Context): String? {
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
return prefs.getString("username", null)
}
|
DataStore
DataStore is the modern replacement for simple key-value storage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map
val Context.dataStore by preferencesDataStore(name = "settings")
class SettingsManager(
private val context: Context
) {
private val USERNAME_KEY = stringPreferencesKey("username")
val usernameFlow = context.dataStore.data.map { preferences ->
preferences[USERNAME_KEY] ?: "Guest"
}
suspend fun saveUsername(name: String) {
context.dataStore.edit { preferences ->
preferences[USERNAME_KEY] = name
}
}
}
|
Room Entity
Room is used for local database storage.
1
2
3
4
5
6
7
8
9
10
| import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val description: String
)
|
Room DAO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY id DESC")
fun getNotes(): Flow<List<NoteEntity>>
@Insert
suspend fun insertNote(note: NoteEntity)
@Delete
suspend fun deleteNote(note: NoteEntity)
}
|
Room Database
1
2
3
4
5
6
7
8
9
10
| import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [NoteEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
}
|
File Storage
1
2
3
4
5
6
7
8
9
10
11
| fun saveTextFile(context: Context, fileName: String, content: String) {
val file = File(context.filesDir, fileName)
file.writeText(content)
}
fun readTextFile(context: Context, fileName: String): String {
val file = File(context.filesDir, fileName)
return file.readText()
}
|
9. Coroutines and Flow
Coroutines are used for asynchronous programming. Flow is used for continuous streams of data such as database updates, download progress, or UI state.
Coroutine Scope
1
2
3
4
5
6
7
| import kotlinx.coroutines.*
fun main() {
CoroutineScope(Dispatchers.IO).launch {
println("Running in background")
}
}
|
Suspend Function
1
2
3
4
5
6
| import kotlinx.coroutines.delay
suspend fun fetchUserName(): String {
delay(1000)
return "Envobyte"
}
|
viewModelScope
viewModelScope runs coroutines inside a ViewModel and automatically cancels them when the ViewModel is cleared.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UserViewModel : ViewModel() {
fun loadUser() {
viewModelScope.launch {
val name = fetchUserName()
println(name)
}
}
}
|
Dispatchers
Dispatchers decide where coroutine work runs.
1
2
3
4
5
6
7
8
9
10
11
| viewModelScope.launch(Dispatchers.IO) {
// Database or network work
}
viewModelScope.launch(Dispatchers.Main) {
// UI-related work
}
viewModelScope.launch(Dispatchers.Default) {
// CPU-heavy work
}
|
withContext
withContext switches coroutine thread context.
1
2
3
4
5
6
7
8
| import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun loadData(): String {
return withContext(Dispatchers.IO) {
"Data loaded from background"
}
}
|
Flow
Flow emits multiple values over time.
1
2
3
4
5
6
7
8
| import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun downloadProgress(): Flow<Int> = flow {
for (progress in 1..100) {
emit(progress)
}
}
|
StateFlow
StateFlow is commonly used for UI state.
1
2
3
4
5
6
7
8
9
10
11
12
| import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increase() {
_count.value += 1
}
}
|
SharedFlow
SharedFlow is useful for one-time events like toast, snackbar, or navigation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class EventViewModel : ViewModel() {
private val _event = MutableSharedFlow<String>()
val event: SharedFlow<String> = _event
fun showMessage() {
viewModelScope.launch {
_event.emit("Action completed")
}
}
}
|
Collect Flow in Compose
1
2
3
4
5
6
7
8
9
10
11
12
| import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun CounterScreen(
viewModel: CounterViewModel
) {
val count by viewModel.count.collectAsStateWithLifecycle()
Text(text = "Count: $count")
}
|
10. Background Work
Android apps often need background processing for uploads, downloads, sync, compression, OCR, reminders, and scheduled tasks.
WorkManager
WorkManager is used for reliable deferrable background work.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.Result
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Perform background sync
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
|
Enqueue Work
1
2
3
4
5
6
7
8
| import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
fun startSync(context: Context) {
val request = OneTimeWorkRequestBuilder<SyncWorker>().build()
WorkManager.getInstance(context).enqueue(request)
}
|
Periodic Work
1
2
3
4
5
6
7
8
9
10
11
| import androidx.work.PeriodicWorkRequestBuilder
import java.util.concurrent.TimeUnit
fun schedulePeriodicSync(context: Context) {
val request = PeriodicWorkRequestBuilder<SyncWorker>(
15,
TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueue(request)
}
|
Foreground Service
Foreground service is used for ongoing visible tasks like file download, audio recording, or active processing.
1
2
3
4
5
6
7
8
9
10
11
| class DownloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Start foreground notification here
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
|
Cancel WorkManager Task
1
2
3
| fun cancelWork(context: Context, workId: UUID) {
WorkManager.getInstance(context).cancelWorkById(workId)
}
|
11. Permissions and System Features
Professional Android apps often use permissions and system features such as camera, storage, microphone, notifications, location, sharing, and deep links.
Runtime Permission
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun PermissionExample() {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
println("Permission granted: $isGranted")
}
Button(
onClick = {
launcher.launch(Manifest.permission.CAMERA)
}
) {
Text(text = "Request Camera Permission")
}
}
|
Image Picker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
@Composable
fun ImagePickerExample() {
var imageUri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
imageUri = uri
}
Button(onClick = { launcher.launch("image/*") }) {
Text(text = "Pick Image")
}
}
|
Share Text
1
2
3
4
5
6
7
8
| fun shareText(context: Context, text: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
context.startActivity(Intent.createChooser(intent, "Share using"))
}
|
Deep Link Manifest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="details" />
</intent-filter>
</activity>
|
Handle Deep Link
1
2
3
4
5
6
| override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val data = intent?.data
println("Deep link: $data")
}
|
Notification
1
2
3
4
5
6
7
8
9
10
| fun showNotification(context: Context) {
val notification = NotificationCompat.Builder(context, "default_channel")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Task Complete")
.setContentText("Your file has been processed successfully.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
NotificationManagerCompat.from(context).notify(1, notification)
}
|
12. Security
Security is extremely important for production Android apps. Sensitive data should never be stored in plain text.
Store Token Securely
Use secure storage for tokens, not normal SharedPreferences.
1
2
3
4
5
6
7
8
9
10
11
12
13
| class TokenManager(
private val encryptedPrefs: SharedPreferences
) {
fun saveToken(token: String) {
encryptedPrefs.edit()
.putString("auth_token", token)
.apply()
}
fun getToken(): String? {
return encryptedPrefs.getString("auth_token", null)
}
}
|
Use HTTPS
1
| const val BASE_URL = "https://api.example.com/"
|
Avoid Hardcoding Secrets
1
2
3
4
5
| // Bad
const val API_KEY = "secret-key-here"
// Better
// Keep sensitive keys on backend or use secure remote configuration.
|
Hashing Example
1
2
3
4
5
6
7
8
9
10
11
12
| import java.security.MessageDigest
fun sha256(text: String): String {
val bytes = MessageDigest.getInstance("SHA-256")
.digest(text.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
fun main() {
println(sha256("Hello"))
}
|
Basic Request Signing Payload
1
2
3
4
5
6
7
8
9
10
11
12
13
| data class SecureRequestPayload(
val path: String,
val timestamp: Long,
val nonce: String,
val body: String
)
val payload = SecureRequestPayload(
path = "/api/order",
timestamp = System.currentTimeMillis(),
nonce = java.util.UUID.randomUUID().toString(),
body = """{"order_id":123}"""
)
|
13. Build System and Gradle
A professional Android developer must understand Gradle, build variants, dependencies, signing configs, product flavors, and CI/CD.
Module Gradle Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.myapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
}
|
Build Types
1
2
3
4
5
6
7
8
9
10
11
12
| android {
buildTypes {
debug {
isDebuggable = true
}
release {
isMinifyEnabled = true
isShrinkResources = true
}
}
}
|
Product Flavors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| android {
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
}
create("prod") {
dimension = "environment"
}
}
}
|
BuildConfig
1
2
3
4
5
6
7
8
9
| android {
defaultConfig {
buildConfigField(
"String",
"BASE_URL",
"\"https://api.example.com/\""
)
}
}
|
1
| val baseUrl = BuildConfig.BASE_URL
|
14. App Release and Play Store
A real Android developer should understand release engineering and Google Play publishing.
Important Release Skills
You should learn:
- App signing
- Keystore
- Version code
- Version name
- Build variants
- ProGuard/R8
- Play Console
- Internal testing
- Closed testing
- Open testing
- Production release
- Privacy policy
- Data safety form
- Crash reporting
- Analytics
- In-app updates
- In-app review
- Billing setup
Version Info
1
2
3
4
5
6
| fun printAppVersion(context: Context) {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
println("Version name: ${packageInfo.versionName}")
println("Version code: ${packageInfo.longVersionCode}")
}
|
15. Debugging Skills
Outstanding developers are strong debuggers. They can quickly find crashes, performance issues, memory leaks, and wrong app behavior.
Logcat
1
2
3
4
| import android.util.Log
Log.d("HomeScreen", "User opened home screen")
Log.e("ApiError", "Failed to load data")
|
Debug Only Code
1
2
3
| if (BuildConfig.DEBUG) {
Log.d("Debug", "This log only appears in debug builds")
}
|
Custom Logger
1
2
3
4
5
6
7
8
9
| object AppLogger {
fun debug(tag: String, message: String) {
if (BuildConfig.DEBUG) {
Log.d(tag, message)
}
}
}
AppLogger.debug("Login", "Login button clicked")
|
You should master:
- Logcat
- Android Studio Debugger
- Breakpoints
- Layout Inspector
- Database Inspector
- Network Inspector
- Profiler
- Memory Profiler
- CPU Profiler
- APK Analyzer
- Crashlytics
- StrictMode
16. Testing
Testing makes Android apps stable and professional. A strong Android developer should write unit tests, UI tests, integration tests, and repository tests.
Unit Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import org.junit.Assert.assertEquals
import org.junit.Test
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
}
class CalculatorTest {
@Test
fun add_returnsCorrectResult() {
val calculator = Calculator()
val result = calculator.add(2, 3)
assertEquals(5, result)
}
}
|
ViewModel Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse
import org.junit.Test
class LoginViewModel {
fun isValidEmail(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
}
class LoginViewModelTest {
@Test
fun validEmail_returnsTrue() {
val viewModel = LoginViewModel()
assertTrue(viewModel.isValidEmail("test@example.com"))
}
@Test
fun invalidEmail_returnsFalse() {
val viewModel = LoginViewModel()
assertFalse(viewModel.isValidEmail("wrong-email"))
}
}
|
Mock Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| interface UserRepository {
fun getUsers(): List<String>
}
class FakeUserRepository : UserRepository {
override fun getUsers(): List<String> {
return listOf("Envobyte", "John")
}
}
class UserListViewModel(
private val repository: UserRepository
) {
fun loadUsers(): List<String> {
return repository.getUsers()
}
}
|
Compose UI Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @get:Rule
val composeTestRule = createComposeRule()
@Test
fun button_showsCorrectText() {
composeTestRule.setContent {
Button(onClick = {}) {
Text(text = "Continue")
}
}
composeTestRule
.onNodeWithText("Continue")
.assertExists()
}
|
Performance is one of the biggest differences between beginner and senior Android developers.
Avoid Unnecessary Recomposition
Pass only the required state to composables.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| data class HomeUiState(
val username: String = "",
val downloadProgress: Int = 0,
val isLoading: Boolean = false
)
@Composable
fun HomeScreen(uiState: HomeUiState) {
UserHeader(username = uiState.username)
DownloadProgress(progress = uiState.downloadProgress)
}
@Composable
fun UserHeader(username: String) {
Text(text = username)
}
@Composable
fun DownloadProgress(progress: Int) {
Text(text = "Progress: $progress%")
}
|
Use remember for Expensive Calculation
1
2
3
4
5
6
7
8
| @Composable
fun ExpensiveCalculationScreen(numbers: List<Int>) {
val total = remember(numbers) {
numbers.sum()
}
Text(text = "Total: $total")
}
|
Use LazyColumn for Large Lists
1
2
3
4
5
6
7
8
| @Composable
fun LargeListScreen() {
LazyColumn {
items(1000) { index ->
Text(text = "Item $index")
}
}
}
|
Image Loading with Coil
1
2
3
4
5
6
7
8
9
| import coil.compose.AsyncImage
@Composable
fun UserAvatar(imageUrl: String) {
AsyncImage(
model = imageUrl,
contentDescription = "User avatar"
)
}
|
Stable Data Class
Use immutable UI state when possible.
1
2
3
4
5
| data class ProfileUiState(
val name: String,
val imageUrl: String,
val isPremium: Boolean
)
|
Avoid Heavy Work on Main Thread
1
2
3
4
5
6
7
8
| viewModelScope.launch {
val result = withContext(Dispatchers.Default) {
// CPU-heavy work
processLargeData()
}
println(result)
}
|
18. Material Design and UX Thinking
A great Android developer does not only write code. They also understand user experience, accessibility, consistency, and Material Design.
1
2
3
4
5
6
7
8
9
10
11
| @Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick
) {
Text(text = text)
}
}
|
Empty State
1
2
3
4
5
6
7
| @Composable
fun EmptyStateView() {
Column {
Text(text = "No Files Yet")
Text(text = "Your scanned files will appear here after you create them.")
}
}
|
Loading State
1
2
3
4
5
6
7
| @Composable
fun LoadingView() {
Column {
CircularProgressIndicator()
Text(text = "Processing...")
}
}
|
Error State
1
2
3
4
5
6
7
| @Composable
fun ErrorStateView(message: String) {
Column {
Text(text = "Something went wrong")
Text(text = message)
}
}
|
Accessibility
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Composable
fun AccessibleFavoriteButton() {
IconButton(
onClick = {
println("Favorite clicked")
}
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Add to favorites"
)
}
}
|
19. Advanced Android Topics
After learning the core skills, you can move to advanced Android development.
CameraX
CameraX is used for camera preview, image capture, video recording, and analysis.
1
2
3
4
5
| // Common use cases:
// - Preview
// - ImageCapture
// - VideoCapture
// - ImageAnalysis
|
ML Kit
ML Kit is useful for OCR, barcode scanning, translation, face detection, and image labeling.
1
2
3
4
5
6
| // Common use cases:
// - Text recognition
// - Face detection
// - Barcode scanning
// - Translation
// - Object detection
|
Media3 ExoPlayer is used for audio and video playback.
1
2
3
4
5
| // Common use cases:
// - Video player
// - Audio player
// - Streaming playback
// - Playback controls
|
Billing
Google Play Billing is used for subscriptions, in-app purchases, trials, and premium features.
1
2
3
4
5
6
7
| // Common flow:
// 1. Connect BillingClient
// 2. Query products
// 3. Launch billing flow
// 4. Verify purchase
// 5. Acknowledge purchase
// 6. Unlock premium feature
|
Firebase
Firebase is useful for analytics, crash reporting, remote config, authentication, Firestore, FCM, and cloud functions.
1
2
3
4
5
6
7
| // Common Firebase tools:
// - Firebase Analytics
// - Firebase Crashlytics
// - Firebase Remote Config
// - Firebase Firestore
// - Firebase Authentication
// - Firebase Cloud Messaging
|
Multi-Module Architecture
Large apps should be split into modules.
1
2
3
4
5
6
7
8
9
| :app
:core:common
:core:network
:core:database
:core:designsystem
:feature:home
:feature:downloads
:feature:settings
:feature:premium
|
KMP
Kotlin Multiplatform allows sharing business logic between Android and iOS.
1
2
3
4
5
6
7
| // Shared layer can include:
// - Models
// - Repository interfaces
// - Use cases
// - Validation logic
// - API clients
// - Business rules
|
20. Real Project Practice
The fastest way to become outstanding is to build real apps.
You should build Android apps that include:
- Authentication
- API calling
- Local database
- Offline support
- File handling
- Image picker
- Camera
- Permissions
- Background task
- Notification
- Payment screen
- Subscription logic
- Clean architecture
- Jetpack Compose UI
- Error handling
- Loading state
- Empty state
- Testing
- Release build
Mini Project Example: Notes App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| data class Note(
val id: Int,
val title: String,
val description: String
)
data class NotesUiState(
val notes: List<Note> = emptyList(),
val title: String = "",
val description: String = ""
)
class NotesViewModel : ViewModel() {
private val _uiState = MutableStateFlow(NotesUiState())
val uiState: StateFlow<NotesUiState> = _uiState
fun onTitleChanged(title: String) {
_uiState.value = _uiState.value.copy(title = title)
}
fun onDescriptionChanged(description: String) {
_uiState.value = _uiState.value.copy(description = description)
}
fun addNote() {
val currentState = _uiState.value
val note = Note(
id = currentState.notes.size + 1,
title = currentState.title,
description = currentState.description
)
_uiState.value = currentState.copy(
notes = currentState.notes + note,
title = "",
description = ""
)
}
}
|
Notes App UI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| @Composable
fun NotesScreen(
viewModel: NotesViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Column(
modifier = Modifier.padding(16.dp)
) {
OutlinedTextField(
value = uiState.title,
onValueChange = viewModel::onTitleChanged,
label = {
Text(text = "Title")
}
)
OutlinedTextField(
value = uiState.description,
onValueChange = viewModel::onDescriptionChanged,
label = {
Text(text = "Description")
}
)
Button(
onClick = {
viewModel.addNote()
}
) {
Text(text = "Add Note")
}
LazyColumn {
items(uiState.notes) { note ->
Column(
modifier = Modifier.padding(vertical = 8.dp)
) {
Text(text = note.title)
Text(text = note.description)
}
}
}
}
}
|
Recommended Learning Order
- Kotlin basics
- Kotlin null safety
- Kotlin OOP
- Lambda and higher-order functions
- Collections
- Sealed class and data class
- Coroutines basics
- Flow and StateFlow
- Android fundamentals
- Activity lifecycle
- Intent and permissions
- Jetpack Compose basics
- Compose state management
- Navigation
- ViewModel
- MVVM architecture
- Repository pattern
- Clean Architecture
- Hilt dependency injection
- Retrofit and OkHttp
- Room database
- DataStore
- WorkManager
- Foreground service
- Notifications
- File handling
- Security
- Testing
- Debugging
- Performance optimization
- Gradle and build variants
- Play Store release
- Firebase
- Billing
- Advanced topics
- Real project development
The best way to improve is to build real apps repeatedly and solve real production problems.