Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
*~
50 changes: 50 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
18 changes: 18 additions & 0 deletions app/src/main/java/com/todoapp/model/TodoItem.kt
Original file line number Diff line number Diff line change
@@ -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
)
87 changes: 87 additions & 0 deletions app/src/main/java/com/todoapp/ui/adapter/TodoListAdapter.kt
Original file line number Diff line number Diff line change
@@ -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<TodoItem, TodoListAdapter.TodoItemViewHolder>(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<TodoItem>() {
override fun areItemsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
return oldItem == newItem
}
}
}
37 changes: 37 additions & 0 deletions app/src/main/res/layout/todo_item_layout.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">

<CheckBox
android:id="@+id/todo_completion_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp" />

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">

<TextView
android:id="@+id/todo_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Buy groceries" />

<TextView
android:id="@+id/todo_due_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
tools:text="Due: 25 May 2023" />
</LinearLayout>
</LinearLayout>
103 changes: 103 additions & 0 deletions app/src/test/java/com/todoapp/ui/adapter/TodoListAdapterTest.kt
Original file line number Diff line number Diff line change
@@ -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<TextView>(R.id.todo_title)
val dueDateTextView = viewHolder.itemView.findViewById<TextView>(R.id.todo_due_date)
val completionCheckBox = viewHolder.itemView.findViewById<CheckBox>(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<TextView>(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)
}
}