I have a problem with serializing data on response to an api call.
It's my first time using this.
Service is called with "Retrofit" and everything goes well, the logs report the request and the response exactly.
Al network is into NetworModule
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
#Singleton
fun provideHttpClient(): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.addInterceptor { chain ->
val httpUrl: HttpUrl = chain.request().url.newBuilder().build()
val request: Request = chain.request().newBuilder().url(httpUrl).build()
chain.proceed(request)
}
.build()
#Provides
#Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.build()
#Provides
fun provideApiNetwork(retrofit: Retrofit): RetrofitApiNetwork =
retrofit.create(RetrofitApiNetwork::class.java)
}
response in json is:
{
"response": {
"info": {
...
},
"body": {
...
}
}
}
the problem comes in constructing a generic model with generic type.
Interface of calls:
interface RetrofitApiNetwork {
#GET("....")
suspend fun getElements(): APINetworkResponse<Element>
}
Generic models is:
#Serializable
data class APINetworkResponse<T>(
val response: Response<T>
)
#Serializable
data class Response<T>(
val info: Info,
val body: T
)
The value of APINetworkResponse<Element> is APINetworkResponse(response=null)
Can someone help me understand why and give me the solution. Thanks
Update
add repository:
class DataRepository #Inject constructor(
private val retrofitApiNetwork: RetrofitApiNetwork
){
suspend fun getArtistChart(): Flow<Element> = flow {
val apiNetworkResponse: APINetworkResponse<Element> = retrofitApiNetwork.getElements()
println(apiNetworkResponse) /*APINetworkResponse(response=null)*/
}
}
add dependecies:
[versions]
androidGradlePlugin = "7.3.1"
kotlin = "1.7.10"
kotlinDsl = "2.3.3"
junit = "4.13.2"
junitExt = "1.1.4"
testCore = "1.5.0"
espresso = "3.5.0"
coreKtx = "1.9.0"
appcompat = "1.5.1"
material = "1.7.0"
retrofit = "2.9.0"
hilt = "2.44.2"
nav = "2.5.3"
[libraries]
android-pluginGradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
kotlin-pluginGradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitExt" }
espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "testCore" }
androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "testCore" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
android-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
android-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
android-retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
android-navigation = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "nav" }
android-navigation-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "nav" }
android-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "nav" }
android-hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
android-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-dsl = { id = "org.gradle.kotlin.kotlin-dsl", version.ref = "kotlinDsl" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
```
None of the answers were successful. I fixed it by adding rules to the application proguard-rules file.
I followed this documentation.
You are using a suspend function to get the results using Coroutines, so make sure you have these dependencies on your project:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
Check for the latest versions, I'm using 1.6.4 at the time of writing this response.
Without these dependencies, Retrofit doesn't know about transforming a coroutine function and when the code is compiled, the mapper is unaware of the models because of the suspend function.
The problem is that you are using the data keyword when defining the classes which creates the component1(), component2(), etc. automatically, but those functions do not support generics.
Instead, you should use the #Serializable annotation on the class, and use #SerialName on the variables.
Try changing your classes to this:
#Serializable
class APINetworkResponse<T> (
#SerialName("response") val response: Response<T>
)
#Serializable
class Response<T> (
#SerialName("info") val info: Info,
#SerialName("body") val body: T
)
Also, you should make sure that the Element class is serializable. You can use the #Serializable annotation on it too.
Related
i am trying to setup ktor replacing retrofit on a compose simple android app but i am getting this error:
java.lang.NoClassDefFoundError: Failed resolution of: Lio/ktor/utils/io/NativeUtilsJvmKt;
at io.ktor.client.features.HttpSend.<init>(HttpSend.kt:49)
at io.ktor.client.features.HttpSend.<init>(HttpSend.kt:41)
at io.ktor.client.features.HttpSend$Feature.prepare(HttpSend.kt:75)
at io.ktor.client.features.HttpSend$Feature.prepare(HttpSend.kt:72)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:77)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig.install(HttpClientConfig.kt:97)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:172)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:81)
at io.ktor.client.HttpClientKt.HttpClient(HttpClient.kt:43)
My service is like this:
interface ApiService {
suspend fun getProducts(): List<ResponseModel>
companion object {
private val json = kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
}
fun create(): ApiService {
return ApiServiceImpl(
client = HttpClient(Android) {
// Logging
install(Logging) {
level = LogLevel.BODY
}
// JSON
install(JsonFeature) {
serializer = KotlinxSerializer(json)
//or serializer = KotlinxSerializer()
}
// Timeout
install(HttpTimeout) {
requestTimeoutMillis = 15000L
connectTimeoutMillis = 15000L
socketTimeoutMillis = 15000L
}
}
)
}
}
}
and ApiServiceImpl
class ApiServiceImpl(private val client: HttpClient) : ApiService {
override suspend fun getProducts(): List<ResponseModel> {
return client.get {
url(HttpRoutes.PRODUCTS)
}
}
}
On my main activity I just want to make the api call and just display an attribute of the first item of the list(just for verifying that the call was successful).
class MainActivity : ComponentActivity() {
private val service = ColorBarApiService.create()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
val products = produceState(
initialValue = emptyList<ResponseModel>(),
producer = {
value = service.getProducts()
}
)
// just to see if api call is success
Text(text = "First product has description ${products.value[0].description}!")
}
}
}
}
}
Finally on app build.gradle I am using these:
// ktor
implementation "io.ktor:ktor-client-core:$ktor_version"
// HTTP engine: The HTTP client used to perform network requests.
implementation "io.ktor:ktor-client-android:$ktor_version"
// The serialization engine used to convert objects to and from JSON.
implementation "io.ktor:ktor-client-serialization:$ktor_version"
// Logging
implementation "io.ktor:ktor-client-logging:$ktor_version"
implementation "io.ktor:ktor-utils-jvm:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
with values
ext {
compose_ui_version = '1.2.1'
koin_version= "3.2.2"
koin_android_version= "3.2.3"
koin_android_compose_version= "3.2.2"
koin_ktor= "3.2.2"
ktor_version = '1.6.4'
serialization_version = '1.4.1'
}
[versions]
groovy = "3.0.5"
checkstyle = "8.37"
[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = {
strictly = "[3.8, 4.0[", prefer="3.9" } }
[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]
[plugins]
jmh = { id = "me.champeau.jmh", version = "0.6.5" }
Then, when I actually use the artifact, I have to do this:
implementation(libs.groovy-core) {artifact {type = "aar"}}
but when I use bundle? Not all use artifact, How to deal with it
implementation(libs.bundle.groovy) { artifact {type = "aar"}}
I'm using ktor for the android client but I have an error.
When I run the app for the first time everything is fine and there is no issue, but when I click on the device back button and close the app, and open it again, the app is crashed and I get this error about the ktor:
Parent job is Completed
this is my ktor configure the module:
#InstallIn(SingletonComponent::class)
#Module
object NetworkModule {
private const val TIME_OUT = 60_000
#Singleton
#Provides
fun provideKtor(): HttpClient = HttpClient(Android) {
install(HttpCache)
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
install(ContentNegotiation) {
json(json = Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
})
}
install(HttpTimeout) {
connectTimeoutMillis = TIME_OUT.toLong()
socketTimeoutMillis = TIME_OUT.toLong()
requestTimeoutMillis = TIME_OUT.toLong()
}
install(ResponseObserver) {
onResponse { response ->
Log.d("HttpClientLogger - HTTP status", "${response.status.value}")
Log.d("HttpClientLogger - Response:", response.toString())
}
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.v("Logger Ktor =>", message)
}
}
level = LogLevel.NONE
}
}
}
Note: I use ktor version "2.0.2".
const val ktor_client_core = "io.ktor:ktor-client-core:$ktor_version"
const val ktor_client_cio = "io.ktor:ktor-client-cio:$ktor_version"
const val ktor_serialization_json = "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
const val ktor_serialization = "io.ktor:ktor-client-serialization:$ktor_version"
const val ktor_android = "io.ktor:ktor-client-android:$ktor_version"
const val ktor_negotiation = "io.ktor:ktor-client-content-negotiation:$ktor_version"
const val ktor_okhttp = "io.ktor:ktor-client-okhttp:$ktor_version"
const val ktor_logging = "io.ktor:ktor-client-logging:$ktor_version"
How can i fix it?
I found the reason: This is related to Hilt Di (NetworkModule). I have to use an object instead of hilt module for now
The problem is that you cannot use the same instance of the HttpClient.
companion object {
val client get() = HttpClient(Android) { }
}
This is my application test class
class ApplicationTest {
private val heroRepository: HeroRepository by inject(HeroRepository::class.java)
#OptIn(InternalAPI::class)
#Test
fun `access all heroes endpoints, assert correct information`() = testApplication {
val response = client.get("/naruto/heroes")
assertEquals(
expected =
"""
{
success = true,
message = "ok",
prevPage = null,
nextPage = 2,
heroes = ${heroRepository.heroes[1]!!}
}
""".trimIndent() ,
actual = response.bodyAsText()
)
}
}
It show the error of java.lang.ClassCastException when heroRepository is getting inject and i am using koin for dependency injection
java.lang.ClassCastException: class com.example.repository.HeroRepositoryImpl cannot be cast to class com.example.repository.HeroRepository (com.example.repository.HeroRepositoryImpl is in unnamed module of loader io.ktor.server.engine.OverridingClassLoader$ChildURLClassLoader #7f6ad6c8; com.example.repository.HeroRepository is in unnamed module of loader 'app')
And this is my AllHeroesRoute and here it's perfectly injecting heroRepository
fun Route.getAllHeroes() {
val heroRepository: HeroRepository by inject()
get("/naruto/heroes") {
try {
val page = call.request.queryParameters["page"]?.toInt() ?: 1
require(page in 1..5)
val apiResponse = heroRepository.getAllHeroes(page = page)
call.respond(
message = apiResponse,
status = HttpStatusCode.OK
)
} catch (e: NumberFormatException) {
call.respond(
message = ApiResponse(success = false, message = "Only numbers allowed"),
status = HttpStatusCode.BadRequest
)
} catch (e: IllegalArgumentException) {
call.respond(
message = ApiResponse(success = false, message = "Heroes Not Found"),
status = HttpStatusCode.BadRequest
)
}
}
}
I had the same issue, disabling the developmentMode fixed it:
fun myTestFunc() = testApplication {
environment {
developmentMode = false
}
....
}
I am new to test cases and I am trying to write test cases for the below code but I did not get success after trying several method. My main target is to cover code coverage of MaintenanceStatusResponseHandler.kt class. I am using mockito to write the test cases. I am already implemented jococo for code coverage but I am facing some issue to write a test cases. Please help me to write test cases of MaintenanceStatusResponseHandler class
Thanks in advance
internal class MaintenanceStatusResponseHandler {
public fun getMaintenanceResponse(voiceAiConfig : VoiceAiConfig):MaintenanceStatus{
val maintenanceStatus = MaintenanceStatus()
val retrofitRepository = RetrofitRepository()
val maintenanceUrl : String
val jwtToken : String
when (voiceAiConfig.server) {
BuildConfig.ENV_PRODUCTION_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_PRODUCTION
maintenanceUrl = BuildConfig.MAINTENANCE_PROD_URL
}
BuildConfig.ENV_STAGING_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_STAGING
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
else -> {
jwtToken = BuildConfig.JWT_TOKEN_SANDBOX
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
}
val header = "${VoiceAISDKConstant.JWT_TOKEN_PREFIX} $jwtToken"
retrofitRepository.getRetrofit(maintenanceUrl)
.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}