init
This commit is contained in:
1
data/.gitignore
vendored
Normal file
1
data/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
51
data/build.gradle.kts
Normal file
51
data/build.gradle.kts
Normal file
@@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = Config.SdkVersion.COMPILE
|
||||
namespace = "com.testappbank.test"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Config.SdkVersion.MIN
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))
|
||||
implementation(project(":domain:domain"))
|
||||
|
||||
implementation(libs.timber)
|
||||
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.retrofit.converter.scalars)
|
||||
|
||||
|
||||
// Okhttp
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging.interceptor)
|
||||
|
||||
//ROOM
|
||||
implementation(libs.room.runtime)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
}
|
||||
21
data/proguard-rules.pro
vendored
Normal file
21
data/proguard-rules.pro
vendored
Normal 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.kts.kts.
|
||||
#
|
||||
# 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
|
||||
59
data/src/main/java/com/testapp/data/api/RemoteWeather.kt
Normal file
59
data/src/main/java/com/testapp/data/api/RemoteWeather.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.testapp.data.api
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class RemoteWeather(
|
||||
@SerializedName("currentConditions") val cc: RemoteWeatherCurrentConditions
|
||||
)
|
||||
|
||||
|
||||
data class RemoteWeatherCurrentConditions(
|
||||
@SerializedName("datetimeEpoch") val datetime: Long,
|
||||
@SerializedName("temp") val temp: String,
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* Response sample
|
||||
"*"{
|
||||
"latitude":38.9697,
|
||||
"longitude":-77.385,
|
||||
"resolvedAddress":"Reston, VA, United States",
|
||||
"address":" Reston,VA",
|
||||
"timezone":"America/New_York",
|
||||
"tzoffset":-5,
|
||||
"description":"Cooling down with a chance of rain on Friday.",
|
||||
"days":[
|
||||
{
|
||||
"datetime":"2020-11-12",
|
||||
"datetimeEpoch":1605157200,
|
||||
"temp":59.6,
|
||||
"feelslike":59.6,
|
||||
"...""stations":{
|
||||
|
||||
},
|
||||
"source":"obs",
|
||||
"hours":[
|
||||
{
|
||||
"datetime":"01:00:00",
|
||||
"..."
|
||||
},
|
||||
"."
|
||||
},
|
||||
"..."
|
||||
],
|
||||
"alerts":[
|
||||
{
|
||||
"event":"Flash Flood Watch",
|
||||
"description":"..."
|
||||
}
|
||||
],
|
||||
"currentConditions":{
|
||||
"datetime":"2020-11-11T22:48:35",
|
||||
"datetimeEpoch":160515291500,
|
||||
"temp":67.9,
|
||||
"..."
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.testapp.data.api
|
||||
|
||||
import com.testapp.data.datasource.RemoteWeatherDataSource
|
||||
import com.testapp.domain.domain.models.ActionResult
|
||||
import com.testapp.domain.domain.models.Weather
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class RemoteWeatherDataSourceImpl @Inject constructor(
|
||||
private val restClient: RestClient
|
||||
) : RemoteWeatherDataSource {
|
||||
|
||||
|
||||
override suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather> {
|
||||
return try {
|
||||
val remoteWeather = restClient.provideWeatherApi().getWeatherByLocation(placeName)
|
||||
ActionResult.Success(remoteWeather.mapToDomainWhether(placeName))
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e)
|
||||
ActionResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun RemoteWeather.mapToDomainWhether(placeName: String) = Weather(
|
||||
id = UUID.randomUUID().toString(),
|
||||
location = placeName,
|
||||
date = Date(cc.datetime * 1000),
|
||||
temperature = cc.temp.toDouble()
|
||||
)
|
||||
5
data/src/main/java/com/testapp/data/api/RestClient.kt
Normal file
5
data/src/main/java/com/testapp/data/api/RestClient.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.testapp.data.api
|
||||
|
||||
interface RestClient {
|
||||
fun provideWeatherApi(): WeatherApi
|
||||
}
|
||||
44
data/src/main/java/com/testapp/data/api/RestClientImpl.kt
Normal file
44
data/src/main/java/com/testapp/data/api/RestClientImpl.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.testapp.data.api
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class RestClientImpl @Inject constructor() : RestClient {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL =
|
||||
"https://weather.visualcrossing.com/"
|
||||
const val TIME_OUT = 25L
|
||||
}
|
||||
|
||||
private val gson = Gson()
|
||||
private val okHttpClient by lazy { provideOkHttp() }
|
||||
private val retrofit: Retrofit by lazy { creteRetrofit(okHttpClient) }
|
||||
|
||||
|
||||
private fun provideOkHttp(): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||
|
||||
builder.addInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT))
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
private fun creteRetrofit(okHttpClient: OkHttpClient) = Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
|
||||
|
||||
override fun provideWeatherApi() = retrofit.create(WeatherApi::class.java)
|
||||
|
||||
}
|
||||
13
data/src/main/java/com/testapp/data/api/WeatherApi.kt
Normal file
13
data/src/main/java/com/testapp/data/api/WeatherApi.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.testapp.data.api
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface WeatherApi {
|
||||
@GET("VisualCrossingWebServices/rest/services/timeline/{location}")
|
||||
suspend fun getWeatherByLocation(
|
||||
@Path("location") location: String,
|
||||
@Query("key") key: String = "3CHX9BE656PW6PYSV43FA493M",
|
||||
): RemoteWeather
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.testapp.data.datasource
|
||||
|
||||
import com.testapp.domain.domain.models.Weather
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalWeatherStorage {
|
||||
|
||||
suspend fun getWeather(id: String): Weather?
|
||||
|
||||
suspend fun saveWeather(weather: Weather)
|
||||
|
||||
fun subscribeForWeatherList(): Flow<List<Weather>>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.testapp.data.datasource
|
||||
|
||||
import com.testapp.domain.domain.models.ActionResult
|
||||
import com.testapp.domain.domain.models.Weather
|
||||
|
||||
interface RemoteWeatherDataSource {
|
||||
suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather>
|
||||
|
||||
}
|
||||
27
data/src/main/java/com/testapp/data/db/DbFactory.kt
Normal file
27
data/src/main/java/com/testapp/data/db/DbFactory.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.testapp.data.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class DbFactory @Inject constructor(
|
||||
@ApplicationContext val context: Context
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val DATA_BASE_NAME = "WeatherDb"
|
||||
}
|
||||
|
||||
private val database by lazy { createDB(context) }
|
||||
|
||||
private fun createDB(context: Context) =
|
||||
Room.databaseBuilder(context, WeatherDb::class.java, DATA_BASE_NAME)
|
||||
.addCallback(object : RoomDatabase.Callback() {})
|
||||
.build()
|
||||
|
||||
private fun provideDB() = database
|
||||
|
||||
fun provideWeatherDao() = provideDB().weatherDao()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.testapp.data.db
|
||||
|
||||
import com.testapp.data.datasource.LocalWeatherStorage
|
||||
import com.testapp.data.db.table.WeatherEntity
|
||||
import com.testapp.domain.domain.models.Weather
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class LocalWeatherStorageImpl @Inject constructor(
|
||||
private val dbFactory: DbFactory,
|
||||
) : LocalWeatherStorage {
|
||||
|
||||
override suspend fun getWeather(id: String): Weather? {
|
||||
return dbFactory.provideWeatherDao().getWeather(id)?.toWeather()
|
||||
}
|
||||
|
||||
override suspend fun saveWeather(weather: Weather) {
|
||||
dbFactory.provideWeatherDao().insert(listOf(weather.toWeather()))
|
||||
}
|
||||
|
||||
override fun subscribeForWeatherList(): Flow<List<Weather>> {
|
||||
return dbFactory.provideWeatherDao()
|
||||
.observeWeatherList().map { weatherEntities ->
|
||||
weatherEntities.map { it.toWeather() }
|
||||
}.onStart {
|
||||
if (dbFactory.provideWeatherDao().getSavedPlaceCount() == 0)
|
||||
emit(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WeatherEntity.toWeather() = Weather(
|
||||
id = _id,
|
||||
location = location,
|
||||
date = Date(date),
|
||||
temperature = temperature ?: 0.0,
|
||||
)
|
||||
|
||||
|
||||
private fun Weather.toWeather() = WeatherEntity(
|
||||
_id = id,
|
||||
location = location,
|
||||
date = date.time,
|
||||
temperature = temperature,
|
||||
)
|
||||
19
data/src/main/java/com/testapp/data/db/WeatherDb.kt
Normal file
19
data/src/main/java/com/testapp/data/db/WeatherDb.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.testapp.data.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.testapp.data.db.dao.WeatherDao
|
||||
import com.testapp.data.db.table.WeatherEntity
|
||||
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
WeatherEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class WeatherDb : RoomDatabase() {
|
||||
abstract fun weatherDao(): WeatherDao
|
||||
|
||||
}
|
||||
28
data/src/main/java/com/testapp/data/db/dao/WeatherDao.kt
Normal file
28
data/src/main/java/com/testapp/data/db/dao/WeatherDao.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.testapp.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.testapp.data.db.table.WeatherEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface WeatherDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(commodityInfoList: List<WeatherEntity>)
|
||||
|
||||
@Query("Select count(*) from ${WeatherEntity.TABLE_NAME}")
|
||||
suspend fun getSavedPlaceCount(): Int
|
||||
|
||||
@Query("Select * from ${WeatherEntity.TABLE_NAME}")
|
||||
fun observeWeatherList(): Flow<List<WeatherEntity>>
|
||||
|
||||
@Query("Select * from ${WeatherEntity.TABLE_NAME} where ${WeatherEntity.ID} =:id ")
|
||||
suspend fun getWeather(id: String): WeatherEntity?
|
||||
|
||||
@Query("Delete from ${WeatherEntity.TABLE_NAME} where ${WeatherEntity.ID} =:id")
|
||||
suspend fun delete(id: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.testapp.data.db.table
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = WeatherEntity.TABLE_NAME)
|
||||
class WeatherEntity(
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey
|
||||
val _id: String,
|
||||
@ColumnInfo(name = NAME)
|
||||
val location: String,
|
||||
@ColumnInfo(name = DATE)
|
||||
val date: Long,
|
||||
@ColumnInfo(name = TEMP)
|
||||
val temperature: Double?
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE_NAME = "SavedWeather"
|
||||
const val ID = "_id"
|
||||
const val NAME = "location"
|
||||
const val DATE = "date"
|
||||
const val TEMP = "temp"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.testapp.data.gatway
|
||||
|
||||
import com.testapp.data.datasource.LocalWeatherStorage
|
||||
import com.testapp.data.datasource.RemoteWeatherDataSource
|
||||
import com.testapp.domain.domain.gatway.WeatherGateWay
|
||||
import com.testapp.domain.domain.models.ActionResult
|
||||
import com.testapp.domain.domain.models.Weather
|
||||
import com.testapp.domain.domain.usecase.GetWeatherList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class WeatherGateWayImpl @Inject constructor(
|
||||
private val localWeatherStorage: LocalWeatherStorage,
|
||||
private val remoteWeatherDataSource: RemoteWeatherDataSource,
|
||||
private val getWeatherList: GetWeatherList,
|
||||
) : WeatherGateWay {
|
||||
|
||||
override suspend fun getWeather(id: String): Weather {
|
||||
return localWeatherStorage.getWeather(id) ?: throw RuntimeException("No Weather by id")
|
||||
}
|
||||
|
||||
override suspend fun subscribeForWeatherList(): Flow<List<Weather>> {
|
||||
return getWeatherList.subscribeForWeatherList()
|
||||
}
|
||||
|
||||
override suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather> {
|
||||
return remoteWeatherDataSource.requestNewWeatherForPlace(placeName)
|
||||
}
|
||||
|
||||
override suspend fun saveWeather(weather: Weather) {
|
||||
localWeatherStorage.saveWeather(weather)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user