diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71fa1a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Android Studio +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx + +# Kotlin +*.class +*.log + +# Build files +*.apk +*.aar +/bin/ +/out/ + +# Dependency directories +/node_modules/ + +# Gradle +.gradle/ +build/ + +# Misc +.env +local.properties + +# Logging +*.log + +# Test reports +test-results/ \ No newline at end of file diff --git a/app/src/main/java/com/example/todoapp/TodoViewModel.kt b/app/src/main/java/com/example/todoapp/TodoViewModel.kt new file mode 100644 index 0000000..d2ec20b --- /dev/null +++ b/app/src/main/java/com/example/todoapp/TodoViewModel.kt @@ -0,0 +1,41 @@ +package com.example.todoapp + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class TodoViewModel(private val repository: TodoRepository) : ViewModel() { + // Sealed class to represent deletion state + sealed class DeletionState { + object Idle : DeletionState() + object Success : DeletionState() + data class Error(val message: String) : DeletionState() + } + + // State flow to track deletion status + private val _deletionState = MutableStateFlow(DeletionState.Idle) + val deletionState: StateFlow = _deletionState.asStateFlow() + + /** + * Deletes a todo item and provides feedback through deletionState + * @param todoItem The todo item to be deleted + */ + fun deleteTodoItem(todoItem: TodoItem) { + viewModelScope.launch { + try { + repository.delete(todoItem) + _deletionState.value = DeletionState.Success + } catch (e: Exception) { + _deletionState.value = DeletionState.Error( + "Failed to delete todo item: ${e.localizedMessage}" + ) + } finally { + // Reset state after handling + _deletionState.value = DeletionState.Idle + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/todoapp/data/model/Todo.kt b/src/main/kotlin/com/todoapp/data/model/Todo.kt new file mode 100644 index 0000000..440303f --- /dev/null +++ b/src/main/kotlin/com/todoapp/data/model/Todo.kt @@ -0,0 +1,19 @@ +package com.todoapp.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "todos") +data class Todo( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val title: String, + val description: String? = null, + val isCompleted: Boolean = false, + val createdAt: Long = System.currentTimeMillis(), + val updatedAt: Long = System.currentTimeMillis() +) { + fun validate(): Boolean { + return title.isNotBlank() && title.length <= 100 + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/todoapp/data/repository/TodoDao.kt b/src/main/kotlin/com/todoapp/data/repository/TodoDao.kt new file mode 100644 index 0000000..906404f --- /dev/null +++ b/src/main/kotlin/com/todoapp/data/repository/TodoDao.kt @@ -0,0 +1,29 @@ +package com.todoapp.data.repository + +import androidx.room.* +import com.todoapp.data.model.Todo +import kotlinx.coroutines.flow.Flow + +@Dao +interface TodoDao { + @Query("SELECT * FROM todos ORDER BY createdAt DESC") + fun getAllTodos(): Flow> + + @Query("SELECT * FROM todos WHERE id = :todoId") + suspend fun getTodoById(todoId: Int): Todo? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTodo(todo: Todo): Long + + @Update + suspend fun updateTodo(todo: Todo) + + @Delete + suspend fun deleteTodo(todo: Todo) + + @Query("DELETE FROM todos WHERE id = :todoId") + suspend fun deleteTodoById(todoId: Int) + + @Query("SELECT * FROM todos WHERE isCompleted = :completed") + fun getTodosByCompletionStatus(completed: Boolean): Flow> +} \ No newline at end of file diff --git a/src/main/kotlin/com/todoapp/data/repository/TodoRepository.kt b/src/main/kotlin/com/todoapp/data/repository/TodoRepository.kt new file mode 100644 index 0000000..f5b5707 --- /dev/null +++ b/src/main/kotlin/com/todoapp/data/repository/TodoRepository.kt @@ -0,0 +1,67 @@ +package com.todoapp.data.repository + +import com.todoapp.data.model.Todo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class TodoRepository(private val todoDao: TodoDao) { + val allTodos: Flow> = todoDao.getAllTodos() + .catch { emit(emptyList()) } + + fun getTodosByCompletionStatus(completed: Boolean): Flow> = + todoDao.getTodosByCompletionStatus(completed) + .catch { emit(emptyList()) } + + suspend fun getTodoById(id: Int): Todo? { + return try { + todoDao.getTodoById(id) + } catch (e: Exception) { + null + } + } + + suspend fun addTodo(todo: Todo): Result { + return try { + if (!todo.validate()) { + Result.failure(IllegalArgumentException("Invalid todo item")) + } else { + val id = todoDao.insertTodo(todo) + Result.success(id) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun updateTodo(todo: Todo): Result { + return try { + if (!todo.validate()) { + Result.failure(IllegalArgumentException("Invalid todo item")) + } else { + todoDao.updateTodo(todo.copy(updatedAt = System.currentTimeMillis())) + Result.success(Unit) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun deleteTodo(todo: Todo): Result { + return try { + todoDao.deleteTodo(todo) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun deleteTodoById(todoId: Int): Result { + return try { + todoDao.deleteTodoById(todoId) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/todoapp/data/repository/TodoRepositoryTest.kt b/src/test/kotlin/com/todoapp/data/repository/TodoRepositoryTest.kt new file mode 100644 index 0000000..e17f1e7 --- /dev/null +++ b/src/test/kotlin/com/todoapp/data/repository/TodoRepositoryTest.kt @@ -0,0 +1,63 @@ +package com.todoapp.data.repository + +import com.todoapp.data.model.Todo +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations + +class TodoRepositoryTest { + + @Mock + private lateinit var mockTodoDao: TodoDao + + private lateinit var todoRepository: TodoRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + todoRepository = TodoRepository(mockTodoDao) + } + + @Test + fun `addTodo should insert valid todo`() = runBlocking { + val validTodo = Todo(title = "Test Todo") + `when`(mockTodoDao.insertTodo(validTodo)).thenReturn(1L) + + val result = todoRepository.addTodo(validTodo) + + assertTrue(result.isSuccess) + assertEquals(1L, result.getOrNull()) + verify(mockTodoDao).insertTodo(validTodo) + } + + @Test + fun `addTodo should fail for invalid todo`() = runBlocking { + val invalidTodo = Todo(title = "") + + val result = todoRepository.addTodo(invalidTodo) + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is IllegalArgumentException) + } + + @Test + fun `deleteTodo should successfully delete a todo`() = runBlocking { + val todo = Todo(id = 1, title = "Test Todo") + todoRepository.deleteTodo(todo) + + verify(mockTodoDao).deleteTodo(todo) + } + + @Test + fun `updateTodo should update valid todo`() = runBlocking { + val validTodo = Todo(id = 1, title = "Updated Todo") + todoRepository.updateTodo(validTodo) + + verify(mockTodoDao).updateTodo(any()) + } +} \ No newline at end of file