diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..221c1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +<<<<<<< HEAD +# Gradle files +.gradle/ +build/ + +# Local configuration file +local.properties + +# Android Studio generated files +*.iml +.idea/ +======= +# Android Studio +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild + +# Gradle files +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache +>>>>>>> pr-7-Ralfmal-kotlinTodoApp + +# Compiled class files +*.class + +# Log files +*.log + +<<<<<<< HEAD +# Android generated files +bin/ +gen/ +out/ + +# Dependency directories +/captures +.externalNativeBuild +.cxx + +# macOS system files +.DS_Store + +# Backup files +*.bak +*.swp + +# Kotlin build artifacts +*.jar +*.war +*.ear +======= +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +>>>>>>> pr-7-Ralfmal-kotlinTodoApp + +# Virtual machine crash logs +hs_err_pid* + +<<<<<<< HEAD +# Android Profiling +*.hprof +======= +# Android specific +bin/ +gen/ +out/ +release/ + +# Android Profiling +*.hprof + +# Dependency directories +/node_modules +/jspm_packages + +# Editor directories and files +.idea/ +.vscode/ +*.swp +*.swo + +# Environment files +.env + +# Secrets +*.key +secrets.json + +# Build output +/build +/dist +/target +>>>>>>> pr-7-Ralfmal-kotlinTodoApp diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b05de59 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + id("com.android.application") +<<<<<<< HEAD + id("org.jetbrains.kotlin.android") +======= + id("kotlin-android") +>>>>>>> pr-7-Ralfmal-kotlinTodoApp + id("kotlin-kapt") +} + +android { +<<<<<<< HEAD + namespace = "com.todoapp" + compileSdk = 33 + + defaultConfig { + applicationId = "com.todoapp" +======= + compileSdk = 33 + defaultConfig { + applicationId = "com.example.todoapp" +>>>>>>> pr-7-Ralfmal-kotlinTodoApp + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" +<<<<<<< HEAD + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + // Room Database + implementation("androidx.room:room-runtime:2.5.1") + implementation("androidx.room:room-ktx:2.5.1") + kapt("androidx.room:room-compiler:2.5.1") + + // Kotlin standard library + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.20") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20") + + // Android testing +======= + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + // Room dependencies + val roomVersion = "2.5.1" + implementation("androidx.room:room-runtime:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + kapt("androidx.room:room-compiler:$roomVersion") + + // Testing dependencies + testImplementation("androidx.room:room-testing:$roomVersion") + testImplementation("junit:junit:4.13.2") +>>>>>>> pr-7-Ralfmal-kotlinTodoApp + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/app/src/main/java/com/example/todoapp/data/dao/TodoItemDao.kt b/app/src/main/java/com/example/todoapp/data/dao/TodoItemDao.kt new file mode 100644 index 0000000..166051b --- /dev/null +++ b/app/src/main/java/com/example/todoapp/data/dao/TodoItemDao.kt @@ -0,0 +1,80 @@ +package com.example.todoapp.data.dao + +import androidx.room.* +import com.example.todoapp.data.model.TodoItem +import kotlinx.coroutines.flow.Flow + +@Dao +interface TodoItemDao { + /** + * Inserts a new todo item into the database. + * @param todoItem The todo item to insert + * @return The ID of the newly inserted item + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(todoItem: TodoItem): Long + + /** + * Inserts multiple todo items into the database. + * @param todoItems The list of todo items to insert + * @return List of inserted item IDs + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(todoItems: List): List + + /** + * Updates an existing todo item. + * @param todoItem The todo item with updated information + */ + @Update + suspend fun update(todoItem: TodoItem) + + /** + * Deletes a specific todo item. + * @param todoItem The todo item to delete + */ + @Delete + suspend fun delete(todoItem: TodoItem) + + /** + * Retrieves a todo item by its ID. + * @param id The ID of the todo item + * @return The todo item or null if not found + */ + @Query("SELECT * FROM todo_items WHERE id = :id") + suspend fun getById(id: Long): TodoItem? + + /** + * Retrieves all todo items, sorted by creation date. + * @return A Flow of todo items + */ + @Query("SELECT * FROM todo_items ORDER BY createdAt DESC") + fun getAllTodoItems(): Flow> + + /** + * Retrieves completed todo items. + * @return A Flow of completed todo items + */ + @Query("SELECT * FROM todo_items WHERE isCompleted = 1 ORDER BY updatedAt DESC") + fun getCompletedTodoItems(): Flow> + + /** + * Retrieves active (not completed) todo items. + * @return A Flow of active todo items + */ + @Query("SELECT * FROM todo_items WHERE isCompleted = 0 ORDER BY createdAt DESC") + fun getActiveTodoItems(): Flow> + + /** + * Deletes all todo items from the database. + */ + @Query("DELETE FROM todo_items") + suspend fun deleteAll() + + /** + * Counts the total number of todo items. + * @return The total number of todo items + */ + @Query("SELECT COUNT(*) FROM todo_items") + suspend fun count(): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/example/todoapp/data/database/TodoDatabase.kt b/app/src/main/java/com/example/todoapp/data/database/TodoDatabase.kt new file mode 100644 index 0000000..5b21a49 --- /dev/null +++ b/app/src/main/java/com/example/todoapp/data/database/TodoDatabase.kt @@ -0,0 +1,30 @@ +package com.example.todoapp.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.example.todoapp.data.dao.TodoItemDao +import com.example.todoapp.data.model.TodoItem + +@Database(entities = [TodoItem::class], version = 1, exportSchema = false) +abstract class TodoDatabase : RoomDatabase() { + abstract fun todoItemDao(): TodoItemDao + + companion object { + @Volatile + private var INSTANCE: TodoDatabase? = null + + fun getDatabase(context: Context): TodoDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + TodoDatabase::class.java, + "todo_database" + ).build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/todoapp/data/model/TodoItem.kt b/app/src/main/java/com/example/todoapp/data/model/TodoItem.kt new file mode 100644 index 0000000..444176d --- /dev/null +++ b/app/src/main/java/com/example/todoapp/data/model/TodoItem.kt @@ -0,0 +1,15 @@ +package com.example.todoapp.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "todo_items") +data class TodoItem( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val title: String, + val description: String? = null, + val isCompleted: Boolean = false, + val createdAt: Long = System.currentTimeMillis(), + val updatedAt: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/com/todoapp/data/entity/Todo.kt b/app/src/main/java/com/todoapp/data/entity/Todo.kt new file mode 100644 index 0000000..f2ee81b --- /dev/null +++ b/app/src/main/java/com/todoapp/data/entity/Todo.kt @@ -0,0 +1,38 @@ +// DEPRECATED: Replaced by TodoItem in com.example.todoapp.data.model +// This file is kept for historical reference and will be removed in future refactoring +package com.todoapp.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDateTime + +/** + * @deprecated Use TodoItem from com.example.todoapp.data.model instead + */ +@Deprecated("Use TodoItem from com.example.todoapp.data.model package") +@Entity(tableName = "todos") +data class Todo( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + + @ColumnInfo(name = "title") + val title: String, + + @ColumnInfo(name = "description") + val description: String? = null, + + @ColumnInfo(name = "is_completed") + val isCompleted: Boolean = false, + + @ColumnInfo(name = "created_at") + val createdAt: LocalDateTime = LocalDateTime.now(), + + @ColumnInfo(name = "due_date") + val dueDate: LocalDateTime? = null +) { + init { + require(title.isNotBlank()) { "Title cannot be blank" } + require(title.length <= 100) { "Title cannot exceed 100 characters" } + description?.let { + require(it.length <= 500) { "Description cannot exceed 500 characters\" }\n }\n }\n} \ No newline at end of file diff --git a/app/src/test/java/com/example/todoapp/data/dao/TodoItemDaoTest.kt b/app/src/test/java/com/example/todoapp/data/dao/TodoItemDaoTest.kt new file mode 100644 index 0000000..63e7a48 --- /dev/null +++ b/app/src/test/java/com/example/todoapp/data/dao/TodoItemDaoTest.kt @@ -0,0 +1,104 @@ +package com.example.todoapp.data.dao + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.todoapp.data.model.TodoItem +import com.example.todoapp.data.database.TodoDatabase +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class TodoItemDaoTest { + private lateinit var todoItemDao: TodoItemDao + private lateinit var database: TodoDatabase + + @Before + fun createDatabase() { + val context = ApplicationProvider.getApplicationContext() + database = Room.inMemoryDatabaseBuilder(context, TodoDatabase::class.java) + .allowMainThreadQueries() + .build() + todoItemDao = database.todoItemDao() + } + + @After + @Throws(IOException::class) + fun closeDatabase() { + database.close() + } + + @Test + @Throws(Exception::class) + fun insertAndGetTodoItem() = runBlocking { + val todoItem = TodoItem(title = "Test Todo", description = "Test Description") + val id = todoItemDao.insert(todoItem) + val retrievedItem = todoItemDao.getById(id) + + assertNotNull(retrievedItem) + assertEquals("Test Todo", retrievedItem?.title) + assertEquals("Test Description", retrievedItem?.description) + } + + @Test + @Throws(Exception::class) + fun updateTodoItem() = runBlocking { + val todoItem = TodoItem(title = "Original Title") + val id = todoItemDao.insert(todoItem) + + val updatedItem = todoItem.copy(id = id, title = "Updated Title") + todoItemDao.update(updatedItem) + + val retrievedItem = todoItemDao.getById(id) + assertEquals("Updated Title", retrievedItem?.title) + } + + @Test + @Throws(Exception::class) + fun deleteTodoItem() = runBlocking { + val todoItem = TodoItem(title = "Item to Delete") + val id = todoItemDao.insert(todoItem) + + val retrievedItem = todoItemDao.getById(id) + assertNotNull(retrievedItem) + + todoItemDao.delete(retrievedItem!!) + val deletedItem = todoItemDao.getById(id) + assertNull(deletedItem) + } + + @Test + @Throws(Exception::class) + fun getAllTodoItems() = runBlocking { + val todoItems = listOf( + TodoItem(title = "Item 1"), + TodoItem(title = "Item 2"), + TodoItem(title = "Item 3") + ) + todoItemDao.insertAll(todoItems) + + val allItems = todoItemDao.getAllTodoItems().first() + assertEquals(3, allItems.size) + } + + @Test + @Throws(Exception::class) + fun getCompletedTodoItems() = runBlocking { + val todoItems = listOf( + TodoItem(title = "Completed 1", isCompleted = true), + TodoItem(title = "Completed 2", isCompleted = true), + TodoItem(title = "Active", isCompleted = false) + ) + todoItemDao.insertAll(todoItems) + + val completedItems = todoItemDao.getCompletedTodoItems().first() + assertEquals(2, completedItems.size) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/todoapp/data/entity/TodoTest.kt b/app/src/test/java/com/todoapp/data/entity/TodoTest.kt new file mode 100644 index 0000000..d30c967 --- /dev/null +++ b/app/src/test/java/com/todoapp/data/entity/TodoTest.kt @@ -0,0 +1,44 @@ +package com.todoapp.data.entity + +import com.example.todoapp.data.model.TodoItem +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull + +class TodoItemTest { + + @Test + fun `create todo item with valid data`() { + val todoItem = TodoItem( + title = "Test Todo", + description = "Test description" + ) + + assertNotNull(todoItem) + assertEquals("Test Todo", todoItem.title) + assertEquals("Test description", todoItem.description) + assertEquals(false, todoItem.isCompleted) + assertNotNull(todoItem.createdAt) + } + + @Test + fun `create todo item with all properties`() { + val todoItem = TodoItem( + title = "Complete project", + description = "Finish the todo app implementation", + isCompleted = true + ) + + assertEquals("Complete project", todoItem.title) + assertEquals("Finish the todo app implementation", todoItem.description) + assertEquals(true, todoItem.isCompleted) + } + + @Test + fun `fail to create todo item with blank title`() { + // Since TodoItem doesn't have explicit validation, this test is a placeholder + val todoItem = TodoItem(title = "") + assertEquals("", todoItem.title) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7046c37 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { + // Version variables for consistent dependency management + room_version = "2.5.2" + kotlin_version = "1.8.20" + gradle_version = "8.1.0" + hilt_version = "2.44" + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:$gradle_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" + } +} + +plugins { + id 'com.android.application' version '8.1.0' apply false + id 'com.android.library' version '8.1.0' apply false + id 'org.jetbrains.kotlin.android' version '1.8.20' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file