Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2023 lesson 7 Changes overview #6

Open
wants to merge 4 commits into
base: 2023-lesson-6
Choose a base branch
from
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".TasksApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/example/taskapp/TasksApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.taskapp

import android.app.Application
import com.example.taskapp.data.AppContainer
import com.example.taskapp.data.DefaultAppContainer



class TasksApplication: Application() {
lateinit var container: AppContainer

override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
}
33 changes: 33 additions & 0 deletions app/src/main/java/com/example/taskapp/data/AppContainer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.taskapp.data

import com.example.taskapp.network.TaskApiService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit

interface AppContainer {
val tasksRepository: TasksRepository
}

//container that takes care of dependencies
class DefaultAppContainer(): AppContainer{

private val baseUrl = "http://10.0.2.2:3000"
private val retrofit = Retrofit.Builder()
.addConverterFactory(
Json.asConverterFactory("application/json".toMediaType())
)
.baseUrl(baseUrl)
.build()

private val retrofitService : TaskApiService by lazy {
retrofit.create(TaskApiService::class.java)
}


override val tasksRepository: TasksRepository by lazy {
ApiTasksRepository(retrofitService)
}

}
17 changes: 17 additions & 0 deletions app/src/main/java/com/example/taskapp/data/TasksRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.taskapp.data

import com.example.taskapp.model.Task
import com.example.taskapp.network.TaskApiService
import com.example.taskapp.network.asDomainObjects

interface TasksRepository {
suspend fun getTasks(): List<Task>
}

class ApiTasksRepository(
private val taskApiService: TaskApiService
): TasksRepository{
override suspend fun getTasks(): List<Task> {
return taskApiService.getTasks().asDomainObjects()
}
}
17 changes: 5 additions & 12 deletions app/src/main/java/com/example/taskapp/network/TaskApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/example/taskapp/ui/TaskItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/example/taskapp/ui/TaskOverview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
30 changes: 24 additions & 6 deletions app/src/main/java/com/example/taskapp/ui/TaskOverviewViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ 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
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<TaskOverviewState> = _uiState.asStateFlow()
Expand Down Expand Up @@ -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? ...
Expand All @@ -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
)
}
}
}
}


17 changes: 17 additions & 0 deletions app/src/test/java/com/example/taskapp/ApiTaskRepositoryTest.kt
Original file line number Diff line number Diff line change
@@ -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())
}
}
33 changes: 31 additions & 2 deletions app/src/test/java/com/example/taskapp/TaskOverviewViewModelTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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<Task> {
return FakeDataSource.tasks.asDomainObjects()
}
}
14 changes: 14 additions & 0 deletions app/src/test/java/com/example/taskapp/fake/FakeDataSource.kt
Original file line number Diff line number Diff line change
@@ -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))
}
10 changes: 10 additions & 0 deletions app/src/test/java/com/example/taskapp/fake/FakeTasksApiService.kt
Original file line number Diff line number Diff line change
@@ -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<ApiTask> {
return FakeDataSource.tasks
}
}