mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-05-02 05:34:20 +02:00
Milestones/ms1 (#16)
* Closes #11 * Closes #13 * Closes #17 * Closes #18 * Closes #19 * Closes #6 * Closes #3 * Closes #12 * Closes #15
This commit is contained in:
committed by
Melih Aksoy
parent
11889446cb
commit
625776609d
@@ -8,6 +8,6 @@ import com.melih.repository.interactors.base.Result
|
||||
*/
|
||||
abstract class Repository {
|
||||
|
||||
internal abstract suspend fun getNextLaunches(count: Int): Result<List<LaunchEntity>>
|
||||
internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>>
|
||||
internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity>
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.melih.repository.DEFAULT_NAME
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Entity(tableName = "Launches")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LaunchEntity(
|
||||
@PrimaryKey val id: Long = 0L,
|
||||
val name: String = DEFAULT_NAME,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.melih.repository.entities
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LaunchesEntity(
|
||||
val id: Long = 0L,
|
||||
val launches: List<LaunchEntity> = listOf(),
|
||||
|
||||
@@ -2,13 +2,16 @@ package com.melih.repository.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import com.melih.repository.DEFAULT_NAME
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationEntity(
|
||||
@ColumnInfo(name = "id_location") val id: Long = 0L,
|
||||
@ColumnInfo(name = "name_location") val name: String = DEFAULT_NAME,
|
||||
val pads: List<PadEntity> = listOf(PadEntity())
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PadEntity(
|
||||
@ColumnInfo(name = "id_pad") val id: Long = 0L,
|
||||
@ColumnInfo(name = "name_pad") val name: String = DEFAULT_NAME,
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.melih.repository.entities
|
||||
import androidx.room.ColumnInfo
|
||||
import com.melih.repository.DEFAULT_NAME
|
||||
import com.melih.repository.EMPTY_STRING
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MissionEntity(
|
||||
@ColumnInfo(name = "id_mission") val id: Long = 0L,
|
||||
@ColumnInfo(name = "name_mission") val name: String = DEFAULT_NAME,
|
||||
|
||||
@@ -4,7 +4,9 @@ import androidx.room.ColumnInfo
|
||||
import com.melih.repository.DEFAULT_NAME
|
||||
import com.melih.repository.EMPTY_STRING
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RocketEntity(
|
||||
@ColumnInfo(name = "id_rocket") val id: Long = 0L,
|
||||
@ColumnInfo(name = "name_rocket") val name: String = DEFAULT_NAME,
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.melih.repository.interactors
|
||||
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.BaseInteractor
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.InteractorParameters
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.sources.SourceManager
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.sources.NetworkSource
|
||||
import com.melih.repository.sources.PersistenceSource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import javax.inject.Inject
|
||||
@@ -12,13 +15,32 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Gets next given number of launches
|
||||
*/
|
||||
class GetLaunchDetails @Inject constructor(
|
||||
private val sourceManager: SourceManager
|
||||
) : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() {
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override suspend fun run(collector: FlowCollector<Result<LaunchEntity>>, params: Params) {
|
||||
collector.emit(sourceManager.getLaunchById(params.id))
|
||||
@field:Inject
|
||||
internal lateinit var networkSource: NetworkSource
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var persistenceSource: PersistenceSource
|
||||
|
||||
override suspend fun FlowCollector<Result<LaunchEntity>>.run(params: Params) {
|
||||
val result = persistenceSource.getLaunchById(params.id)
|
||||
|
||||
if (result !is Success) {
|
||||
when (val response = networkSource.getLaunchById(params.id)) {
|
||||
// Save result and return again from persistence
|
||||
is Success -> {
|
||||
persistenceSource.saveLaunch(response.successData)
|
||||
emit(persistenceSource.getLaunchById(params.id))
|
||||
}
|
||||
|
||||
// Redirect failure as it is
|
||||
is Failure -> emit(response)
|
||||
}
|
||||
} else {
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
|
||||
@@ -4,24 +4,45 @@ import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.BaseInteractor
|
||||
import com.melih.repository.interactors.base.InteractorParameters
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.sources.SourceManager
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.sources.NetworkSource
|
||||
import com.melih.repository.sources.PersistenceSource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import javax.inject.Inject
|
||||
|
||||
const val DEFAULT_LAUNCHES_AMOUNT = 15
|
||||
|
||||
/**
|
||||
* Gets next given number of launches
|
||||
*/
|
||||
class GetLaunches @Inject constructor(
|
||||
private val sourceManager: SourceManager
|
||||
) : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() {
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override suspend fun run(collector: FlowCollector<Result<List<LaunchEntity>>>, params: Params) {
|
||||
collector.emit(sourceManager.getNextLaunches(params.count))
|
||||
@field:Inject
|
||||
internal lateinit var networkSource: NetworkSource
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var persistenceSource: PersistenceSource
|
||||
|
||||
override suspend fun FlowCollector<Result<List<LaunchEntity>>>.run(params: Params) {
|
||||
|
||||
// Start network fetch - we're not handling state here to ommit them
|
||||
networkSource
|
||||
.getNextLaunches(params.count, params.page)
|
||||
.also {
|
||||
if (it is Success) {
|
||||
persistenceSource.saveLaunches(it.successData)
|
||||
emit(persistenceSource.getNextLaunches(params.count, params.page))
|
||||
} else {
|
||||
emit(it)
|
||||
emit(persistenceSource.getNextLaunches(params.count, params.page))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val count: Int = 10
|
||||
val count: Int = DEFAULT_LAUNCHES_AMOUNT,
|
||||
val page: Int
|
||||
) : InteractorParameters
|
||||
}
|
||||
|
||||
@@ -10,23 +10,22 @@ import kotlinx.coroutines.flow.flowOn
|
||||
/**
|
||||
* Base use case that wraps [suspending][suspend] [run] function with [flow][Flow] and returns it for later usage.
|
||||
*/
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BaseInteractor<T, in P : InteractorParameters> {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
protected abstract suspend fun run(collector: FlowCollector<Result<T>>, params: P)
|
||||
protected abstract suspend fun FlowCollector<Result<T>>.run(params: P)
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
operator fun invoke(params: P) =
|
||||
flow<Result<T>> {
|
||||
emit(Result.State.Loading())
|
||||
run(this, params)
|
||||
emit(Result.State.Loaded())
|
||||
}.flowOn(Dispatchers.IO)
|
||||
flow<Result<T>> {
|
||||
emit(State.Loading())
|
||||
run(params)
|
||||
emit(State.Loaded())
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import com.melih.repository.R
|
||||
|
||||
|
||||
/**
|
||||
* [Result.Failure] reasons
|
||||
* [Failure] reasons
|
||||
*/
|
||||
sealed class Reason(@StringRes val messageRes: Int) {
|
||||
sealed class Reason(@StringRes val messageRes: Int)
|
||||
|
||||
class NetworkError : Reason(R.string.reason_network)
|
||||
class EmptyResultError : Reason(R.string.reason_empty_body)
|
||||
class GenericError : Reason(R.string.reason_generic)
|
||||
class ResponseError : Reason(R.string.reason_response)
|
||||
class TimeoutError : Reason(R.string.reason_timeout)
|
||||
class PersistenceEmpty : Reason(R.string.reason_persistance_empty)
|
||||
class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty)
|
||||
}
|
||||
class NetworkError : Reason(R.string.reason_network)
|
||||
class EmptyResultError : Reason(R.string.reason_empty_body)
|
||||
class GenericError : Reason(R.string.reason_generic)
|
||||
class ResponseError : Reason(R.string.reason_response)
|
||||
class TimeoutError : Reason(R.string.reason_timeout)
|
||||
class PersistenceEmpty : Reason(R.string.reason_persistance_empty)
|
||||
class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty)
|
||||
|
||||
@@ -1,45 +1,53 @@
|
||||
package com.melih.repository.interactors.base
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
|
||||
/**
|
||||
* Result class that wraps any [Success], [Failure] or [State] that can be generated by any derivation of [BaseInteractor]
|
||||
*/
|
||||
sealed class Result<out T> {
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
sealed class Result<out T>
|
||||
|
||||
// region Subclasses
|
||||
// region Subclasses
|
||||
|
||||
class Success<out T>(val successData: T) : Result<T>()
|
||||
class Failure(val errorData: Reason) : Result<Nothing>()
|
||||
class Success<out T>(val successData: T) : Result<T>()
|
||||
class Failure(val errorData: Reason) : Result<Nothing>()
|
||||
|
||||
sealed class State : Result<Nothing>() {
|
||||
class Loading : State()
|
||||
class Loaded : State()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
inline fun handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) {
|
||||
when (this) {
|
||||
is Success -> successBlock(successData)
|
||||
is Failure -> failureBlock(errorData)
|
||||
is State -> stateBlock(this)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun handleSuccess(successBlock: (T) -> Unit) {
|
||||
if (this is Success)
|
||||
successBlock(successData)
|
||||
}
|
||||
|
||||
inline fun handleFailure(errorBlock: (Reason) -> Unit) {
|
||||
if (this is Failure)
|
||||
errorBlock(errorData)
|
||||
}
|
||||
|
||||
inline fun handleState(stateBlock: (State) -> Unit) {
|
||||
if (this is State)
|
||||
stateBlock(this)
|
||||
}
|
||||
// endregion
|
||||
sealed class State : Result<Nothing>() {
|
||||
class Loading : State()
|
||||
class Loaded : State()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Extensions
|
||||
|
||||
inline fun <T> Result<T>.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) {
|
||||
when (this) {
|
||||
is Success -> successBlock(successData)
|
||||
is Failure -> failureBlock(errorData)
|
||||
is State -> stateBlock(this)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Result<T>.onSuccess(successBlock: (T) -> Unit): Result<T> {
|
||||
if (this is Success)
|
||||
successBlock(successData)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <T> Result<T>.onFailure(errorBlock: (Reason) -> Unit): Result<T> {
|
||||
if (this is Failure)
|
||||
errorBlock(errorData)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <T> Result<T>.onState(stateBlock: (State) -> Unit): Result<T> {
|
||||
if (this is State)
|
||||
stateBlock(this)
|
||||
|
||||
return this
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -5,15 +5,21 @@ import com.melih.repository.entities.LaunchesEntity
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Retrofit interface for networking
|
||||
*/
|
||||
interface Api {
|
||||
internal interface Api {
|
||||
|
||||
@GET("launch/next/{count}")
|
||||
suspend fun getNextLaunches(@Path("count") count: Int): Response<LaunchesEntity>
|
||||
suspend fun getNextLaunches(
|
||||
@Path("count") count: Int,
|
||||
@Query("offset") offset: Int
|
||||
): Response<LaunchesEntity>
|
||||
|
||||
@GET("launch/{id}")
|
||||
suspend fun getLaunchById(@Path("id") id: Long): Response<LaunchEntity>
|
||||
suspend fun getLaunchById(
|
||||
@Path("id") id: Long
|
||||
): Response<LaunchEntity>
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class ApiImpl @Inject constructor() : Api {
|
||||
internal const val TIMEOUT_DURATION = 7L
|
||||
|
||||
internal class ApiImpl @Inject constructor() : Api {
|
||||
|
||||
// region Properties
|
||||
|
||||
@@ -23,9 +26,12 @@ class ApiImpl @Inject constructor() : Api {
|
||||
Retrofit.Builder()
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
|
||||
.readTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor()
|
||||
.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
).build()
|
||||
)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
@@ -35,9 +41,14 @@ class ApiImpl @Inject constructor() : Api {
|
||||
}
|
||||
// endregion
|
||||
|
||||
override suspend fun getNextLaunches(count: Int): Response<LaunchesEntity> =
|
||||
service.getNextLaunches(count)
|
||||
override suspend fun getNextLaunches(
|
||||
count: Int,
|
||||
offset: Int
|
||||
): Response<LaunchesEntity> =
|
||||
service.getNextLaunches(count, offset)
|
||||
|
||||
override suspend fun getLaunchById(id: Long): Response<LaunchEntity> =
|
||||
override suspend fun getLaunchById(
|
||||
id: Long
|
||||
): Response<LaunchEntity> =
|
||||
service.getLaunchById(id)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.melih.repository.persistence
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
@@ -24,7 +26,14 @@ const val DB_NAME = "LaunchesDB"
|
||||
RocketConverter::class,
|
||||
MissionConverter::class
|
||||
)
|
||||
abstract class LaunchesDatabase : RoomDatabase() {
|
||||
internal abstract class LaunchesDatabase : RoomDatabase() {
|
||||
|
||||
abstract val launchesDao: LaunchesDao
|
||||
companion object {
|
||||
|
||||
fun getInstance(ctx: Context) =
|
||||
Room.databaseBuilder(ctx, LaunchesDatabase::class.java, DB_NAME)
|
||||
.build()
|
||||
}
|
||||
|
||||
internal abstract val launchesDao: LaunchesDao
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.melih.repository.persistence.converters
|
||||
|
||||
import com.melih.repository.entities.LocationEntity
|
||||
import com.melih.repository.entities.LocationEntityJsonAdapter
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
|
||||
@@ -9,5 +10,5 @@ import com.squareup.moshi.Moshi
|
||||
*/
|
||||
class LocationConverter : BaseConverter<LocationEntity>() {
|
||||
override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> =
|
||||
moshi.adapter(LocationEntity::class.java)
|
||||
LocationEntityJsonAdapter(moshi)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.melih.repository.persistence.converters
|
||||
|
||||
import com.melih.repository.entities.RocketEntity
|
||||
import com.melih.repository.entities.RocketEntityJsonAdapter
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
|
||||
@@ -9,5 +10,5 @@ import com.squareup.moshi.Moshi
|
||||
*/
|
||||
class RocketConverter : BaseConverter<RocketEntity>() {
|
||||
override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> =
|
||||
moshi.adapter(RocketEntity::class.java)
|
||||
RocketEntityJsonAdapter(moshi)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@ package com.melih.repository.persistence.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
|
||||
/**
|
||||
* DAO for list of [launches][LaunchEntity]
|
||||
*/
|
||||
@Dao
|
||||
abstract class LaunchesDao {
|
||||
internal abstract class LaunchesDao {
|
||||
|
||||
// region Queries
|
||||
|
||||
@Query("SELECT * FROM Launches LIMIT :count")
|
||||
abstract suspend fun getLaunches(count: Int): List<LaunchEntity>
|
||||
@Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count")
|
||||
abstract suspend fun getLaunches(count: Int, page: Int): List<LaunchEntity>
|
||||
|
||||
@Query("SELECT * FROM Launches WHERE id=:id LIMIT 1")
|
||||
abstract suspend fun getLaunchById(id: Long): LaunchEntity?
|
||||
@@ -26,19 +26,10 @@ abstract class LaunchesDao {
|
||||
|
||||
// region Insertion
|
||||
|
||||
@Insert
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveLaunches(launches: List<LaunchEntity>)
|
||||
|
||||
@Insert
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveLaunch(launch: LaunchEntity)
|
||||
// endregion
|
||||
|
||||
// region Transactions
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateLaunches(launches: List<LaunchEntity>) {
|
||||
nukeLaunches()
|
||||
saveLaunches(launches)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -4,8 +4,15 @@ import android.net.NetworkInfo
|
||||
import com.melih.repository.DEFAULT_IMAGE_SIZE
|
||||
import com.melih.repository.Repository
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
|
||||
import com.melih.repository.interactors.base.EmptyResultError
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.NetworkError
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.ResponseError
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.interactors.base.TimeoutError
|
||||
import com.melih.repository.network.ApiImpl
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
@@ -14,9 +21,9 @@ import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository],
|
||||
* returning either [failure][Result.Failure] with proper [reason][Reason] or [success][Result.Success] with data
|
||||
* returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data
|
||||
*/
|
||||
class NetworkSource @Inject constructor(
|
||||
internal class NetworkSource @Inject constructor(
|
||||
private val apiImpl: ApiImpl,
|
||||
private val networkInfoProvider: Provider<NetworkInfo>
|
||||
) : Repository() {
|
||||
@@ -31,8 +38,10 @@ class NetworkSource @Inject constructor(
|
||||
|
||||
// region Functions
|
||||
|
||||
override suspend fun getNextLaunches(count: Int): Result<List<LaunchEntity>> =
|
||||
safeExecute(apiImpl::getNextLaunches, count) { entity ->
|
||||
override suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> =
|
||||
safeExecute({
|
||||
apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT)
|
||||
}) { entity ->
|
||||
entity.launches.map { launch ->
|
||||
if (!launch.rocket.imageURL.isNotBlank()) {
|
||||
launch.copy(
|
||||
@@ -50,7 +59,9 @@ class NetworkSource @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun getLaunchById(id: Long): Result<LaunchEntity> =
|
||||
safeExecute(apiImpl::getLaunchById, id) {
|
||||
safeExecute({
|
||||
apiImpl.getLaunchById(id)
|
||||
}) {
|
||||
if (!it.rocket.imageURL.isNotBlank()) {
|
||||
it.copy(
|
||||
rocket = it.rocket.copy(
|
||||
@@ -62,28 +73,27 @@ class NetworkSource @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T, P, R> safeExecute(
|
||||
block: suspend (param: P) -> Response<T>,
|
||||
param: P,
|
||||
private suspend inline fun <T, R> safeExecute(
|
||||
block: suspend () -> Response<T>,
|
||||
transform: (T) -> R
|
||||
) =
|
||||
if (isNetworkConnected) {
|
||||
try {
|
||||
block(param).extractResponseBody(transform)
|
||||
block().extractResponseBody(transform)
|
||||
} catch (e: IOException) {
|
||||
Result.Failure(Reason.TimeoutError())
|
||||
Failure(TimeoutError())
|
||||
}
|
||||
} else {
|
||||
Result.Failure(Reason.NetworkError())
|
||||
Failure(NetworkError())
|
||||
}
|
||||
|
||||
private inline fun <T, R> Response<T>.extractResponseBody(transform: (T) -> R) =
|
||||
if (isSuccessful) {
|
||||
body()?.let {
|
||||
Result.Success(transform(it))
|
||||
} ?: Result.Failure(Reason.EmptyResultError())
|
||||
Success(transform(it))
|
||||
} ?: Failure(EmptyResultError())
|
||||
} else {
|
||||
Result.Failure(Reason.ResponseError())
|
||||
Failure(ResponseError())
|
||||
}
|
||||
|
||||
private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) =
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
package com.melih.repository.sources
|
||||
|
||||
import android.content.Context
|
||||
import com.melih.repository.Repository
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.PersistenceEmpty
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.persistence.LaunchesDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Persistance source using Room database to save / read objects for SST - offline usage
|
||||
*/
|
||||
class PersistenceSource @Inject constructor(
|
||||
private val launchesDatabase: LaunchesDatabase
|
||||
internal class PersistenceSource @Inject constructor(
|
||||
ctx: Context
|
||||
) : Repository() {
|
||||
// region Functions
|
||||
|
||||
override suspend fun getNextLaunches(count: Int): Result<List<LaunchEntity>> =
|
||||
private val launchesDatabase = LaunchesDatabase.getInstance(ctx)
|
||||
|
||||
override suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> =
|
||||
launchesDatabase
|
||||
.launchesDao
|
||||
.getLaunches(count)
|
||||
.getLaunches(count, page)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.run {
|
||||
Result.Success(this)
|
||||
} ?: Result.Failure(Reason.PersistenceEmpty())
|
||||
Success(this)
|
||||
} ?: Failure(PersistenceEmpty())
|
||||
|
||||
override suspend fun getLaunchById(id: Long): Result<LaunchEntity> =
|
||||
launchesDatabase
|
||||
@@ -30,11 +36,11 @@ class PersistenceSource @Inject constructor(
|
||||
.getLaunchById(id)
|
||||
.takeIf { it != null }
|
||||
?.run {
|
||||
Result.Success(this)
|
||||
} ?: Result.Failure(Reason.PersistenceEmpty())
|
||||
Success(this)
|
||||
} ?: Failure(PersistenceEmpty())
|
||||
|
||||
internal suspend fun saveLaunches(launches: List<LaunchEntity>) {
|
||||
launchesDatabase.launchesDao.updateLaunches(launches)
|
||||
launchesDatabase.launchesDao.saveLaunches(launches)
|
||||
}
|
||||
|
||||
internal suspend fun saveLaunch(launch: LaunchEntity) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.melih.repository.sources
|
||||
|
||||
import com.melih.repository.Repository
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Manages SST by using network & persistance sources
|
||||
*/
|
||||
class SourceManager @Inject constructor(
|
||||
private val networkSource: NetworkSource,
|
||||
private val persistenceSource: PersistenceSource
|
||||
) : Repository() {
|
||||
|
||||
// region Functions
|
||||
|
||||
override suspend fun getNextLaunches(count: Int): Result<List<LaunchEntity>> {
|
||||
networkSource
|
||||
.getNextLaunches(count)
|
||||
.takeIf { it is Result.Success }
|
||||
?.let {
|
||||
persistenceSource.saveLaunches((it as Result.Success).successData)
|
||||
}
|
||||
|
||||
return persistenceSource
|
||||
.getNextLaunches(count)
|
||||
.takeIf {
|
||||
it is Result.Success && it.successData.isNotEmpty()
|
||||
}
|
||||
?: Result.Failure(Reason.NoNetworkPersistenceEmpty())
|
||||
}
|
||||
|
||||
override suspend fun getLaunchById(id: Long): Result<LaunchEntity> {
|
||||
val result =
|
||||
persistenceSource
|
||||
.getLaunchById(id)
|
||||
|
||||
return if (result is Result.Failure) {
|
||||
networkSource
|
||||
.getLaunchById(id)
|
||||
.takeIf { it is Result.Success }
|
||||
?.let {
|
||||
persistenceSource.saveLaunch((it as Result.Success).successData)
|
||||
}
|
||||
|
||||
persistenceSource
|
||||
.getLaunchById(id)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -12,14 +12,13 @@ import org.amshove.kluent.shouldEqualTo
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class BaseInteractorTest {
|
||||
|
||||
val testInteractor = spyk(TestInteractor())
|
||||
val testParams = TestParams()
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `BaseInteractor should send states and items emmited by run`() {
|
||||
// Using run blocking due to threading problems in runBlockingTest
|
||||
// See https://github.com/Kotlin/kotlinx.coroutines/issues/1204
|
||||
@@ -42,24 +41,23 @@ class BaseInteractorTest {
|
||||
resultDeque.size shouldEqualTo 3
|
||||
|
||||
// Verify first item is Loading state
|
||||
resultDeque.poll() shouldBeInstanceOf Result.State.Loading::class
|
||||
resultDeque.poll() shouldBeInstanceOf State.Loading::class
|
||||
|
||||
// Verify second item is Success, with default value we set below in TestParams class
|
||||
resultDeque.poll().also {
|
||||
it shouldBeInstanceOf Result.Success::class
|
||||
(it as Result.Success<Int>).successData shouldEqualTo 10
|
||||
it shouldBeInstanceOf Success::class
|
||||
(it as Success<Int>).successData shouldEqualTo 10
|
||||
}
|
||||
|
||||
// Verify last item is Loaded state
|
||||
resultDeque.poll() shouldBeInstanceOf Result.State.Loaded::class
|
||||
resultDeque.poll() shouldBeInstanceOf State.Loaded::class
|
||||
}
|
||||
}
|
||||
|
||||
inner class TestInteractor : BaseInteractor<Int, TestParams>() {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override suspend fun run(collector: FlowCollector<Result<Int>>, params: TestParams) {
|
||||
collector.emit(Result.Success(params.testValue))
|
||||
override suspend fun FlowCollector<Result<Int>>.run(params: TestParams) {
|
||||
emit(Success(params.testValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ class ResultTest {
|
||||
|
||||
private val number = 10
|
||||
|
||||
private val success = Result.Success(number)
|
||||
private val failure = Result.Failure(Reason.GenericError())
|
||||
private val state = Result.State.Loading()
|
||||
private val success = Success(number)
|
||||
private val failure = Failure(GenericError())
|
||||
private val state = State.Loading()
|
||||
|
||||
private val emptyStateBlock = spyk({ _: Result.State -> })
|
||||
private val emptyStateBlock = spyk({ _: State -> })
|
||||
private val emptyFailureBlock = spyk({ _: Reason -> })
|
||||
private val emptySuccessBlock = spyk({ _: Int -> })
|
||||
|
||||
@@ -37,8 +37,8 @@ class ResultTest {
|
||||
@Test
|
||||
fun `Failure should only invoke failureBlock with correct error`() {
|
||||
val actualFailureBlock = spyk({ reason: Reason ->
|
||||
reason shouldBeInstanceOf Reason.GenericError::class
|
||||
(reason as Reason.GenericError).messageRes shouldEqualTo R.string.reason_generic
|
||||
reason shouldBeInstanceOf GenericError::class
|
||||
(reason as GenericError).messageRes shouldEqualTo R.string.reason_generic
|
||||
Unit
|
||||
})
|
||||
|
||||
@@ -51,8 +51,8 @@ class ResultTest {
|
||||
|
||||
@Test
|
||||
fun `State should only invoke stateBlock with correct state`() {
|
||||
val actualSuccessBlock = spyk({ state: Result.State ->
|
||||
state shouldBeInstanceOf Result.State.Loading::class
|
||||
val actualSuccessBlock = spyk({ state: State ->
|
||||
state shouldBeInstanceOf State.Loading::class
|
||||
Unit
|
||||
})
|
||||
|
||||
|
||||
@@ -4,8 +4,13 @@ import android.net.NetworkInfo
|
||||
import com.melih.repository.R
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.entities.LaunchesEntity
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.EmptyResultError
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.NetworkError
|
||||
import com.melih.repository.interactors.base.ResponseError
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.interactors.base.onFailure
|
||||
import com.melih.repository.interactors.base.onSuccess
|
||||
import com.melih.repository.network.ApiImpl
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
@@ -19,6 +24,7 @@ import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import javax.inject.Provider
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class NetworkSourceTest {
|
||||
|
||||
private val apiImpl = mockk<ApiImpl>(relaxed = true)
|
||||
@@ -32,66 +38,62 @@ class NetworkSourceTest {
|
||||
inner class GetNextLaunches {
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return network error when internet is not connected`() {
|
||||
every { networkInfoProvider.get().isConnected } returns false
|
||||
|
||||
runBlockingTest {
|
||||
val result = source.getNextLaunches(1)
|
||||
val result = source.getNextLaunches(1, 0)
|
||||
|
||||
result shouldBeInstanceOf Result.Failure::class
|
||||
result.handleFailure {
|
||||
it shouldBeInstanceOf Reason.NetworkError::class
|
||||
result shouldBeInstanceOf Failure::class
|
||||
result.onFailure {
|
||||
it shouldBeInstanceOf NetworkError::class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return response error when it is not successful`() {
|
||||
every { networkInfoProvider.get().isConnected } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns false
|
||||
coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns false
|
||||
|
||||
runBlockingTest {
|
||||
val result = source.getNextLaunches(1)
|
||||
val result = source.getNextLaunches(1, 0)
|
||||
|
||||
result shouldBeInstanceOf Result.Failure::class
|
||||
result.handleFailure {
|
||||
it shouldBeInstanceOf Reason.ResponseError::class
|
||||
(it as Reason.ResponseError).messageRes shouldEqualTo R.string.reason_response
|
||||
result shouldBeInstanceOf Failure::class
|
||||
result.onFailure {
|
||||
it shouldBeInstanceOf ResponseError::class
|
||||
(it as ResponseError).messageRes shouldEqualTo R.string.reason_response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return empty result error when body is null`() {
|
||||
every { networkInfoProvider.get().isConnected } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any()).body() } returns null
|
||||
coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns null
|
||||
|
||||
runBlockingTest {
|
||||
val result = source.getNextLaunches(1)
|
||||
val result = source.getNextLaunches(1, 0)
|
||||
|
||||
result shouldBeInstanceOf Result.Failure::class
|
||||
result.handleFailure {
|
||||
it shouldBeInstanceOf Reason.EmptyResultError::class
|
||||
result shouldBeInstanceOf Failure::class
|
||||
result.onFailure {
|
||||
it shouldBeInstanceOf EmptyResultError::class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return success with data if execution is successful`() {
|
||||
every { networkInfoProvider.get().isConnected } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013)))
|
||||
coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true
|
||||
coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013)))
|
||||
|
||||
runBlockingTest {
|
||||
val result = source.getNextLaunches(1)
|
||||
val result = source.getNextLaunches(1, 0)
|
||||
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
result shouldBeInstanceOf Success::class
|
||||
result.onSuccess {
|
||||
it shouldBeInstanceOf List::class
|
||||
it.size shouldEqualTo 1
|
||||
it[0].id shouldEqualTo 1013
|
||||
|
||||
@@ -1,51 +1,68 @@
|
||||
package com.melih.repository.sources
|
||||
|
||||
import android.content.Context
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.PersistenceEmpty
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import com.melih.repository.interactors.base.onFailure
|
||||
import com.melih.repository.interactors.base.onSuccess
|
||||
import com.melih.repository.persistence.LaunchesDatabase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.spyk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeInstanceOf
|
||||
import org.amshove.kluent.shouldEqualTo
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PersistanceSourceTest {
|
||||
|
||||
private val ctx = mockk<Context>(relaxed = true)
|
||||
private val dbImplementation = mockk<LaunchesDatabase>(relaxed = true)
|
||||
private val source = spyk(PersistenceSource(dbImplementation))
|
||||
private val source = spyk(PersistenceSource(ctx))
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkObject(LaunchesDatabase)
|
||||
every { LaunchesDatabase.getInstance(ctx) } returns dbImplementation
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetNextLaunches {
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return persistance empty error when db is empty`() {
|
||||
runBlockingTest {
|
||||
coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns emptyList()
|
||||
coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns emptyList()
|
||||
|
||||
val result = source.getNextLaunches(10)
|
||||
result shouldBeInstanceOf Result.Failure::class
|
||||
result.handleFailure {
|
||||
it shouldBeInstanceOf Reason.PersistenceEmpty::class
|
||||
scope.launch {
|
||||
val result = source.getNextLaunches(10, 0)
|
||||
|
||||
result shouldBeInstanceOf Failure::class
|
||||
result.onFailure {
|
||||
it shouldBeInstanceOf PersistenceEmpty::class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return success with data if db is not empty`() {
|
||||
runBlockingTest {
|
||||
coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns listOf(LaunchEntity(id = 1013))
|
||||
coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns listOf(LaunchEntity(id = 1013))
|
||||
|
||||
val result = source.getNextLaunches(10)
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
scope.launch {
|
||||
val result = source.getNextLaunches(10, 0)
|
||||
|
||||
result shouldBeInstanceOf Success::class
|
||||
result.onSuccess {
|
||||
it.isEmpty() shouldBe false
|
||||
it.size shouldEqualTo 1
|
||||
it[0].id shouldEqualTo 1013
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package com.melih.repository.sources
|
||||
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import io.mockk.called
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.amshove.kluent.shouldBeInstanceOf
|
||||
import org.amshove.kluent.shouldEqualTo
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SourceManagerTest {
|
||||
|
||||
private val networkSource = mockk<NetworkSource>(relaxed = true)
|
||||
private val persistenceSource = mockk<PersistenceSource>(relaxed = true)
|
||||
|
||||
private val sourceManager = spyk(SourceManager(networkSource, persistenceSource))
|
||||
|
||||
@Nested
|
||||
inner class GetNextLaunches {
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should try to fetch, save and return result from persistance`() {
|
||||
runBlockingTest {
|
||||
val amount = 10
|
||||
coEvery { networkSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1012)))
|
||||
coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013)))
|
||||
|
||||
val result = sourceManager.getNextLaunches(amount)
|
||||
|
||||
coVerifyOrder {
|
||||
networkSource.getNextLaunches(any())
|
||||
persistenceSource.saveLaunches(any())
|
||||
persistenceSource.getNextLaunches(any())
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { networkSource.getNextLaunches(any()) }
|
||||
coVerify(exactly = 1) { persistenceSource.saveLaunches(any()) }
|
||||
coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) }
|
||||
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
it.size shouldEqualTo 1
|
||||
it[0].id shouldEqualTo 1013
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should not save response if fetching was failure and return result from persistance`() {
|
||||
runBlockingTest {
|
||||
val amount = 10
|
||||
coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError())
|
||||
coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013)))
|
||||
|
||||
val result = sourceManager.getNextLaunches(amount)
|
||||
|
||||
coVerify { persistenceSource.saveLaunches(any()) wasNot called }
|
||||
coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) }
|
||||
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
it.size shouldEqualTo 1
|
||||
it[0].id shouldEqualTo 1013
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `should return failure if network and persistance fails`() {
|
||||
runBlockingTest {
|
||||
val amount = 10
|
||||
coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError())
|
||||
coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Failure(Reason.PersistenceEmpty())
|
||||
|
||||
val result = sourceManager.getNextLaunches(amount)
|
||||
|
||||
coVerify { persistenceSource.saveLaunches(any()) wasNot called }
|
||||
coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) }
|
||||
|
||||
result shouldBeInstanceOf Result.Failure::class
|
||||
result.handleFailure {
|
||||
it shouldBeInstanceOf Reason.NoNetworkPersistenceEmpty::class
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetLaunchDetails {
|
||||
|
||||
@Test
|
||||
fun `should return result from persistance immediately if it's found`() {
|
||||
runBlocking {
|
||||
coEvery { persistenceSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013))
|
||||
|
||||
val result = sourceManager.getLaunchById(1)
|
||||
|
||||
coVerify { networkSource.getLaunchById(any()) wasNot called }
|
||||
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
it.id shouldEqualTo 1013
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fetch result from network if it's not found in persistance`() {
|
||||
runBlocking {
|
||||
coEvery {
|
||||
persistenceSource.getLaunchById(any())
|
||||
} returns Result.Failure(Reason.PersistenceEmpty()) andThen Result.Success(LaunchEntity(id = 1013))
|
||||
|
||||
coEvery { networkSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013))
|
||||
|
||||
val result = sourceManager.getLaunchById(1)
|
||||
|
||||
coVerify(exactly = 1) { networkSource.getLaunchById(any()) }
|
||||
coVerify(exactly = 1) { persistenceSource.saveLaunch(any()) }
|
||||
coVerify(exactly = 2) { persistenceSource.getLaunchById(any()) }
|
||||
|
||||
result shouldBeInstanceOf Result.Success::class
|
||||
result.handleSuccess {
|
||||
it.id shouldEqualTo 1013
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user