All topics
Mobile · Learning hub

Android notes for developers

Master Android with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Mobile notes
Android

Jetpack Compose UI

Android: Jetpack Compose UI Jetpack Compose is Android's modern declarative UI toolkit. Instead of XML layouts, you write Kotlin functions annotated with @Compo

Android: Jetpack Compose UI

Jetpack Compose is Android's modern declarative UI toolkit. Instead of XML layouts, you write Kotlin functions annotated with @Composable that describe the UI. Compose re-runs composables when state changes — no manual view updates.

Composable Functions

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

// Stateful composable — owns and manages state
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Count: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// Stateless composable — receives state as params (easier to test/reuse)
@Composable
fun CounterDisplay(count: Int, onIncrement: () -> Unit) {
    Column {
        Text("Count: $count")
        Button(onClick = onIncrement) { Text("Increment") }
    }
}

// Preview
@Preview(showBackground = true)
@Composable
fun CounterPreview() {
    MyAppTheme { Counter() }
}

Layout Composables

// Column — vertical stack
Column(
    modifier = Modifier.fillMaxWidth().padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("Item 1")
    Text("Item 2")
}

// Row — horizontal stack
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) {
    Text("Left")
    Icon(Icons.Default.ArrowForward, contentDescription = null)
}

// Box — overlapping (Z-stack)
Box(modifier = Modifier.fillMaxSize()) {
    Image(painter = painterResource(R.drawable.bg), contentDescription = null,
        modifier = Modifier.fillMaxSize())
    Text("Overlay text", modifier = Modifier.align(Alignment.Center))
}

// LazyColumn — scrollable list (RecyclerView equivalent)
LazyColumn(contentPadding = PaddingValues(16.dp)) {
    items(items, key = { it.id }) { item ->
        ItemCard(item = item, onClick = { onItemClick(item) })
    }
    item { Spacer(modifier = Modifier.height(80.dp)) }  // bottom padding
}

Modifier

// Modifier chains — order matters
Modifier
    .fillMaxSize()                         // fill parent
    .fillMaxWidth().height(56.dp)          // fixed height, full width
    .size(48.dp)                           // square
    .padding(horizontal = 16.dp, vertical = 8.dp)
    .background(MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp))
    .clip(RoundedCornerShape(8.dp))        // clip to shape
    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
    .clickable { onClick() }
    .clickable(onClick = onClick, role = Role.Button)
    .semantics { contentDescription = "Increment counter" }  // accessibility
    .testTag("counter_button")             // UI testing
    .weight(1f)                            // in Row/Column: take remaining space

Common Components

// Text field
var text by remember { mutableStateOf("") }
OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Email") },
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
    isError = !text.contains("@"),
    supportingText = { if (!text.contains("@")) Text("Invalid email") }
)

// Image loading (Coil)
// implementation("io.coil-kt:coil-compose:2.6.0")
AsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = "Profile photo",
    modifier = Modifier.size(40.dp).clip(CircleShape),
    contentScale = ContentScale.Crop,
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error),
)

// Scaffold — standard screen layout
Scaffold(
    topBar = {
        TopAppBar(
            title = { Text("My App") },
            navigationIcon = {
                IconButton(onClick = { navController.popBackStack() }) {
                    Icon(Icons.Default.ArrowBack, contentDescription = "Back")
                }
            }
        )
    },
    floatingActionButton = {
        FloatingActionButton(onClick = { /* add item */ }) {
            Icon(Icons.Default.Add, contentDescription = "Add")
        }
    }
) { paddingValues ->
    Content(modifier = Modifier.padding(paddingValues))
}
Android

Architecture: ViewModel, StateFlow & Room

Android: Architecture ViewModel, StateFlow & Room ViewModel & StateFlow // ViewModel — survives configuration changes (rotation) // implementation("androidx.lif

Android: Architecture ViewModel, StateFlow & Room

ViewModel & StateFlow

// ViewModel — survives configuration changes (rotation)
// implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

class ArticlesViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    // UI state
    private val _uiState = MutableStateFlow(ArticlesUiState())
    val uiState: StateFlow<ArticlesUiState> = _uiState.asStateFlow()

    init {
        loadArticles()
    }

    fun loadArticles() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val articles = repository.getArticles()
                _uiState.update { it.copy(articles = articles, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }

    fun deleteArticle(id: Int) {
        viewModelScope.launch {
            repository.deleteArticle(id)
            loadArticles()
        }
    }
}

data class ArticlesUiState(
    val articles: List<Article> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
)

// In Composable
@Composable
fun ArticlesScreen(viewModel: ArticlesViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> CircularProgressIndicator()
        uiState.error != null -> Text("Error: ${uiState.error}")
        else -> ArticlesList(articles = uiState.articles)
    }
}

Room Database

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

@Entity(tableName = "articles")
data class ArticleEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val content: String,
    val createdAt: Long = System.currentTimeMillis(),
)

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY createdAt DESC")
    fun getAllArticles(): Flow<List<ArticleEntity>>   // Flow = reactive stream

    @Query("SELECT * FROM articles WHERE id = :id")
    suspend fun getArticleById(id: Int): ArticleEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(article: ArticleEntity)

    @Update
    suspend fun update(article: ArticleEntity)

    @Delete
    suspend fun delete(article: ArticleEntity)

    @Query("DELETE FROM articles WHERE id = :id")
    suspend fun deleteById(id: Int)
}

@Database(entities = [ArticleEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun articleDao(): ArticleDao

    companion object {
        @Volatile private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build().also { INSTANCE = it }
            }
        }
    }
}

Repository Pattern

class ArticleRepository(private val dao: ArticleDao, private val api: ArticleApi) {

    // Combine local DB (offline-first) with network
    val articles: Flow<List<Article>> = dao.getAllArticles()
        .map { entities -> entities.map { it.toArticle() } }

    suspend fun refreshArticles() {
        val remoteArticles = api.getArticles()
        dao.insertAll(remoteArticles.map { it.toEntity() })
    }

    suspend fun deleteArticle(id: Int) {
        dao.deleteById(id)
    }
}

Keep your Android knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever