• Latest Code...

    Featured Post

    Implementing Hilt in a Kotlin Android Jetpack Compose Project with MVVM Architecture

     In modern Android development, maintaining a scalable codebase can be challenging, especially when it comes to dependency management. Hilt,...

    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!

    Contact Form

    Name

    Email *

    Message *