Skip to content

Commit d3456ce

Browse files
committed
Use ktor and kotlinX serialization
1 parent e11746a commit d3456ce

File tree

24 files changed

+664
-303
lines changed

24 files changed

+664
-303
lines changed

app/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ plugins {
2222
alias(libs.plugins.kotlin.compose.compiler)
2323
alias(libs.plugins.kotlin.ksp)
2424
alias(libs.plugins.kotlin.parcelize)
25+
alias(libs.plugins.kotlin.serialization)
2526
}
2627

2728
val appConfig = AppConfig()
@@ -99,7 +100,6 @@ dependencies {
99100
implementation(libs.ktor.client.content.negotiation)
100101
implementation(libs.kotlinx.serialization.json)
101102
implementation(libs.ktor.serialization.kotlinx.json)
102-
implementation(libs.converter.gson)
103103

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

134135
// UI tests dependencies
135136
androidTestImplementation(composeBom)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.fernandocejas.sample
2+
3+
import com.fernandocejas.sample.core.navigation.navigationFeature
4+
import com.fernandocejas.sample.core.network.networkFeature
5+
import com.fernandocejas.sample.features.auth.authFeature
6+
import com.fernandocejas.sample.features.login.loginFeature
7+
import com.fernandocejas.sample.features.movies.di.moviesFeature
8+
9+
fun allFeatures() = listOf(
10+
networkFeature(),
11+
authFeature(),
12+
loginFeature(),
13+
moviesFeature(),
14+
navigationFeature(),
15+
)

app/src/main/kotlin/com/fernandocejas/sample/AndroidApplication.kt

-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
package com.fernandocejas.sample
1717

1818
import android.app.Application
19-
import com.fernandocejas.sample.core.allFeatures
20-
import com.fernandocejas.sample.core.di.coreModule
2119
import org.koin.android.ext.koin.androidContext
2220
import org.koin.android.ext.koin.androidLogger
2321
import org.koin.core.context.GlobalContext.startKoin

app/src/main/kotlin/com/fernandocejas/sample/core/di/CoreModule.kt

-23
This file was deleted.

app/src/main/kotlin/com/fernandocejas/sample/core/Core.kt renamed to app/src/main/kotlin/com/fernandocejas/sample/core/di/Feature.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
package com.fernandocejas.sample.core
1+
package com.fernandocejas.sample.core.di
22

3-
import com.fernandocejas.sample.core.di.coreModule
4-
import com.fernandocejas.sample.features.auth.authFeature
5-
import com.fernandocejas.sample.features.login.loginFeature
6-
import com.fernandocejas.sample.features.movies.moviesFeature
73
import org.koin.core.module.Module
84

95
/**
@@ -45,15 +41,3 @@ interface Feature {
4541
*/
4642
// fun databaseTables(): List<Table> = emptyList()
4743
}
48-
49-
private fun coreFeature() = object : Feature {
50-
override fun name() = "core"
51-
override fun diModule() = coreModule
52-
}
53-
54-
fun allFeatures() = listOf(
55-
coreFeature(),
56-
authFeature(),
57-
loginFeature(),
58-
moviesFeature(),
59-
)

app/src/main/kotlin/com/fernandocejas/sample/core/navigation/Navigator.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ import android.content.Intent
2121
import android.net.Uri
2222
import android.view.View
2323
import androidx.fragment.app.FragmentActivity
24+
import com.fernandocejas.sample.core.di.Feature
2425
import com.fernandocejas.sample.core.extension.emptyString
2526
import com.fernandocejas.sample.features.auth.credentials.Authenticator
2627
import com.fernandocejas.sample.features.movies.ui.MovieView
2728
import com.fernandocejas.sample.features.movies.ui.MoviesActivity
29+
import org.koin.core.module.Module
30+
import org.koin.core.module.dsl.singleOf
31+
import org.koin.dsl.module
2832

2933

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

