Code Challenge: Implement a Task Manager in Kotlin Android

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:

    1. Add tasks with title and description.
    2. Toggle task completion status.
    3. Delete tasks.
    4. Filter tasks by All, Completed, or Pending states.

Technologies Used:

    1. Jetpack Compose for modern UI development.
    2. Room Database for persistent local storage.
    3. StateFlow for reactive state updates.
    4. ViewModel for managing UI state and logic.

Additional Enhancements:

    1. Unit tests for ViewModel logic using JUnit.
    2. 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

  1. Jetpack Compose: Declarative and modern UI toolkit for Kotlin.
  2. Room: Manages persistent data.
  3. StateFlow: Reactive state management with Kotlin Coroutines.
  4. 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 -&gt;
                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

  1. Unit Tests ensure the ViewModel logic functions correctly.
  2. 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!

0 comments:

Post a Comment