An overview of common LeetCode-style Android engineer coding challenges in Kotlin, incorporating the latest UI and technologies in Android development, such as Jetpack Compose, Kotlin Coroutines, and Flow.
These challenges simulate real-world scenarios and test problem-solving skills, algorithmic thinking, and practical Android development knowledge.
1. Challenge: Implement a Task Manager
Problem Statement
Create an Android app using Jetpack Compose that allows users to manage tasks. Users can:
Features Implemented:
- Add tasks with title and description.
- Toggle task completion status.
- Delete tasks.
- Filter tasks by All, Completed, or Pending states.
Technologies Used:
- Jetpack Compose for modern UI development.
- Room Database for persistent local storage.
- StateFlow for reactive state updates.
- ViewModel for managing UI state and logic.
Additional Enhancements:
- Unit tests for ViewModel logic using JUnit.
- Snackbar notifications for task actions.
Solution Outline
We'll use the following:
- Jetpack Compose for UI.
- ViewModel to manage state.
- StateFlow for reactive state updates.
- Room Database for local storage.
Code Implementation
// File: TaskManager.kt
package com.example.taskmanager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.room.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
// Data Model
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val description: String,
val isCompleted: Boolean = false
)
// DAO
@Dao
interface TaskDao {
@Insert
suspend fun insertTask(task: Task)
@Update
suspend fun updateTask(task: Task)
@Delete
suspend fun deleteTask(task: Task)
@Query("SELECT * FROM tasks")
fun getAllTasks(): List<Task>
}
// Database
@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
// ViewModel
class TaskViewModel(private val taskDao: TaskDao) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> get() = _tasks
init {
fetchTasks()
}
private fun fetchTasks() {
viewModelScope.launch {
_tasks.value = taskDao.getAllTasks()
}
}
fun addTask(title: String, description: String) {
viewModelScope.launch {
taskDao.insertTask(Task(title = title, description = description))
fetchTasks()
}
}
fun toggleCompletion(task: Task) {
viewModelScope.launch {
taskDao.updateTask(task.copy(isCompleted = !task.isCompleted))
fetchTasks()
}
}
fun deleteTask(task: Task) {
viewModelScope.launch {
taskDao.deleteTask(task)
fetchTasks()
}
}
}
// ViewModel Factory
class TaskViewModelFactory(private val taskDao: TaskDao) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TaskViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return TaskViewModel(taskDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// UI with Jetpack Compose
@Composable
fun TaskManagerUI(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsState()
var title by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
Column(Modifier.padding(16.dp)) {
Row(Modifier.fillMaxWidth()) {
BasicTextField(value = title, onValueChange = { title = it }, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(8.dp))
BasicTextField(value = description, onValueChange = { description = it }, modifier = Modifier.weight(2f))
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
viewModel.addTask(title, description)
title = ""
description = ""
}) {
Text("Add Task")
}
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(tasks) { task ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(task.title)
Row {
Checkbox(checked = task.isCompleted, onCheckedChange = {
viewModel.toggleCompletion(task)
})
Button(onClick = { viewModel.deleteTask(task) }) {
Text("Delete")
}
}
}
}
}
}
}
// Entry Point
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "task-db"
).build()
val taskDao = db.taskDao()
val viewModel: TaskViewModel by viewModels { TaskViewModelFactory(taskDao) }
setContent {
TaskManagerUI(viewModel)
}
}
}
@Preview(showBackground = true)
@Composable
fun TaskManagerPreview() {
// Empty preview
TaskManagerUI(viewModel())
}
Features Highlighted
- Jetpack Compose: Declarative and modern UI toolkit for Kotlin.
- Room: Manages persistent data.
- StateFlow: Reactive state management with Kotlin Coroutines.
- ViewModel: Decoupled business logic.
a. Unit Tests for TaskViewModel
Unit tests for TaskViewModel
can be implemented using JUnit and a mock of the TaskDao
. Here's the code:
// File: TaskViewModelTest.kt
package com.example.taskmanager
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.*
@OptIn(ExperimentalCoroutinesApi::class)
class TaskViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = StandardTestDispatcher()
private lateinit var mockTaskDao: TaskDao
private lateinit var taskViewModel: TaskViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
mockTaskDao = mock(TaskDao::class.java)
taskViewModel = TaskViewModel(mockTaskDao)
}
@Test
fun `addTask adds a new task and updates task list`() = runTest {
// Arrange
val task = Task(title = "Test Task", description = "Test Description")
`when`(mockTaskDao.getAllTasks()).thenReturn(listOf(task))
// Act
taskViewModel.addTask(task.title, task.description)
advanceUntilIdle()
// Assert
val tasks = taskViewModel.tasks.first()
assertEquals(1, tasks.size)
assertEquals("Test Task", tasks[0].title)
verify(mockTaskDao).insertTask(any(Task::class.java))
}
@Test
fun `toggleCompletion toggles the task's completed state`() = runTest {
// Arrange
val task = Task(id = 1, title = "Task", description = "Description", isCompleted = false)
`when`(mockTaskDao.getAllTasks()).thenReturn(listOf(task))
// Act
taskViewModel.toggleCompletion(task)
advanceUntilIdle()
// Assert
verify(mockTaskDao).updateTask(task.copy(isCompleted = true))
}
@Test
fun `deleteTask removes the task from the list`() = runTest {
// Arrange
val task = Task(id = 1, title = "Task", description = "Description")
`when`(mockTaskDao.getAllTasks()).thenReturn(listOf())
// Act
taskViewModel.deleteTask(task)
advanceUntilIdle()
// Assert
val tasks = taskViewModel.tasks.first()
assertEquals(0, tasks.size)
verify(mockTaskDao).deleteTask(task)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}
b. Filtering Tasks by State (Completed/Pending)
Add filtering functionality to the UI, using a state variable and the TaskViewModel
. Here's the updated code:
Update ViewModel
// File: TaskViewModel.kt
enum class FilterType { ALL, COMPLETED, PENDING }
class TaskViewModel(private val taskDao: TaskDao) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> get() = _tasks
private val _filter = MutableStateFlow(FilterType.ALL)
val filteredTasks = _tasks.combine(_filter) { tasks, filter ->
when (filter) {
FilterType.ALL -> tasks
FilterType.COMPLETED -> tasks.filter { it.isCompleted }
FilterType.PENDING -> tasks.filter { !it.isCompleted }
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
// Filter setter
fun setFilter(filter: FilterType) {
_filter.value = filter
}
}
Update UI
// File: TaskManagerUI.kt
@Composable
fun TaskManagerUI(viewModel: TaskViewModel) {
val tasks by viewModel.filteredTasks.collectAsState()
var title by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
Column(Modifier.padding(16.dp)) {
Row(Modifier.fillMaxWidth()) {
BasicTextField(value = title, onValueChange = { title = it }, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(8.dp))
BasicTextField(value = description, onValueChange = { description = it }, modifier = Modifier.weight(2f))
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
viewModel.addTask(title, description)
title = ""
description = ""
}) {
Text("Add Task")
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { viewModel.setFilter(FilterType.ALL) }) { Text("All") }
Button(onClick = { viewModel.setFilter(FilterType.COMPLETED) }) { Text("Completed") }
Button(onClick = { viewModel.setFilter(FilterType.PENDING) }) { Text("Pending") }
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(tasks) { task ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(task.title)
Row {
Checkbox(checked = task.isCompleted, onCheckedChange = {
viewModel.toggleCompletion(task)
})
Button(onClick = { viewModel.deleteTask(task) }) {
Text("Delete")
}
}
}
}
}
}
}
Summary of Updates
- Unit Tests ensure the ViewModel logic functions correctly.
- Filtering adds an interactive feature for better usability.
Suggestions:
a. Refactor filtering logic into a separate function for better modularity.
b. Add a Snackbar
to notify users when tasks are added, updated, or deleted.
Happy coding!
What’s your favorite part of this article? Share in the comments below!