86-
90+
// temporary solution to compile till Navigator is deleted
91+
fun navigationFeature() = object : Feature {
92+
override fun name() = "navigation"
93+
override fun diModule() = module {
94+
singleOf(::Navigator)
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.fernandocejas.sample.core.network
2+
3+
import com.fernandocejas.sample.core.functional.Either
4+
import com.fernandocejas.sample.core.functional.toLeft
5+
import com.fernandocejas.sample.core.functional.toRight
6+
7+
sealed class ApiResponse<out T, out E> {
8+
/**
9+
* Represents successful network responses (2xx).
10+
*/
11+
data class Success<T>(val body: T) : ApiResponse<T, Nothing>()
12+
13+
sealed class Error<E> : ApiResponse<Nothing, E>() {
14+
/**
15+
* Represents server (50x) and client (40x) errors.
16+
*/
17+
data class HttpError<E>(val code: Int, val errorBody: E?) : Error<E>()
18+
19+
/**
20+
* Represent IOExceptions and connectivity issues.
21+
*/
22+
data object NetworkError : Error<Nothing>()
23+
24+
/**
25+
* Represent SerializationExceptions.
26+
*/
27+
data object SerializationError : Error<Nothing>()
28+
}
29+
}
30+
31+
// Side Effect helpers
32+
inline fun <T, E> ApiResponse<T, E>.onSuccess(block: (T) -> Unit): ApiResponse<T, E> {
33+
if (this is ApiResponse.Success) {
34+
block(body)
35+
}
36+
return this
37+
}
38+
39+
fun <T, E> ApiResponse<T, E>.toEither(): Either<E?, T> {
40+
return when (this) {
41+
is ApiResponse.Success -> body.toRight()
42+
is ApiResponse.Error.HttpError -> errorBody.toLeft()
43+
is ApiResponse.Error.NetworkError -> null.toLeft()
44+
is ApiResponse.Error.SerializationError -> null.toLeft()
45+
}
46+
}
47+
48+
fun <T, E, F, D> ApiResponse<T, E>.toEither(
49+
successTransform: (T) -> D,
50+
errorTransform: (ApiResponse.Error<E>) -> F,
51+
): Either<F, D> {
52+
return when (this) {
53+
is ApiResponse.Success -> successTransform(body).toRight()
54+
is ApiResponse.Error -> errorTransform(this).toLeft()
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.fernandocejas.sample.core.network
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.call.body
5+
import io.ktor.client.plugins.ClientRequestException
6+
import io.ktor.client.plugins.ResponseException
7+
import io.ktor.client.plugins.ServerResponseException
8+
import io.ktor.client.request.HttpRequestBuilder
9+
import io.ktor.client.request.request
10+
import io.ktor.serialization.JsonConvertException
11+
import kotlinx.io.IOException
12+
13+
suspend inline fun <reified T, reified E> HttpClient.safeRequest(
14+
block: HttpRequestBuilder.() -> Unit,
15+
): ApiResponse<T, E> =
16+
try {
17+
val response = request { block() }
18+
ApiResponse.Success(response.body())
19+
} catch (e: ClientRequestException) {
20+
ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
21+
} catch (e: ServerResponseException) {
22+
ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
23+
} catch (e: IOException) {
24+
ApiResponse.Error.NetworkError
25+
} catch (e: JsonConvertException) {
26+
ApiResponse.Error.SerializationError
27+
}
28+
29+
suspend inline fun <reified E> ResponseException.errorBody(): E? =
30+
try {
31+
response.body()
32+
} catch (e: JsonConvertException) {
33+
null
34+
}

app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkHandler.kt

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package com.fernandocejas.sample.core.network
1717

1818
import android.content.Context
1919
import android.net.NetworkCapabilities
20-
import android.os.Build
2120
import com.fernandocejas.sample.core.extension.connectivityManager
2221

2322
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.fernandocejas.sample.core.network
2+
3+
import co.touchlab.kermit.Logger
4+
import com.fernandocejas.sample.core.di.Feature
5+
import io.ktor.client.HttpClient
6+
import io.ktor.client.engine.okhttp.OkHttp
7+
import io.ktor.client.plugins.HttpTimeout
8+
import io.ktor.client.plugins.cache.HttpCache
9+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
10+
import io.ktor.client.plugins.logging.LogLevel
11+
import io.ktor.client.plugins.logging.Logging
12+
import io.ktor.http.ContentType
13+
import io.ktor.serialization.kotlinx.json.json
14+
import kotlinx.serialization.json.Json
15+
import org.koin.core.module.dsl.singleOf
16+
import org.koin.dsl.module
17+
import io.ktor.client.plugins.logging.Logger as KtorLogger
18+
19+
fun networkFeature() = object : Feature {
20+
override fun name() = "network"
21+
override fun diModule() = networkModule
22+
}
23+
24+
private val networkModule = module {
25+
singleOf(::NetworkHandler)
26+
single { json }
27+
single { client }
28+
}
29+
30+
private val json = Json {
31+
ignoreUnknownKeys = true
32+
explicitNulls = false
33+
}
34+
35+
private val client = HttpClient(OkHttp) {
36+
engine {
37+
config {
38+
followRedirects(true)
39+
}
40+
}
41+
install(HttpCache)
42+
install(HttpTimeout)
43+
install(ContentNegotiation) {
44+
json(json, ContentType.Text.Plain)
45+
}
46+
install(Logging) {
47+
logger = object : KtorLogger {
48+
override fun log(message: String) {
49+
Logger.withTag("HTTP").d { "\uD83C\uDF10 $message" }
50+
}
51+
}
52+
level = LogLevel.HEADERS
53+
}
54+
}

app/src/main/kotlin/com/fernandocejas/sample/features/auth/Auth.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.fernandocejas.sample.features.auth
22

3-
import com.fernandocejas.sample.core.Feature
3+
import com.fernandocejas.sample.core.di.Feature
44
import com.fernandocejas.sample.features.auth.credentials.Authenticator
55
import org.koin.core.module.dsl.singleOf
66
import org.koin.dsl.module

app/src/main/kotlin/com/fernandocejas/sample/features/auth/di/AuthModule.kt

-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
package com.fernandocejas.sample.features.auth.di
22

3-
import com.fernandocejas.sample.core.navigation.Navigator
4-
import com.fernandocejas.sample.core.network.NetworkHandler
53
import com.fernandocejas.sample.features.auth.credentials.Authenticator
6-
import okhttp3.OkHttpClient
74
import org.koin.core.module.dsl.singleOf
85
import org.koin.dsl.module
9-
import retrofit2.Retrofit
10-
import retrofit2.converter.gson.GsonConverterFactory
116

127
val authModule = module {
138
singleOf(::Authenticator)

app/src/main/kotlin/com/fernandocejas/sample/features/login/Login.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.fernandocejas.sample.features.login
22

3-
import com.fernandocejas.sample.core.Feature
3+
import com.fernandocejas.sample.core.di.Feature
44
import org.koin.dsl.module
55

66
fun loginFeature() = object : Feature {

app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MovieEntity.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
package com.fernandocejas.sample.features.movies.data
1717

1818
import com.fernandocejas.sample.features.movies.interactor.Movie
19+
import kotlinx.serialization.Serializable
1920

20-
data class MovieEntity(private val id: Int, private val poster: String) {
21-
fun toMovie() = Movie(id, poster)
22-
}
21+
@Serializable
22+
data class MovieEntity(val id: Int, val poster: String)
23+
24+
25+
fun MovieEntity.toMovie() = Movie(id, poster)

app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesApi.kt

-34
This file was deleted.

0 commit comments

Comments
 (0)