Skip to content
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
11 changes: 9 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ plugins {
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.dagger.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kotlin.kapt)
}

android {
Expand Down Expand Up @@ -96,10 +98,8 @@ dependencies {
// Networking
implementation(libs.retrofit2)
implementation(libs.retrofit2.converter.gson)
implementation(libs.retrofit2.adapter.rxjava2)
implementation(libs.okhttp3)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.kotlinx.serialization.json)

// DataStore, ProtoBuf
implementation(libs.datastore)
Expand All @@ -115,6 +115,13 @@ dependencies {
implementation(libs.room.ktx)
implementation(libs.room.paging)

// JsonApiX
implementation(libs.kotlinx.serialization.json)
implementation(libs.jsonapix.core)
kapt(libs.jsonapix.processor)
implementation(libs.jsonapix.retrofit)
implementation(libs.retrofit2.kotlinx.serialization.converter)

// Unit Test
testImplementation(libs.junit)
testImplementation(libs.robolectric)
Expand Down
42 changes: 16 additions & 26 deletions app/src/main/java/com/mkdev/nimblesurvey/di/DataModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.mkdev.nimblesurvey.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import com.google.gson.Gson
import com.infinum.jsonapix.TypeAdapterFactory
import com.infinum.jsonapix.retrofit.JsonXConverterFactory
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.mkdev.data.datasource.local.UserLocal
import com.mkdev.data.datasource.local.crypto.Crypto
import com.mkdev.data.datasource.local.crypto.CryptoImpl
Expand All @@ -15,7 +17,6 @@ import com.mkdev.data.datasource.local.datastore.UserLocalSourceImpl
import com.mkdev.data.datasource.remote.api.AuthApi
import com.mkdev.data.datasource.remote.api.SurveyApi
import com.mkdev.data.datasource.remote.interceptor.AuthInterceptor
import com.mkdev.data.utils.ApiErrorHandler
import com.mkdev.nimblesurvey.BuildConfig
import com.mkdev.nimblesurvey.utils.ApiConfigs
import dagger.Binds
Expand All @@ -24,15 +25,15 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
import kotlin.time.toJavaDuration


@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
Expand All @@ -54,16 +55,6 @@ abstract class DataModule {

internal companion object {

@Provides
@Singleton
fun provideApiErrorHandler(gson: Gson): ApiErrorHandler {
return ApiErrorHandler(gson)
}

@Provides
@Singleton
fun provideGson(): Gson = Gson()

@Provides
@Singleton
fun provideDataStore(
Expand Down Expand Up @@ -99,24 +90,23 @@ abstract class DataModule {
.readTimeout(ApiConfigs.Timeouts.read.toJavaDuration())
.build()

@Singleton
@Provides
fun provideConverterFactory(): Converter.Factory {
return GsonConverterFactory.create()
}

@Singleton
@Provides
fun provideRetrofitApiService(
okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
): Retrofit =
Retrofit.Builder()
): Retrofit {
val networkJson = Json {
ignoreUnknownKeys = true
isLenient = true
}

return Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.addConverterFactory(converterFactory)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(JsonXConverterFactory(TypeAdapterFactory()))
.addConverterFactory(networkJson.asConverterFactory("application/json".toMediaType()))
.client(okHttpClient)
.build()
}

@Singleton
@Provides
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.dagger.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.kotlin.kapt) apply false
}
11 changes: 9 additions & 2 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ plugins {
alias(libs.plugins.dagger.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.protobuf)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kotlin.kapt)
}

android {
Expand Down Expand Up @@ -59,10 +61,8 @@ dependencies {
// Networking
implementation(libs.retrofit2)
implementation(libs.retrofit2.converter.gson)
implementation(libs.retrofit2.adapter.rxjava2)
implementation(libs.okhttp3)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.kotlinx.serialization.json)

// DataStore, ProtoBuf
implementation(libs.datastore)
Expand All @@ -75,6 +75,13 @@ dependencies {
implementation(libs.room.ktx)
implementation(libs.room.paging)

// JsonApiX
implementation(libs.kotlinx.serialization.json)
implementation(libs.jsonapix.core)
kapt(libs.jsonapix.processor)
implementation(libs.jsonapix.retrofit)
implementation(libs.retrofit2.kotlinx.serialization.converter)

// Unit Test
testImplementation(libs.junit)
testImplementation(libs.mockito)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class SurveyDaoTest {
@Test
fun getById_should_return_null_when_survey_not_found() = runTest {
// Given
val nonexistentId = "nonexistent_id"
val nonexistentId = 100001

// When
val retrievedSurvey = surveyDao.getById(nonexistentId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class SurveyRemoteKeyDaoTest {
@Test
fun remoteKeysId_should_return_null_when_remote_key_not_found() = runTest(testDispatcher) {
// Given
val nonexistentId = "nonexistent_id"
val nonexistentId = 100001

// When
val retrievedRemoteKey = surveyRemoteKeyDao.remoteKeysId(nonexistentId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import com.mkdev.data.datasource.local.database.room.entity.SurveyEntity
object SurveyEntityFactory {

fun createSurveyEntity(
id: String = "survey_id",
title: String = "Survey Title",
description: String = "Survey Description",
coverImageUrl: String = "https://example.com/image.jpg",
isActive: Boolean = true,
surveyType: String = "customer_satisfaction"
): SurveyEntity {
return SurveyEntity(
id = id,
title = title,
description = description,
coverImageUrl = coverImageUrl,
Expand All @@ -24,7 +22,7 @@ object SurveyEntityFactory {

fun createSurveyEntityList(count: Int = 5): List<SurveyEntity> {
return (1..count).map {
createSurveyEntity(id = "survey_id_$it")
createSurveyEntity()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.mkdev.data.datasource.local.database.room.entity.SurveyRemoteKeyEntit

object SurveyRemoteKeyEntityFactory {
fun createSurveyRemoteKeyEntity(
surveyId: String = "survey_id",
surveyId: Int = 1,
prevPage: Int? = 1,
nextPage: Int? = 2
): SurveyRemoteKeyEntity {
Expand All @@ -17,7 +17,7 @@ object SurveyRemoteKeyEntityFactory {

fun createSurveyRemoteKeyEntityList(count: Int = 5): List<SurveyRemoteKeyEntity> {
return (1..count).map {
createSurveyRemoteKeyEntity(surveyId = "survey_id_$it")
createSurveyRemoteKeyEntity(surveyId = it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface SurveyDao {
fun getByPaging(): PagingSource<Int, SurveyEntity>

@Query("SELECT * FROM survey_table WHERE id =:id LIMIT 1")
suspend fun getById(id: String): SurveyEntity
suspend fun getById(id: Int): SurveyEntity

@Query("DELETE FROM survey_table")
suspend fun clearAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface SurveyRemoteKeyDao {
suspend fun insertOrReplace(remoteKey: SurveyRemoteKeyEntity)

@Query("SELECT * FROM survey_remote_key_table WHERE surveyId = :id")
suspend fun remoteKeysId(id: String): SurveyRemoteKeyEntity?
suspend fun remoteKeysId(id: Int): SurveyRemoteKeyEntity?

@Query("DELETE FROM survey_remote_key_table")
suspend fun clearRemoteKeys()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import androidx.room.PrimaryKey

@Entity(tableName = "survey_table")
data class SurveyEntity(
@PrimaryKey
val id: String,
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val description: String,
val coverImageUrl: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import androidx.room.PrimaryKey

@Entity(tableName = "survey_remote_key_table")
data class SurveyRemoteKeyEntity(
@PrimaryKey
val surveyId: String,
@PrimaryKey(autoGenerate = true)
val surveyId: Int = 0,
val prevPage: Int?,
val nextPage: Int?
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import javax.inject.Inject

class SignInMapper @Inject constructor() {
fun mapToUserLocal(signInResponse: SignInResponse?): UserLocal? {
return signInResponse?.attributes?.let { attributes ->
return signInResponse?.let { attributes ->
UserLocal.newBuilder()
.setAccessToken(attributes.accessToken)
.setRefreshToken(attributes.refreshToken)
.setCreatedAt(attributes.createdAt)
.setExpiresIn(attributes.expiresIn)
.setCreatedAt(attributes.createdAt.toString())
.setExpiresIn(attributes.expiresIn.toString())
.setTokenType(attributes.tokenType)
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import javax.inject.Inject

class SurveyEntityMapper @Inject constructor() {
fun mapToSurveyEntity(surveyResponse: SurveyResponse): SurveyEntity {
return with(surveyResponse.attributes) {
return with(surveyResponse) {
SurveyEntity(
id = surveyResponse.id,
title = title,
description = description,
coverImageUrl = coverImageUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import com.mkdev.data.datasource.remote.api.SurveyApi
import com.mkdev.data.utils.RemoteApiPaging
import retrofit2.HttpException
import java.io.IOException
import java.lang.RuntimeException

@OptIn(ExperimentalPagingApi::class)
internal class SurveyRemoteMediator(
Expand Down Expand Up @@ -62,8 +61,8 @@ internal class SurveyRemoteMediator(
}

val response = surveyApi.getSurveys(page = page, pageSize = RemoteApiPaging.PAGE_SIZE)
val surveys = response.body()?.data
val endOfPaginationReached = surveys.isNullOrEmpty()
val surveys = response.data
val endOfPaginationReached = surveys.isEmpty()

// Clear local data for REFRESH
if (loadType == LoadType.REFRESH) {
Expand All @@ -74,15 +73,15 @@ internal class SurveyRemoteMediator(
val prevKey = if (page == RemoteApiPaging.FIRST_PAGE) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1

val surveyEntities = surveys?.map { surveyDto ->
surveyEntityMapper.mapToSurveyEntity(surveyDto)
val surveyEntities = surveys.map { surveyDto ->
surveyEntityMapper.mapToSurveyEntity(surveyDto.data)
}

// Insert new surveys and remote keys
surveyEntities?.let { surveyDao.insertAll(it) }
surveys?.map {
SurveyRemoteKeyEntity(surveyId = it.id, prevPage = prevKey, nextPage = nextKey)
}?.let { surveyRemoteKeyDao.insertAll(it) }
surveyEntities.let { surveyDao.insertAll(it) }
surveys.map {
SurveyRemoteKeyEntity(prevPage = prevKey, nextPage = nextKey)
}.let { surveyRemoteKeyDao.insertAll(it) }

return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: IOException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.mkdev.data.datasource.remote.api
import com.mkdev.data.datasource.remote.model.request.refreshToken.RefreshTokenRequest
import com.mkdev.data.datasource.remote.model.request.resetPassword.ResetPasswordRequest
import com.mkdev.data.datasource.remote.model.request.singIn.SignInRequest
import com.mkdev.data.datasource.remote.model.response.singIn.SignInResponse
import com.mkdev.data.datasource.remote.model.response.resetPassword.ResetPasswordResponseModel
import com.mkdev.data.datasource.remote.model.response.singIn.SignInResponseModel
import com.mkdev.data.utils.ApiConfigs
import com.mkdev.data.datasource.remote.model.response.base.BaseApiResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
Expand All @@ -14,14 +14,14 @@ import retrofit2.http.POST
interface AuthApi {
@Headers("${ApiConfigs.CUSTOM_HEADER}: ${ApiConfigs.NO_AUTH}")
@POST("oauth/token")
suspend fun signIn(@Body requestBody: SignInRequest): Response<BaseApiResponse<SignInResponse>>
suspend fun signIn(@Body requestBody: SignInRequest): SignInResponseModel

@Headers("${ApiConfigs.CUSTOM_HEADER}: ${ApiConfigs.NO_AUTH}")
@POST("oauth/token")
suspend fun refreshToken(@Body requestBody: RefreshTokenRequest): Response<BaseApiResponse<SignInResponse>>
suspend fun refreshToken(@Body requestBody: RefreshTokenRequest): Response<SignInResponseModel>

@POST("passwords")
suspend fun resetPassword(
@Body requestBody: ResetPasswordRequest
): Response<BaseApiResponse<Unit>>
): ResetPasswordResponseModel
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.mkdev.data.datasource.remote.api

import com.mkdev.data.datasource.remote.model.response.survey.SurveyResponse
import com.mkdev.data.datasource.remote.model.response.base.BaseApiResponse
import retrofit2.Response
import com.mkdev.data.datasource.remote.model.response.survey.SurveyResponseList
import retrofit2.http.GET
import retrofit2.http.Query

Expand All @@ -11,5 +9,5 @@ interface SurveyApi {
suspend fun getSurveys(
@Query("page[number]") page: Int,
@Query("page[size]") pageSize: Int
): Response<BaseApiResponse<List<SurveyResponse>>>
): SurveyResponseList
}
Loading
Loading