Skip to content

Use ktor and kotlinx serialization #126

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ plugins {
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
}

val appConfig = AppConfig()
@@ -99,7 +100,6 @@ dependencies {
implementation(libs.ktor.client.content.negotiation)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.converter.gson)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this! For a long time wanted to get rid of gson :)


// Compose
// @see: https://developer.android.google.cn/develop/ui/compose/setup?hl=en#kotlin_1
@@ -130,6 +130,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.robolectric)
testImplementation(libs.ktor.client.mock)

// UI tests dependencies
androidTestImplementation(composeBom)
15 changes: 15 additions & 0 deletions app/src/main/kotlin/com/fernandocejas/sample/AllFeatures.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.fernandocejas.sample

import com.fernandocejas.sample.core.navigation.navigationFeature
import com.fernandocejas.sample.core.network.networkFeature
import com.fernandocejas.sample.features.auth.authFeature
import com.fernandocejas.sample.features.login.loginFeature
import com.fernandocejas.sample.features.movies.di.moviesFeature

fun allFeatures() = listOf(
networkFeature(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not make network and navigation a feature. Unless we have a good reason for it. Think of features as user features. Networking and Navigation belong to core, which is the module shared across features.

Happy to read your thoughts :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree about the semantics here. My idea was to break core into independent functionalities like network, etc. If it's too much too early, we can keep core as is and see how it develops? Navigation feature is only temporary to keep the proeject compiling, till we move to androidx navigation and use one activity. Let me know your thoughts :)

authFeature(),
loginFeature(),
moviesFeature(),
navigationFeature(),
)
Original file line number Diff line number Diff line change
@@ -16,8 +16,6 @@
package com.fernandocejas.sample

import android.app.Application
import com.fernandocejas.sample.core.allFeatures
import com.fernandocejas.sample.core.di.coreModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
23 changes: 0 additions & 23 deletions app/src/main/kotlin/com/fernandocejas/sample/core/di/CoreModule.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.fernandocejas.sample.core
package com.fernandocejas.sample.core.di

import com.fernandocejas.sample.core.di.coreModule
import com.fernandocejas.sample.features.auth.authFeature
import com.fernandocejas.sample.features.login.loginFeature
import com.fernandocejas.sample.features.movies.moviesFeature
import org.koin.core.module.Module

/**
@@ -45,15 +41,3 @@ interface Feature {
*/
// fun databaseTables(): List<Table> = emptyList()
}

private fun coreFeature() = object : Feature {
override fun name() = "core"
override fun diModule() = coreModule
}

fun allFeatures() = listOf(
coreFeature(),
authFeature(),
loginFeature(),
moviesFeature(),
)
Original file line number Diff line number Diff line change
@@ -21,10 +21,14 @@ import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.fragment.app.FragmentActivity
import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.core.extension.emptyString
import com.fernandocejas.sample.features.auth.credentials.Authenticator
import com.fernandocejas.sample.features.movies.ui.MovieView
import com.fernandocejas.sample.features.movies.ui.MoviesActivity
import org.koin.core.module.Module
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module


