-
Notifications
You must be signed in to change notification settings - Fork 941
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
authFeature(), | ||
loginFeature(), | ||
moviesFeature(), | ||
navigationFeature(), | ||
) |
This file was deleted.
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
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 :)