WIP: feature/abstractions (#45)

* Abstraction layer backup

* Removed DataEntity, was unnecessary for now

* Separated network, persistence, entities and interaction, closes #29

* Renamed binding

* Removed build files, example tests

Removed build files, example tests

* Fixed build files were not being ignored all around app

* Updated CI ymls

* Small changes

* Fixed legacy repository package names

* Fixed CQ findings

* Updated Fastlane

* Packaging changes and version upgrades

* Removed core from interactors

* Version bumps

* Added new module graph
This commit is contained in:
Melih Aksoy
2019-10-30 17:27:53 +01:00
committed by GitHub
parent 83e39400a9
commit 88022629e1
103 changed files with 1098 additions and 921 deletions

View File

@@ -0,0 +1,17 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/module.gradle"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':abstractions')
implementation libraries.room
implementation libraries.moshiKotlin
kapt annotationProcessors.roomCompiler
kapt annotationProcessors.moshi
}

View File

21
data/definitions/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.melih.definitions.entities" />

View File

@@ -0,0 +1,4 @@
package com.melih.definitions
internal const val DEFAULT_NAME = "Default name"
internal const val EMPTY_STRING = ""

View File

@@ -0,0 +1,23 @@
package com.melih.definitions
import com.melih.abstractions.data.ViewEntity
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.entities.LaunchEntity
/**
* Contract for sources to seperate business logic from build and return type
*/
interface Source {
//region Abstractions
suspend fun <T : ViewEntity> getNextLaunches(
count: Int,
page: Int,
mapper: Mapper<LaunchEntity, T>
): Result<List<T>>
suspend fun <T : ViewEntity> getLaunchById(id: Long, mapper: Mapper<LaunchEntity, T>): Result<T>
//endregion
}

View File

@@ -0,0 +1,19 @@
package com.melih.definitions.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.melih.definitions.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,
@field:Json(name = "wsstamp") val launchStartTime: Long = 0L,
@field:Json(name = "westamp") val launchEndTime: Long = 0L,
val location: LocationEntity = LocationEntity(),
val rocket: RocketEntity = RocketEntity(),
val missions: List<MissionEntity> = listOf(MissionEntity())
)

View File

@@ -0,0 +1,12 @@
package com.melih.definitions.entities
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LaunchesEntity(
val id: Long = 0L,
val launches: List<LaunchEntity> = listOf(),
val total: Int = 0,
val offset: Int = 0,
val count: Int = 0
)

View File

@@ -0,0 +1,20 @@
package com.melih.definitions.entities
import androidx.room.ColumnInfo
import com.melih.definitions.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,
val lat: Long = 0L,
val long: Long = 0L
)

View File

@@ -0,0 +1,14 @@
package com.melih.definitions.entities
import androidx.room.ColumnInfo
import com.melih.definitions.DEFAULT_NAME
import com.melih.definitions.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,
val description: String = EMPTY_STRING,
val typeName: String = EMPTY_STRING
)

View File

@@ -0,0 +1,16 @@
package com.melih.definitions.entities
import androidx.room.ColumnInfo
import com.melih.definitions.DEFAULT_NAME
import com.melih.definitions.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,
@field:Json(name = "familyname") val familyName: String = DEFAULT_NAME,
val imageSizes: IntArray = intArrayOf(),
val imageURL: String = EMPTY_STRING
)

View File

@@ -0,0 +1,22 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/module.gradle"
apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':data:definitions')
implementation project(':data:network')
implementation project(':data:persistence')
implementation libraries.coroutines
implementation libraries.retrofit
testImplementation testLibraries.coroutinesCore
testImplementation testLibraries.coroutinesTest
compileOnly libraries.room
}

View File

21
data/interactors/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.melih.interactors" />

View File

@@ -0,0 +1,40 @@
package com.melih.interactors
import com.melih.abstractions.data.ViewEntity
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.entities.LaunchEntity
import com.melih.interactors.base.BaseInteractor
import com.melih.interactors.base.InteractorParameters
import com.melih.interactors.sources.LaunchesSource
import kotlinx.coroutines.flow.FlowCollector
import javax.inject.Inject
/**
* Gets next given number of launches
*/
class GetLaunchDetails<T : ViewEntity> @Inject constructor(
private val mapper: @JvmSuppressWildcards Mapper<LaunchEntity, T>
) : BaseInteractor<T, GetLaunchDetails.Params>() {
//region Properties
@Inject
internal lateinit var launchesSource: LaunchesSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<T>>.run(params: Params) {
emit(launchesSource.getLaunchById(params.id, mapper))
}
//endregion
//region Parameters
data class Params(
val id: Long
) : InteractorParameters
//endregion
}

View File

@@ -0,0 +1,44 @@
package com.melih.interactors
import com.melih.abstractions.data.ViewEntity
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.entities.LaunchEntity
import com.melih.interactors.base.BaseInteractor
import com.melih.interactors.base.InteractorParameters
import com.melih.interactors.sources.LaunchesSource
import kotlinx.coroutines.flow.FlowCollector
import javax.inject.Inject
const val DEFAULT_LAUNCHES_AMOUNT = 15
/**
* Gets next given number of launches
*/
class GetLaunches<T : ViewEntity> @Inject constructor(
private val mapper: @JvmSuppressWildcards Mapper<LaunchEntity, T>
) : BaseInteractor<List<T>, GetLaunches.Params>() {
//region Properties
@Inject
internal lateinit var launchesSource: LaunchesSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<List<T>>>.run(params: Params) {
// Start network fetch - we're not handling state here to ommit them
emit(
launchesSource
.getNextLaunches(params.count, params.page, mapper)
)
}
//endregion
data class Params(
val count: Int = DEFAULT_LAUNCHES_AMOUNT,
val page: Int
) : InteractorParameters
}

View File

@@ -0,0 +1,42 @@
package com.melih.interactors.base
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.deliverable.State
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
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
protected abstract suspend fun FlowCollector<Result<T>>.run(params: P)
//endregion
//region Functions
operator fun invoke(params: P) =
flow<Result<T>> {
emit(State.Loading())
run(params)
emit(State.Loaded())
}.flowOn(Dispatchers.IO)
//endregion
}
/**
* Contract for parameter classes
*/
interface InteractorParameters
/**
* Symbolizes absence of parameters for an [interactor][BaseInteractor]
*/
class None : Any(), InteractorParameters

View File

@@ -0,0 +1,17 @@
package com.melih.interactors.error
import androidx.annotation.StringRes
import com.melih.abstractions.deliverable.Reason
import com.melih.interactors.R
sealed class InteractionErrorReason(@StringRes override val messageRes: Int) : Reason()
class GenericError(@StringRes override val messageRes: Int = R.string.reason_generic) : InteractionErrorReason(messageRes)
sealed class NetworkError(override val messageRes: Int) : InteractionErrorReason(messageRes)
class ConnectionError : NetworkError(R.string.reason_network)
class EmptyResultError : NetworkError(R.string.reason_empty_body)
class ResponseError : NetworkError(R.string.reason_response)
class TimeoutError : NetworkError(R.string.reason_timeout)
class PersistenceEmptyError : InteractionErrorReason(R.string.reason_persistance_empty)

View File

@@ -0,0 +1,163 @@
package com.melih.interactors.sources
import android.content.Context
import android.net.NetworkInfo
import com.melih.abstractions.data.ViewEntity
import com.melih.abstractions.deliverable.Failure
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.deliverable.Success
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.Source
import com.melih.definitions.entities.LaunchEntity
import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT
import com.melih.interactors.error.ConnectionError
import com.melih.interactors.error.EmptyResultError
import com.melih.interactors.error.NetworkError
import com.melih.interactors.error.PersistenceEmptyError
import com.melih.interactors.error.ResponseError
import com.melih.interactors.error.TimeoutError
import com.melih.network.ApiImpl
import com.melih.persistence.LaunchesDatabase
import retrofit2.Response
import java.io.IOException
import javax.inject.Inject
import javax.inject.Provider
private const val DEFAULT_IMAGE_SIZE = 480
internal class LaunchesSource @Inject constructor(
ctx: Context,
private val apiImpl: ApiImpl,
private val networkInfoProvider: Provider<NetworkInfo>
) : Source {
//region Properties
private val launchesDatabase = LaunchesDatabase.getInstance(ctx)
private val isNetworkConnected: Boolean
get() {
val networkInfo = networkInfoProvider.get()
return networkInfo != null && networkInfo.isConnected
}
//endregion
//region Functions
override suspend fun <T : ViewEntity> getNextLaunches(
count: Int,
page: Int, mapper: Mapper<LaunchEntity, T>
): Result<List<T>> {
val networkResponse = safeExecute({
apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT)
}) { entity ->
entity.launches
.map(::transformRocketImageUrl)
.saveLaunches()
.map(mapper::convert)
}
return if (networkResponse is NetworkError) {
launchesDatabase
.launchesDao
.getLaunches(count, page)
.takeUnless { it.isNullOrEmpty() }
?.run {
Success(map(mapper::convert))
} ?: Failure(PersistenceEmptyError())
} else {
networkResponse
}
}
override suspend fun <T : ViewEntity> getLaunchById(
id: Long,
mapper: Mapper<LaunchEntity, T>
): Result<T> {
return launchesDatabase
.launchesDao
.getLaunchById(id)
.takeIf { it != null }
?.run {
Success(mapper.convert(this))
} ?: loadLaunchFromNetwork(id, mapper)
}
private suspend fun <T : ViewEntity> loadLaunchFromNetwork(
id: Long,
mapper: Mapper<LaunchEntity, T>
): Result<T> =
safeExecute({
apiImpl.getLaunchById(id)
}) {
mapper.convert(
transformRocketImageUrl(it)
.saveLaunch()
)
}
private suspend fun List<LaunchEntity>.saveLaunches() = run {
launchesDatabase.launchesDao.saveLaunches(this)
this
}
private suspend fun LaunchEntity.saveLaunch() = run {
launchesDatabase.launchesDao.saveLaunch(this)
this
}
private inline fun <T, R> safeExecute(
block: () -> Response<T>,
transform: (T) -> R
) =
if (isNetworkConnected) {
try {
block().extractResponseBody(transform)
} catch (e: IOException) {
Failure(TimeoutError())
}
} else {
Failure(ConnectionError())
}
private inline fun <T, R> Response<T>.extractResponseBody(transform: (T) -> R) =
if (isSuccessful) {
body()?.let {
Success(transform(it))
} ?: Failure(EmptyResultError())
} else {
Failure(ResponseError())
}
private fun transformRocketImageUrl(launch: LaunchEntity) =
if (!launch.rocket.imageURL.isNotBlank()) {
launch.copy(
rocket = launch.rocket.copy(
imageURL = transformImageUrl(
launch.rocket.imageURL,
launch.rocket.imageSizes
)
)
)
} else {
launch
}
private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) =
try {
val urlSplit = imageUrl.split("_")
val url = urlSplit[0]
val format = urlSplit[1].split(".")[1]
val requestedSize = if (!supportedSizes.contains(DEFAULT_IMAGE_SIZE)) {
supportedSizes.last { it < DEFAULT_IMAGE_SIZE }
} else {
DEFAULT_IMAGE_SIZE
}
"${url}_$requestedSize.$format"
} catch (e: Exception) {
imageUrl
}
//endregion
}

View File

@@ -0,0 +1,8 @@
<resources>
<string name="reason_generic">Something went wrong</string>
<string name="reason_persistance_empty">There are no saved launches</string>
<string name="reason_network">Network error</string>
<string name="reason_empty_body">Response is empty</string>
<string name="reason_response">Woops, seems we got a server error</string>
<string name="reason_timeout">Server timed out</string>
</resources>

View File

@@ -0,0 +1,68 @@
package com.melih.interactors.base
import com.melih.abstractions.deliverable.Result
import com.melih.abstractions.deliverable.State
import com.melih.abstractions.deliverable.Success
import io.mockk.coVerify
import io.mockk.spyk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.shouldBeInstanceOf
import org.amshove.kluent.shouldEqualTo
import org.junit.jupiter.api.Test
import java.util.ArrayDeque
@UseExperimental(ExperimentalCoroutinesApi::class)
class BaseInteractorTest {
val testInteractor = spyk(TestInteractor())
val testParams = TestParams()
@Test
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
runBlocking {
// Get result by invoking
val result = testInteractor(testParams)
// Verify we invoked interactor exactly once
coVerify(exactly = 1) { testInteractor.invoke(any()) }
// Verify result is type of Flow
result shouldBeInstanceOf Flow::class
// This will actually collec the flow
val resultDeque = ArrayDeque<Result<Int>>()
result.toCollection(resultDeque)
// We sent exactly 3 items, verify size
resultDeque.size shouldEqualTo 3
// Verify first item is Loading state
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 Success::class
(it as Success<Int>).successData shouldEqualTo 10
}
// Verify last item is Loaded state
resultDeque.poll() shouldBeInstanceOf State.Loaded::class
}
}
inner class TestInteractor : BaseInteractor<Int, TestParams>() {
override suspend fun FlowCollector<Result<Int>>.run(params: TestParams) {
emit(Success(params.testValue))
}
}
data class TestParams(val testValue: Int = 10) : InteractorParameters
}

20
data/network/build.gradle Normal file
View File

@@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/module.gradle"
apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':data:definitions')
implementation libraries.okHttpLogger
implementation libraries.moshiKotlin
implementation libraries.coroutines
implementation libraries.retrofit
testImplementation testLibraries.coroutinesCore
testImplementation testLibraries.coroutinesTest
}

View File

21
data/network/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.melih.network" />

View File

@@ -0,0 +1,28 @@
package com.melih.network
import com.melih.definitions.entities.LaunchEntity
import com.melih.definitions.entities.LaunchesEntity
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
/**
* Retrofit interface for networking
*/
internal interface Api {
//region Get
@GET("launch/next/{count}")
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>
//endregion
}

View File

@@ -0,0 +1,57 @@
package com.melih.network
import com.melih.definitions.entities.LaunchEntity
import com.melih.definitions.entities.LaunchesEntity
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal const val TIMEOUT_DURATION = 7L
class ApiImpl @Inject constructor() : Api {
//region Properties
private val service by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
Retrofit.Builder()
.client(
OkHttpClient.Builder()
.connectTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
.readTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS)
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
).build()
)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl("https://launchlibrary.net/1.4/")
.build()
.create(Api::class.java)
}
//endregion
//region Functions
override suspend fun getNextLaunches(
count: Int,
offset: Int
): Response<LaunchesEntity> =
service.getNextLaunches(count, offset)
override suspend fun getLaunchById(
id: Long
): Response<LaunchEntity> =
service.getLaunchById(id)
//endregion
}

View File

@@ -0,0 +1,31 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/module.gradle"
apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$rootProject.projectDir/reports/room".toString()]
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':data:definitions')
implementation libraries.moshiKotlin
implementation libraries.coroutines
implementation libraries.room
kapt annotationProcessors.roomCompiler
testImplementation testLibraries.coroutinesCore
testImplementation testLibraries.coroutinesTest
}

View File

21
data/persistence/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.melih.persistence" />

View File

@@ -0,0 +1,53 @@
package com.melih.persistence
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.melih.definitions.entities.LaunchEntity
import com.melih.persistence.converters.LocationConverter
import com.melih.persistence.converters.MissionConverter
import com.melih.persistence.converters.RocketConverter
import com.melih.persistence.dao.LaunchesDao
const val DB_NAME = "LaunchesDB"
/**
* DB that manages launches
*/
@Database(
entities = [LaunchEntity::class],
exportSchema = true,
version = 1
)
@TypeConverters(
LocationConverter::class,
RocketConverter::class,
MissionConverter::class
)
abstract class LaunchesDatabase : RoomDatabase() {
//region Companion
companion object {
private lateinit var instance: LaunchesDatabase
fun getInstance(ctx: Context): LaunchesDatabase {
if (!::instance.isInitialized) {
instance = Room.databaseBuilder(ctx, LaunchesDatabase::class.java, DB_NAME)
.build()
}
return instance
}
}
//endregion
//region Abstractions
abstract val launchesDao: LaunchesDao
//endregion
}

View File

@@ -0,0 +1,35 @@
package com.melih.persistence.converters
import androidx.room.TypeConverter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
/**
* Base converter for reduced boilerplate code
*/
abstract class BaseConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
//endregion
//region Properties
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
//endregion
//region Functions
@TypeConverter
fun convertFrom(item: T) =
getAdapter(moshi).toJson(item)
@TypeConverter
fun convertTo(string: String) =
getAdapter(moshi).fromJson(string)
//endregion
}

View File

@@ -0,0 +1,35 @@
package com.melih.persistence.converters
import androidx.room.TypeConverter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
/**
* Base converter for reduced boilerplate code
*/
abstract class BaseListConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
//endregion
//region Properties
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
//endregion
//region Functions
@TypeConverter
fun convertFrom(items: List<T>) =
getAdapter(moshi).toJson(items)
@TypeConverter
fun convertTo(string: String): List<T>? =
getAdapter(moshi).fromJson(string)
//endregion
}

View File

@@ -0,0 +1,15 @@
package com.melih.persistence.converters
import com.melih.definitions.entities.LocationEntity
import com.melih.definitions.entities.LocationEntityJsonAdapter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
/**
* Converts [location][LocationEntity]
*/
class LocationConverter : BaseConverter<LocationEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> =
LocationEntityJsonAdapter(moshi)
}

View File

@@ -0,0 +1,20 @@
package com.melih.persistence.converters
import com.melih.definitions.entities.MissionEntity
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
/**
* Converts [mission][MissionEntity]
*/
class MissionConverter : BaseListConverter<MissionEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<List<MissionEntity>> =
moshi.adapter(
Types.newParameterizedType(
List::class.java,
MissionEntity::class.java
)
)
}

View File

@@ -0,0 +1,15 @@
package com.melih.persistence.converters
import com.melih.definitions.entities.RocketEntity
import com.melih.definitions.entities.RocketEntityJsonAdapter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
/**
* Converts [rocket][RocketEntity]
*/
class RocketConverter : BaseConverter<RocketEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> =
RocketEntityJsonAdapter(moshi)
}

View File

@@ -0,0 +1,35 @@
package com.melih.persistence.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.melih.definitions.entities.LaunchEntity
/**
* DAO for list of [launches][LaunchEntity]
*/
@Dao
abstract class LaunchesDao {
//region Queries
@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?
@Query("DELETE FROM Launches")
abstract suspend fun nukeLaunches()
//endregion
//region Insertion
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveLaunches(launches: List<LaunchEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveLaunch(launch: LaunchEntity)
//endregion
}