class Navigator(private val authenticator: Authenticator) {
@@ -83,4 +87,10 @@ class Navigator(private val authenticator: Authenticator) {
class Extras(val transitionSharedElement: View)
}


// temporary solution to compile till Navigator is deleted
fun navigationFeature() = object : Feature {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the comment above :)

override fun name() = "navigation"
override fun diModule() = module {
singleOf(::Navigator)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fernandocejas.sample.core.network

import com.fernandocejas.sample.core.functional.Either
import com.fernandocejas.sample.core.functional.toLeft
import com.fernandocejas.sample.core.functional.toRight

sealed class ApiResponse<out T, out E> {
/**
* Represents successful network responses (2xx).
*/
data class Success<T>(val body: T) : ApiResponse<T, Nothing>()

sealed class Error<E> : ApiResponse<Nothing, E>() {
/**
* Represents server (50x) and client (40x) errors.
*/
data class HttpError<E>(val code: Int, val errorBody: E?) : Error<E>()

/**
* Represent IOExceptions and connectivity issues.
*/
data object NetworkError : Error<Nothing>()

/**
* Represent SerializationExceptions.
*/
data object SerializationError : Error<Nothing>()
}
}

// Side Effect helpers
inline fun <T, E> ApiResponse<T, E>.onSuccess(block: (T) -> Unit): ApiResponse<T, E> {
if (this is ApiResponse.Success) {
block(body)
}
return this
}

fun <T, E> ApiResponse<T, E>.toEither(): Either<E?, T> {
return when (this) {
is ApiResponse.Success -> body.toRight()
is ApiResponse.Error.HttpError -> errorBody.toLeft()
is ApiResponse.Error.NetworkError -> null.toLeft()
is ApiResponse.Error.SerializationError -> null.toLeft()
}
}

fun <T, E, F, D> ApiResponse<T, E>.toEither(
successTransform: (T) -> D,
errorTransform: (ApiResponse.Error<E>) -> F,
): Either<F, D> {
return when (this) {
is ApiResponse.Success -> successTransform(body).toRight()
is ApiResponse.Error -> errorTransform(this).toLeft()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.fernandocejas.sample.core.network

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.ResponseException
import io.ktor.client.plugins.ServerResponseException
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.serialization.JsonConvertException
import kotlinx.io.IOException

suspend inline fun <reified T, reified E> HttpClient.safeRequest(
block: HttpRequestBuilder.() -> Unit,
): ApiResponse<T, E> =
try {
val response = request { block() }
ApiResponse.Success(response.body())
} catch (e: ClientRequestException) {
ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
} catch (e: ServerResponseException) {
ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
} catch (e: IOException) {
ApiResponse.Error.NetworkError
} catch (e: JsonConvertException) {
ApiResponse.Error.SerializationError
}

suspend inline fun <reified E> ResponseException.errorBody(): E? =
try {
response.body()
} catch (e: JsonConvertException) {
null
}
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@ package com.fernandocejas.sample.core.network

import android.content.Context
import android.net.NetworkCapabilities
import android.os.Build
import com.fernandocejas.sample.core.extension.connectivityManager

/**
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.fernandocejas.sample.core.network

import co.touchlab.kermit.Logger
import com.fernandocejas.sample.core.di.Feature
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import io.ktor.client.plugins.logging.Logger as KtorLogger

fun networkFeature() = object : Feature {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this might not become a feature, the DI part should be rethought then. If do not see any issues to continue having a DI core module that includes all the cross cutting dependencies.

override fun name() = "network"
override fun diModule() = networkModule
}

private val networkModule = module {
singleOf(::NetworkHandler)
single { json }
single { client }
}

private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}

private val client = HttpClient(OkHttp) {
engine {
config {
followRedirects(true)
}
}
install(HttpCache)
install(HttpTimeout)
install(ContentNegotiation) {
json(json, ContentType.Text.Plain)
}
install(Logging) {
logger = object : KtorLogger {
override fun log(message: String) {
Logger.withTag("HTTP").d { "\uD83C\uDF10 $message" }
}
}
level = LogLevel.HEADERS
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.fernandocejas.sample.features.auth

import com.fernandocejas.sample.core.Feature
import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.features.auth.credentials.Authenticator
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.fernandocejas.sample.features.auth.di

import com.fernandocejas.sample.core.navigation.Navigator
import com.fernandocejas.sample.core.network.NetworkHandler
import com.fernandocejas.sample.features.auth.credentials.Authenticator
import okhttp3.OkHttpClient
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

val authModule = module {
singleOf(::Authenticator)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.fernandocejas.sample.features.login

import com.fernandocejas.sample.core.Feature
import com.fernandocejas.sample.core.di.Feature
import org.koin.dsl.module

fun loginFeature() = object : Feature {
Original file line number Diff line number Diff line change
@@ -16,7 +16,10 @@
package com.fernandocejas.sample.features.movies.data

import com.fernandocejas.sample.features.movies.interactor.Movie
import kotlinx.serialization.Serializable

data class MovieEntity(private val id: Int, private val poster: String) {
fun toMovie() = Movie(id, poster)
}
@Serializable
data class MovieEntity(val id: Int, val poster: String)


fun MovieEntity.toMovie() = Movie(id, poster)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -19,57 +19,59 @@ import com.fernandocejas.sample.core.failure.Failure
import com.fernandocejas.sample.core.failure.Failure.NetworkConnection
import com.fernandocejas.sample.core.failure.Failure.ServerError
import com.fernandocejas.sample.core.functional.Either
import com.fernandocejas.sample.core.functional.Either.Left
import com.fernandocejas.sample.core.functional.Either.Right
import com.fernandocejas.sample.core.functional.toLeft
import com.fernandocejas.sample.core.network.ApiResponse
import com.fernandocejas.sample.core.network.NetworkHandler
import com.fernandocejas.sample.core.network.toEither
import com.fernandocejas.sample.features.movies.interactor.Movie
import com.fernandocejas.sample.features.movies.interactor.MovieDetails
import retrofit2.Call

interface MoviesRepository {
fun movies(): Either<Failure, List<Movie>>
fun movieDetails(movieId: Int): Either<Failure, MovieDetails>
suspend fun movies(): Either<Failure, List<Movie>>
suspend fun movieDetails(movieId: Int): Either<Failure, MovieDetails>

class Network(
private val networkHandler: NetworkHandler,
private val service: MoviesService
) : MoviesRepository {

override fun movies(): Either<Failure, List<Movie>> {
override suspend fun movies(): Either<Failure, List<Movie>> {
return when (networkHandler.isNetworkAvailable()) {
true -> request(
service.movies(),
{ it.map { movieEntity -> movieEntity.toMovie() } },
emptyList()
)
false -> Left(NetworkConnection)
}
}
true -> {
service.movies()
.toEither(
successTransform = { it.map { movieEntity -> movieEntity.toMovie() } },
errorTransform = {
when (it) {
is ApiResponse.Error.HttpError<*> -> ServerError
is ApiResponse.Error.NetworkError -> NetworkConnection
is ApiResponse.Error.SerializationError -> ServerError
}
},
)
}

override fun movieDetails(movieId: Int): Either<Failure, MovieDetails> {
return when (networkHandler.isNetworkAvailable()) {
true -> request(
service.movieDetails(movieId),
{ it.toMovieDetails() },
MovieDetailsEntity.empty
)
false -> Left(NetworkConnection)
false -> NetworkConnection.toLeft()
}
}

private fun <T, R> request(
call: Call<T>,
transform: (T) -> R,
default: T
): Either<Failure, R> {
return try {
val response = call.execute()
when (response.isSuccessful) {
true -> Right(transform((response.body() ?: default)))
false -> Left(ServerError)
override suspend fun movieDetails(movieId: Int): Either<Failure, MovieDetails> {
return when (networkHandler.isNetworkAvailable()) {
true -> {
return service.movieDetails(movieId)
.toEither(
successTransform = { it.toMovieDetails() },
errorTransform = {
when (it) {
is ApiResponse.Error.HttpError<*> -> ServerError
is ApiResponse.Error.NetworkError -> NetworkConnection
is ApiResponse.Error.SerializationError -> ServerError
}
},
)
}
} catch (exception: Throwable) {
Left(ServerError)

false -> NetworkConnection.toLeft()
}
}
}
Original file line number Diff line number Diff line change
@@ -15,11 +15,32 @@
*/
package com.fernandocejas.sample.features.movies.data

import retrofit2.Retrofit
import com.fernandocejas.sample.core.network.ApiResponse
import com.fernandocejas.sample.core.network.safeRequest
import io.ktor.client.HttpClient
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.contentType

class MoviesService(retrofit: Retrofit) : MoviesApi {
private val moviesApi by lazy { retrofit.create(MoviesApi::class.java) }
class MoviesService(
private val httpClient: HttpClient,
) {

override fun movies() = moviesApi.movies()
override fun movieDetails(movieId: Int) = moviesApi.movieDetails(movieId)
suspend fun movies(): ApiResponse<List<MovieEntity>, Unit> =
httpClient.safeRequest<List<MovieEntity>, Unit> {
url(BASE_URL + MOVIES)
contentType(ContentType.Text.Plain)
}

suspend fun movieDetails(movieId: Int): ApiResponse<MovieDetailsEntity, Unit> =
httpClient.safeRequest<MovieDetailsEntity, Unit> {
url(BASE_URL + "movie_0${movieId}.json")
contentType(ContentType.Text.Plain)
}

companion object {
private const val MOVIES = "movies.json"
private const val BASE_URL =
"https://raw.githubusercontent.com/android10/Sample-Data/master/Android-CleanArchitecture-Kotlin/"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.fernandocejas.sample.features.movies
package com.fernandocejas.sample.features.movies.di

import com.fernandocejas.sample.core.Feature
import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.features.movies.data.MoviesRepository
import com.fernandocejas.sample.features.movies.data.MoviesService
import com.fernandocejas.sample.features.movies.interactor.GetMovieDetails
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.fernandocejas.sample.core.network

import io.kotest.matchers.equals.shouldBeEqual
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.http.headersOf
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.test.runTest
import kotlinx.io.IOException
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.junit.Test

class HttpClientXTest {

@Test
fun `map to ApiResponse Success when client returns OK`() = runTest {
val tested = FakeApi(httpClient(okEngine))

val expected = ApiResponse.Success(FakeApi.FakeData())
val actual = tested.endpoint()

actual shouldBeEqual expected
}

@Test
fun `map to ApiResponse HttpError with code when client returns 40x`() = runTest {
val tested = FakeApi(httpClient(badRequestEngine))

val expected = ApiResponse.Error.HttpError(400, FakeApi.FakeError("bad request"))
val actual = tested.endpoint()

actual shouldBeEqual expected
}

@Test
fun `map to ApiResponse HttpError with code and body when client returns 50x`() = runTest {
val tested = FakeApi(httpClient(serverErrorEngine))

val expected = ApiResponse.Error.HttpError(500, FakeApi.FakeError("internal server error"))
val actual = tested.endpoint()

actual shouldBeEqual expected
}

@Test
fun `map to ApiResponse HttpError with code and null body when client returns 50x and error doesn't match contract`() =
runTest {
val tested = FakeApi(httpClient(malformedServerErrorEngine))

val expected = ApiResponse.Error.HttpError(500, null)
val actual = tested.endpoint()

actual shouldBeEqual expected
}

@Test
fun `map to ApiResponse NetworkError when there is no internet`() = runTest {
val tested = FakeApi(httpClient(networkErrorEngine))

val expected = ApiResponse.Error.NetworkError
val actual = tested.endpoint()

actual shouldBeEqual expected
}

@Test
fun `map to ApiResponse SerialisationError when data format doesn't match`() = runTest {
val tested = FakeApi(httpClient(serialisationErrorEngine))

val expected = ApiResponse.Error.SerializationError
val actual = tested.endpoint()

actual shouldBeEqual expected
}

private fun httpClient(engine: MockEngine) = HttpClient(engine) {
expectSuccess = true
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
}
)
}
}

private class FakeApi(
private val client: HttpClient,
) {

suspend fun endpoint(): ApiResponse<FakeData, FakeError> =
client.safeRequest {
url("https://movies.com/api")
contentType(ContentType.Application.Json)
}

@Serializable
data class FakeData(
val id: Int = 1,
val name: String = "Movie",
)

@Serializable
data class FakeError(
val message: String
)
}

private val headers = headersOf(HttpHeaders.ContentType, "application/json")

private val serialisationErrorEngine = MockEngine {
respond(
content = "just a string",
status = HttpStatusCode.OK,
headers = headers,
)
}

private val badRequestEngine = MockEngine {
respond(
content = """
{
"message": "bad request"
}
""".trimIndent(),
status = HttpStatusCode.BadRequest,
headers = headers,
)
}

private val serverErrorEngine = MockEngine {
respond(
content = """
{
"message": "internal server error"
}
""".trimIndent(),
status = HttpStatusCode.InternalServerError,
headers = headers,
)
}

private val malformedServerErrorEngine = MockEngine {
respond(
content = """
{
"incorrect_field_name": "error"
}
""".trimIndent(),
status = HttpStatusCode.InternalServerError,
headers = headers,
)
}

private val networkErrorEngine = MockEngine { throw IOException("No internet") }

private val okEngine = MockEngine {
respond(
content = """
{
"id": 1,
"name": "Movie"
}
""".trimIndent(),
status = HttpStatusCode.OK,
headers = headers,
)
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -15,33 +15,26 @@
*/
package com.fernandocejas.sample.features.movies.interactor

import com.fernandocejas.sample.UnitTest
import com.fernandocejas.sample.core.functional.Either.Right
import com.fernandocejas.sample.core.functional.toRight
import com.fernandocejas.sample.features.movies.data.MoviesRepository
import io.mockk.every
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Before
import kotlinx.coroutines.test.runTest
import org.junit.Test

class GetMovieDetailsTest : UnitTest() {

private lateinit var getMovieDetails: GetMovieDetails
class GetMovieDetailsTest {

private val moviesRepository: MoviesRepository = mockk()

@Before
fun setUp() {
getMovieDetails = GetMovieDetails(moviesRepository)
every { moviesRepository.movieDetails(MOVIE_ID) } returns Right(MovieDetails.empty)
}
private val getMovieDetails = GetMovieDetails(moviesRepository)

@Test
fun `should get data from repository`() {
runBlocking { getMovieDetails.run(GetMovieDetails.Params(MOVIE_ID)) }
fun `should get data from repository`() = runTest {
coEvery { moviesRepository.movieDetails(MOVIE_ID) } returns MovieDetails.empty.toRight()

getMovieDetails.run(GetMovieDetails.Params(MOVIE_ID))

verify(exactly = 1) { moviesRepository.movieDetails(MOVIE_ID) }
coVerify(exactly = 1) { moviesRepository.movieDetails(MOVIE_ID) }
}

companion object {
Original file line number Diff line number Diff line change
@@ -15,35 +15,26 @@
*/
package com.fernandocejas.sample.features.movies.interactor

import com.fernandocejas.sample.UnitTest
import com.fernandocejas.sample.core.functional.Either.Right
import com.fernandocejas.sample.core.functional.toRight
import com.fernandocejas.sample.core.interactor.UseCase
import com.fernandocejas.sample.features.movies.data.MoviesRepository
import com.fernandocejas.sample.features.movies.interactor.GetMovies
import com.fernandocejas.sample.features.movies.interactor.Movie
import io.mockk.every
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Before
import kotlinx.coroutines.test.runTest
import org.junit.Test

class GetMoviesTest : UnitTest() {

private lateinit var getMovies: GetMovies
class GetMoviesTest {

private val moviesRepository: MoviesRepository = mockk()

@Before
fun setUp() {
getMovies = GetMovies(moviesRepository)
every { moviesRepository.movies() } returns Right(listOf(Movie.empty))
}
private val getMovies = GetMovies(moviesRepository)

@Test
fun `should get data from repository`() {
runBlocking { getMovies.run(UseCase.None()) }
fun `should get data from repository`() = runTest {
coEvery { moviesRepository.movies() } returns listOf(Movie.empty).toRight()

getMovies.run(UseCase.None())

verify(exactly = 1) { moviesRepository.movies() }
coVerify(exactly = 1) { moviesRepository.movies() }
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fernandocejas.sample.matchers

import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import io.kotest.matchers.string.shouldBeEqualIgnoringCase
import org.robolectric.Robolectric
@@ -8,7 +9,7 @@ import kotlin.reflect.KClass


infix fun KClass<out AppCompatActivity>.shouldNavigateTo(
nextActivity: KClass<out AppCompatActivity>
nextActivity: KClass<out Activity>
): () -> Unit = {

val originActivity = Robolectric.buildActivity(this.java).get()
3 changes: 1 addition & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@ appCompat = "1.7.0"
activityCompose = "1.10.1"
lifecycleViewmodelCompose = "2.8.7"
koinAndroid = "3.5.6"
converterGson = "2.9.0"
coil = "3.1.0"
kermit = "2.0.4"
kotlinxSerializationJson = "1.8.1"
@@ -57,7 +56,6 @@ androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }

koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }
koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koinAndroid" }
@@ -80,6 +78,7 @@ kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest"
junit = { module = "junit:junit", version.ref = "junit" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
ktor-client-mock ={ module = "io.ktor:ktor-client-mock", version.ref = "ktor" }

# main module ui test dependencies ---
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }