This commit is contained in:
2026-02-27 12:50:54 +03:00
commit c6c8897cb4
105 changed files with 2935 additions and 0 deletions

1
data/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

51
data/build.gradle.kts Normal file
View 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
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.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

View 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,
"..."
}
}
*/

View File

@@ -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()
)

View File

@@ -0,0 +1,5 @@
package com.testapp.data.api
interface RestClient {
fun provideWeatherApi(): WeatherApi
}

View 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)
}

View 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
}

View File

@@ -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>>
}

View File

@@ -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>
}

View 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()
}

View File

@@ -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,
)

View 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
}

View 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)
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}