diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8266e94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Android Studio +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx + +# Kotlin/Android specific +bin/ +gen/ +out/ + +# Dependency directories +/node_modules +/app/build/ + +# Logs +*.log + +# Environment files +.env + +# Gradle +.gradle/ +build/ + +# VS Code +.vscode/ + +# MacOS +.DS_Store + +# Temp files +*.swp +*~ \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d68dd1f --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.application") + id("kotlin-android") +} + +android { + compileSdk = 33 + defaultConfig { + applicationId = "com.todoapp" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + getByName("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 { + // Kotlin standard library + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.0") + + // AndroidX + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.recyclerview:recyclerview:1.3.0") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.10.3") + testImplementation("androidx.test:core:1.5.0") + testImplementation("androidx.test.ext:junit:1.1.5") + testImplementation("org.mockito:mockito-core:4.8.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") +} \ No newline at end of file diff --git a/app/src/main/java/com/todoapp/model/TodoItem.kt b/app/src/main/java/com/todoapp/model/TodoItem.kt new file mode 100644 index 0000000..909098d --- /dev/null +++ b/app/src/main/java/com/todoapp/model/TodoItem.kt @@ -0,0 +1,18 @@ +package com.todoapp.model + +import java.util.Date + +/** + * Represents a single todo item in the application. + * + * @property id Unique identifier for the todo item + * @property title Title or description of the todo item + * @property dueDate Date when the todo item is due + * @property isCompleted Indicates whether the todo item is completed + */ +data class TodoItem( + val id: Long = 0, + val title: String, + val dueDate: Date? = null, + val isCompleted: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/todoapp/ui/adapter/TodoListAdapter.kt b/app/src/main/java/com/todoapp/ui/adapter/TodoListAdapter.kt new file mode 100644 index 0000000..e800dfa --- /dev/null +++ b/app/src/main/java/com/todoapp/ui/adapter/TodoListAdapter.kt @@ -0,0 +1,87 @@ +package com.todoapp.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.todoapp.R +import com.todoapp.model.TodoItem +import java.text.SimpleDateFormat +import java.util.Locale + +/** + * RecyclerView adapter for displaying a list of todo items. + * Uses DiffUtil for efficient list updates. + */ +class TodoListAdapter( + private val onItemClick: (TodoItem) -> Unit, + private val onCompletionToggle: (TodoItem) -> Unit +) : ListAdapter(TodoItemDiffCallback()) { + + /** + * ViewHolder for individual todo item views + */ + inner class TodoItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val titleTextView: TextView = itemView.findViewById(R.id.todo_title) + private val dueDateTextView: TextView = itemView.findViewById(R.id.todo_due_date) + private val completionCheckBox: CheckBox = itemView.findViewById(R.id.todo_completion_checkbox) + + /** + * Bind todo item data to the view + */ + fun bind(todoItem: TodoItem) { + titleTextView.text = todoItem.title + + // Format due date if available + todoItem.dueDate?.let { dueDate -> + val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()) + dueDateTextView.text = dateFormat.format(dueDate) + dueDateTextView.visibility = View.VISIBLE + } ?: run { + dueDateTextView.visibility = View.GONE + } + + // Set checkbox state + completionCheckBox.isChecked = todoItem.isCompleted + + // Set click listeners + itemView.setOnClickListener { onItemClick(todoItem) } + completionCheckBox.setOnClickListener { + onCompletionToggle(todoItem) + } + } + } + + /** + * Create new view holder for todo items + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.todo_item_layout, parent, false) + return TodoItemViewHolder(view) + } + + /** + * Bind data to view holder + */ + override fun onBindViewHolder(holder: TodoItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + /** + * DiffUtil callback for efficient list updates + */ + class TodoItemDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/todo_item_layout.xml b/app/src/main/res/layout/todo_item_layout.xml new file mode 100644 index 0000000..7e1c3fb --- /dev/null +++ b/app/src/main/res/layout/todo_item_layout.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/todoapp/ui/adapter/TodoListAdapterTest.kt b/app/src/test/java/com/todoapp/ui/adapter/TodoListAdapterTest.kt new file mode 100644 index 0000000..2937e72 --- /dev/null +++ b/app/src/test/java/com/todoapp/ui/adapter/TodoListAdapterTest.kt @@ -0,0 +1,103 @@ +package com.todoapp.ui.adapter + +import android.view.View +import android.widget.CheckBox +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.todoapp.R +import com.todoapp.model.TodoItem +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import java.util.Date + +@RunWith(RobolectricTestRunner::class) +class TodoListAdapterTest { + + private lateinit var adapter: TodoListAdapter + private lateinit var mockOnItemClick: (TodoItem) -> Unit + private lateinit var mockOnCompletionToggle: (TodoItem) -> Unit + + @Before + fun setup() { + mockOnItemClick = {} + mockOnCompletionToggle = {} + adapter = TodoListAdapter(mockOnItemClick, mockOnCompletionToggle) + } + + @Test + fun `adapter submits list correctly`() { + val todoItems = listOf( + TodoItem(1, "Task 1", Date(), false), + TodoItem(2, "Task 2", null, true) + ) + adapter.submitList(todoItems) + + assertEquals(2, adapter.itemCount) + } + + @Test + fun `view holder binds todo item correctly`() { + val todoItem = TodoItem(1, "Test Task", Date(), false) + val context = RuntimeEnvironment.getApplication() + val parent = RecyclerView(context) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + // Use reflection to access private methods and fields + val bindMethod = TodoListAdapter.TodoItemViewHolder::class.java + .getDeclaredMethod("bind", TodoItem::class.java) + bindMethod.isAccessible = true + bindMethod.invoke(viewHolder, todoItem) + + // Use reflection to check private fields + val titleTextView = viewHolder.itemView.findViewById(R.id.todo_title) + val dueDateTextView = viewHolder.itemView.findViewById(R.id.todo_due_date) + val completionCheckBox = viewHolder.itemView.findViewById(R.id.todo_completion_checkbox) + + assertEquals("Test Task", titleTextView.text) + assertTrue(dueDateTextView.visibility == View.VISIBLE) + assertFalse(completionCheckBox.isChecked) + } + + @Test + fun `todo item with no due date handles visibility correctly`() { + val todoItem = TodoItem(1, "No Date Task", null, false) + val context = RuntimeEnvironment.getApplication() + val parent = RecyclerView(context) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + val bindMethod = TodoListAdapter.TodoItemViewHolder::class.java + .getDeclaredMethod("bind", TodoItem::class.java) + bindMethod.isAccessible = true + bindMethod.invoke(viewHolder, todoItem) + + val dueDateTextView = viewHolder.itemView.findViewById(R.id.todo_due_date) + assertTrue(dueDateTextView.visibility == View.GONE) + } + + @Test + fun `item click invokes callback`() { + var clickedItem: TodoItem? = null + val onItemClick: (TodoItem) -> Unit = { clickedItem = it } + + val todoItem = TodoItem(1, "Clickable Task") + adapter = TodoListAdapter(onItemClick, mockOnCompletionToggle) + + val context = RuntimeEnvironment.getApplication() + val parent = RecyclerView(context) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + val bindMethod = TodoListAdapter.TodoItemViewHolder::class.java + .getDeclaredMethod("bind", TodoItem::class.java) + bindMethod.isAccessible = true + bindMethod.invoke(viewHolder, todoItem) + + // Simulate item click + viewHolder.itemView.performClick() + + assertEquals(todoItem, clickedItem) + } +} \ No newline at end of file