diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 709aaa3..5b2e959 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b7e3d2..5ac4331 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> +} + +class ApiTasksRepository( + private val taskApiService: TaskApiService +): TasksRepository{ + override suspend fun getTasks(): List { + return taskApiService.getTasks().asDomainObjects() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/taskapp/network/TaskApiService.kt b/app/src/main/java/com/example/taskapp/network/TaskApiService.kt index 9f37855..a3316ad 100644 --- a/app/src/main/java/com/example/taskapp/network/TaskApiService.kt +++ b/app/src/main/java/com/example/taskapp/network/TaskApiService.kt @@ -7,20 +7,13 @@ import okhttp3.MediaType.Companion.toMediaType import retrofit2.Retrofit import retrofit2.http.GET -private const val BASE_URL = "http://10.0.2.2:3000" -private val retrofit = Retrofit.Builder() - .addConverterFactory( - Json.asConverterFactory("application/json".toMediaType()) - ) - .baseUrl(BASE_URL) - .build() + //create the actual function implementations (expensive!) -object TaskApi{ - val retrofitService : TaskApiService by lazy { - retrofit.create(TaskApiService::class.java) - } -} +//no longer needed --> moved to the AppContainer +//object TaskApi{ +// +//} //define what the API looks like diff --git a/app/src/main/java/com/example/taskapp/ui/TaskItem.kt b/app/src/main/java/com/example/taskapp/ui/TaskItem.kt index 74c0c81..cb1043c 100644 --- a/app/src/main/java/com/example/taskapp/ui/TaskItem.kt +++ b/app/src/main/java/com/example/taskapp/ui/TaskItem.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -46,7 +47,7 @@ fun TaskItem( Card( modifier = modifier.padding(dimensionResource(R.dimen.padding_small)), ) { - var expanded by remember { mutableStateOf(false) } + var expanded by rememberSaveable { mutableStateOf(false) } val color by animateColorAsState( targetValue = if (expanded) { MaterialTheme.colorScheme.tertiaryContainer diff --git a/app/src/main/java/com/example/taskapp/ui/TaskOverview.kt b/app/src/main/java/com/example/taskapp/ui/TaskOverview.kt index 817d295..971a1c7 100644 --- a/app/src/main/java/com/example/taskapp/ui/TaskOverview.kt +++ b/app/src/main/java/com/example/taskapp/ui/TaskOverview.kt @@ -19,7 +19,7 @@ fun TaskOverview( addingVisible: Boolean, onVisibilityChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, - taskOverviewViewModel: TaskOverviewViewModel = viewModel(), + taskOverviewViewModel: TaskOverviewViewModel = viewModel(factory = TaskOverviewViewModel.Factory), ) { val taskOverviewState by taskOverviewViewModel.uiState.collectAsState() diff --git a/app/src/main/java/com/example/taskapp/ui/TaskOverviewViewModel.kt b/app/src/main/java/com/example/taskapp/ui/TaskOverviewViewModel.kt index 7fa7d4a..c75b0fe 100644 --- a/app/src/main/java/com/example/taskapp/ui/TaskOverviewViewModel.kt +++ b/app/src/main/java/com/example/taskapp/ui/TaskOverviewViewModel.kt @@ -4,11 +4,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.example.taskapp.TasksApplication import com.example.taskapp.data.TaskSampler +import com.example.taskapp.data.TasksRepository import com.example.taskapp.model.Task -import com.example.taskapp.network.TaskApi -import com.example.taskapp.network.asDomainObjects import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,7 +20,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.IOException -class TaskOverviewViewModel : ViewModel() { +class TaskOverviewViewModel(private val tasksRepository: TasksRepository) : ViewModel() { // use StateFlow (Flow: emits current state + any updates) private val _uiState = MutableStateFlow(TaskOverviewState(TaskSampler.getAll())) val uiState: StateFlow = _uiState.asStateFlow() @@ -61,11 +65,13 @@ class TaskOverviewViewModel : ViewModel() { private fun getApiTasks(){ viewModelScope.launch { try{ - val listResult = TaskApi.retrofitService.getTasks() + //use the repository + //val tasksRepository = ApiTasksRepository() //repo is now injected + val listResult = tasksRepository.getTasks() _uiState.update { - it.copy(currentTaskList = listResult.asDomainObjects()) + it.copy(currentTaskList = listResult) } - taskApiState = TaskApiState.Success(listResult.asDomainObjects()) + taskApiState = TaskApiState.Success(listResult) } catch (e: IOException){ //show a toast? save a log on firebase? ... @@ -75,6 +81,18 @@ class TaskOverviewViewModel : ViewModel() { } } + + //object to tell the android framework how to handle the parameter of the viewmodel + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as TasksApplication) + val tasksRepository = application.container.tasksRepository + TaskOverviewViewModel(tasksRepository = tasksRepository + ) + } + } + } } diff --git a/app/src/test/java/com/example/taskapp/ApiTaskRepositoryTest.kt b/app/src/test/java/com/example/taskapp/ApiTaskRepositoryTest.kt new file mode 100644 index 0000000..ae8131b --- /dev/null +++ b/app/src/test/java/com/example/taskapp/ApiTaskRepositoryTest.kt @@ -0,0 +1,17 @@ +package com.example.taskapp + +import com.example.taskapp.data.ApiTasksRepository +import com.example.taskapp.fake.FakeDataSource +import com.example.taskapp.fake.FakeTasksApiService +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApiTaskRepositoryTest { + @Test + fun apiTaskRepository_getTasks_verifyTasksList() = + runTest{ + val repository = ApiTasksRepository(FakeTasksApiService()) + assertEquals(FakeDataSource.tasks, repository.getTasks()) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/example/taskapp/TaskOverviewViewModelTest.kt b/app/src/test/java/com/example/taskapp/TaskOverviewViewModelTest.kt index 2f39003..3c9f715 100644 --- a/app/src/test/java/com/example/taskapp/TaskOverviewViewModelTest.kt +++ b/app/src/test/java/com/example/taskapp/TaskOverviewViewModelTest.kt @@ -1,17 +1,46 @@ package com.example.taskapp +import com.example.taskapp.fake.FakeApiTasksRepository import com.example.taskapp.ui.TaskOverviewViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.junit.Assert +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description class TaskOverviewViewModelTest { - private val viewModel = TaskOverviewViewModel() + + private val someTaskName = "some task name" + @get:Rule + val testDispatcher = TestDispatcherRule() + @Test fun settingNameChangesState() { + val viewModel = TaskOverviewViewModel( + tasksRepository = FakeApiTasksRepository() + ) viewModel.setNewTaskName(someTaskName) - Assert.assertEquals(viewModel.uiState.value.newTaskName, someTaskName) + + } + +} + +class TestDispatcherRule( + val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() +): TestWatcher(){ + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() } } diff --git a/app/src/test/java/com/example/taskapp/fake/FakeApiTasksRepository.kt b/app/src/test/java/com/example/taskapp/fake/FakeApiTasksRepository.kt new file mode 100644 index 0000000..5da32a9 --- /dev/null +++ b/app/src/test/java/com/example/taskapp/fake/FakeApiTasksRepository.kt @@ -0,0 +1,11 @@ +package com.example.taskapp.fake + +import com.example.taskapp.data.TasksRepository +import com.example.taskapp.model.Task +import com.example.taskapp.network.asDomainObjects + +class FakeApiTasksRepository: TasksRepository { + override suspend fun getTasks(): List { + return FakeDataSource.tasks.asDomainObjects() + } +} \ No newline at end of file diff --git a/app/src/test/java/com/example/taskapp/fake/FakeDataSource.kt b/app/src/test/java/com/example/taskapp/fake/FakeDataSource.kt new file mode 100644 index 0000000..74b9c08 --- /dev/null +++ b/app/src/test/java/com/example/taskapp/fake/FakeDataSource.kt @@ -0,0 +1,14 @@ +package com.example.taskapp.fake + +import com.example.taskapp.network.ApiTask + +object FakeDataSource { + const val nameOne = "feed dog" + const val nameTwo = "feed cat" + const val descriptionOne = "food is in the cellar" + const val descriptionTwo = "food is also in the cellar" + + val tasks = listOf( + ApiTask(nameOne, descriptionOne), + ApiTask(nameTwo, descriptionTwo)) +} \ No newline at end of file diff --git a/app/src/test/java/com/example/taskapp/fake/FakeTasksApiService.kt b/app/src/test/java/com/example/taskapp/fake/FakeTasksApiService.kt new file mode 100644 index 0000000..e77d2f1 --- /dev/null +++ b/app/src/test/java/com/example/taskapp/fake/FakeTasksApiService.kt @@ -0,0 +1,10 @@ +package com.example.taskapp.fake + +import com.example.taskapp.network.ApiTask +import com.example.taskapp.network.TaskApiService + +class FakeTasksApiService: TaskApiService { + override suspend fun getTasks(): List { + return FakeDataSource.tasks + } +} \ No newline at end of file