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

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/.idea/
/buildSrc/build/

28
README.md Normal file
View File

@@ -0,0 +1,28 @@
# Welcome
Welcome and thanks for accepting invitation.
⚠️ This test project is designed with list of problems:
Compilation errors, runtime crashes and architecture problems and etc...
Do not pay attention to ui but pls proceed with the UX.
# OVERVIEW:
It a simplest weather app: 1 screen and 2 dialogs.
First page allowed to see history list of places with time and weather.
Dialog that allowed to add other places.
Dialog that allowed to compare current weather with the saved one.
### Test flow
Our suggestion for the flow.
Your goals:
- assemble the project,
- make a class to class code review and fix obvious places,
- while code review you can make notes like FIXME or TODO,
- After higlighte any architecture, naming each flow;
Then we can discuss and implement:
- Next we should exclude room db usage, need to create a migration code that allowed to
temporarily use shared preferences and in memory caching, until we got another solution.
But you can follow any suitable approach.
Good luck and have fun.

1
app/.gitignore vendored Normal file
View File

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

83
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,83 @@
plugins {
id("com.android.application")
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.testapp.test"
defaultConfig {
applicationId = "com.testapp.test"
minSdk = Config.SdkVersion.MIN
targetSdk = Config.SdkVersion.COMPILE
versionCode = System.getenv("BUILD_NUMBER")?.toInt() ?: Config.Version.CODE
versionName = Config.Version.NAME
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
getByName(BuildType.DEBUG) {
isDebuggable = true
isMinifyEnabled = false
}
create(BuildType.STAGING) {
isDebuggable = true
isMinifyEnabled = true
isShrinkResources = true
}
getByName(BuildType.RELEASE) {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
viewBinding = true
}
lint {
abortOnError = false
}
// kapt block удаляем при переходе на KSP
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar", "*.aar")))
// Modules
implementation(project(":style"))
implementation(project(":presentation"))
implementation(project(":data"))
implementation(project(":domain:domain"))
implementation(project(":domain:domain_impl"))
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity.ktx)
implementation(libs.core.ktx)
implementation(libs.fragment.ktx)
implementation(libs.splashscreen)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
debugImplementation(libs.leakcanary)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}

21
app/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.
#
# 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,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="com.example.testproject.application.WeatherApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestProject"
tools:targetApi="31">
</application>
</manifest>

View File

@@ -0,0 +1,8 @@
package com.example.testproject.application
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class WeatherApp : Application() {
}

View File

@@ -0,0 +1,32 @@
package com.example.testproject.di.modules
import com.testapp.data.api.RemoteWeatherDataSourceImpl
import com.testapp.data.api.RestClient
import com.testapp.data.api.RestClientImpl
import com.testapp.data.datasource.LocalWeatherStorage
import com.testapp.data.datasource.RemoteWeatherDataSource
import com.testapp.data.db.LocalWeatherStorageImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface DataSourceModule {
@Singleton
@Binds
fun restClient(impl: RestClientImpl): RestClient
@Singleton
@Binds
fun localWeatherStorage(impl: LocalWeatherStorageImpl): LocalWeatherStorage
@Singleton
@Binds
fun weatherApi(impl: RemoteWeatherDataSourceImpl): RemoteWeatherDataSource
}

View File

@@ -0,0 +1,20 @@
package com.example.testproject.di.modules
import com.testapp.data.gatway.WeatherGateWayImpl
import com.testapp.domain.domain.gatway.WeatherGateWay
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface GateWayModule {
@Singleton
@Binds
fun bindGateWay(impl: WeatherGateWayImpl): WeatherGateWay
}

View File

@@ -0,0 +1,37 @@
package com.example.testproject.di.modules
import com.testapp.domain.domain.usecase.GetWeather
import com.testapp.domain.domain.usecase.GetWeatherList
import com.testapp.domain.domain.usecase.RequestNewWeather
import com.testapp.domain.usecase.RequestNewWeatherImpl
import com.testapp.domain.domain.usecase.SaveWeather
import com.testapp.domain.usecase.GetWeatherImpl
import com.testapp.domain.usecase.GetWeatherListImpl
import com.testapp.domain.usecase.SaveWeatherImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface UseCaseWeather {
@Binds
@Singleton
fun getWeather(impl: GetWeatherImpl): GetWeather
@Binds
@Singleton
fun requestNewWeather(impl: RequestNewWeatherImpl): RequestNewWeather
@Binds
@Singleton
fun getWeatherList(impl: GetWeatherListImpl): GetWeatherList
@Binds
@Singleton
fun saveWeatherImpl(impl: SaveWeatherImpl): SaveWeather
}

8
build.gradle.kts Normal file
View File

@@ -0,0 +1,8 @@
plugins {
id("com.android.application") version libs.versions.agp.get() apply false
id("com.android.library") version libs.versions.agp.get() apply false
id("org.jetbrains.kotlin.android") version libs.versions.kotlin.get() apply false
id("com.google.dagger.hilt.android") version libs.versions.hilt.get() apply false
id("com.google.devtools.ksp") version libs.versions.ksp.get() apply false
}

11
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,11 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(21)
}

View File

@@ -0,0 +1,5 @@
object BuildType {
const val DEBUG = "debug"
const val STAGING = "staging"
const val RELEASE = "release"
}

View File

@@ -0,0 +1,19 @@
object Config {
object SdkVersion {
const val COMPILE = 36
const val MIN = 23
}
object Version {
const val CODE = 1
private const val NAME_MAJOR = 1
private const val NAME_MINOR = 0
private const val NAME_PATCH = 1
const val NAME = "$NAME_MAJOR.$NAME_MINOR.$NAME_PATCH"
}
object Api {
const val VERSION = "1.0"
}
}

View File

@@ -0,0 +1,5 @@
object ProductFlavor {
const val DEV = "dev"
const val STAGE = "stage"
const val PROD = "prod"
}

View File

@@ -0,0 +1,23 @@
package extensions
import org.gradle.api.Project
import java.io.FileInputStream
import java.util.*
fun Project.getSigningConfigValue(key: String): String {
val signingPropsFile = file("../signing.properties")
val signingProps = Properties().apply { load(FileInputStream(signingPropsFile)) }
return signingProps[key] as String
}
fun Project.getBuildConfigFieldValue(key: String): String {
val keyPropsFile = file("../key.properties")
val keyProps = Properties().apply { load(FileInputStream(keyPropsFile)) }
return "\"${keyProps[key]}\""
}
fun Project.getManifestPlaceholderValue(key: String): String {
val keyPropsFile = file("../key.properties")
val keyProps = Properties().apply { load(FileInputStream(keyPropsFile)) }
return keyProps[key].toString()
}

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

1
domain/domain/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,37 @@
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(project(":domain:domain_impl"))
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}

21
domain/domain/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,14 @@
package com.testapp.domain.domain.gatway
import com.testapp.domain.domain.models.ActionResult
import com.testapp.domain.domain.models.Weather
import kotlinx.coroutines.flow.Flow
interface WeatherGateWay {
suspend fun getWeather(id: String): Weather
suspend fun saveWeather(weather:Weather )
suspend fun subscribeForWeatherList(): Flow<List<Weather>>
suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather>
}

View File

@@ -0,0 +1,8 @@
package com.testapp.domain.domain.models
sealed class ActionResult<out Data> {
class Success<Data>(val data: Data) : ActionResult<Data>()
class Error(val reason: Throwable? = null) : ActionResult<Nothing>()
}

View File

@@ -0,0 +1,29 @@
package com.testapp.domain.domain.models
import java.util.Date
data class Weather(
val id: String,
val location: String,
val date: Date,
val temperature: Double
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Weather
if (id != other.id) return false
if (location != other.location) return false
if (date != other.date) return false
if (temperature != other.temperature) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}

View File

@@ -0,0 +1,9 @@
package com.testapp.domain.domain.usecase
import com.testapp.domain.domain.models.Weather
interface GetWeather {
suspend fun getWeather(id:String):Weather
}

View File

@@ -0,0 +1,11 @@
package com.testapp.domain.domain.usecase
import com.testapp.domain.domain.models.Weather
import kotlinx.coroutines.flow.Flow
interface GetWeatherList {
suspend fun subscribeForWeatherList(): Flow<List<Weather>>
}

View File

@@ -0,0 +1,10 @@
package com.testapp.domain.domain.usecase
import com.testapp.domain.domain.models.ActionResult
import com.testapp.domain.domain.models.Weather
interface RequestNewWeather {
suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather>
}

View File

@@ -0,0 +1,10 @@
package com.testapp.domain.domain.usecase
import com.testapp.domain.domain.models.ActionResult
import com.testapp.domain.domain.models.Weather
interface SaveWeather {
suspend fun saveWeather(weather: Weather)
}

1
domain/domain_impl/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,37 @@
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.hilt.android)
ksp(libs.hilt.compiler)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}

View File

@@ -0,0 +1,16 @@
package com.testapp.domain.usecase
import com.testapp.domain.domain.gatway.WeatherGateWay
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.domain.usecase.GetWeather
import javax.inject.Inject
class GetWeatherImpl @Inject constructor(
private val gateWay: WeatherGateWay,
) : GetWeather {
override suspend fun getWeather(id: String): Weather =
gateWay.getWeather(id)
}

View File

@@ -0,0 +1,16 @@
package com.testapp.domain.usecase
import com.testapp.domain.domain.gatway.WeatherGateWay
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.domain.usecase.GetWeatherList
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetWeatherListImpl @Inject constructor(
private val gateWay: WeatherGateWay,
) : GetWeatherList {
override suspend fun subscribeForWeatherList(): Flow<List<Weather>> =
gateWay.subscribeForWeatherList()
}

View File

@@ -0,0 +1,17 @@
package com.testapp.domain.usecase
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.RequestNewWeather
import javax.inject.Inject
class RequestNewWeatherImpl @Inject constructor(
private val gateWay: WeatherGateWay,
) : RequestNewWeather {
override suspend fun requestNewWeatherForPlace(placeName: String): ActionResult<Weather> =
gateWay.requestNewWeatherForPlace(placeName)
}

View File

@@ -0,0 +1,17 @@
package com.testapp.domain.usecase
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.SaveWeather
import javax.inject.Inject
class SaveWeatherImpl @Inject constructor(
val gateWay: WeatherGateWay
) : SaveWeather {
override suspend fun saveWeather(weather: Weather) {
gateWay.saveWeather(weather)
}
}

33
gradle.properties Normal file
View File

@@ -0,0 +1,33 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

112
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,112 @@
[versions]
agp = "9.0.1"
kotlin = "2.3.10"
hilt = "2.59.2"
ksp = "2.3.6"
material = "1.13.0"
androidxActivityKtx = "1.12.4"
androidxFragmentKtx = "1.8.9"
coreKtx = "1.17.0"
splashscreen = "1.2.0"
appcompat = "1.7.1"
constraintlayout = "2.2.1"
recyclerview = "1.4.0"
swiperefreshlayout = "1.2.0"
cardview = "1.0.0"
viewpager2 = "1.1.0"
asyncLayoutInflater = "1.1.0"
photoView = "2.3.0"
facebookShimmer = "0.5.0"
gson = "2.13.2"
leakcanary = "2.14"
timber = "5.0.1"
junit4 = "4.13.2"
androidxCoreTesting = "2.2.0"
intercom = "17.4.5"
pinwheel = "3.6.0"
room = "2.8.4"
coroutines = "1.10.2"
lifecycle = "2.10.0"
retrofit = "3.0.0"
okhttp = "5.3.2"
appspector = "1.4.4"
glide = "5.0.5"
mockitoInline = "5.2.0"
mockitoKotlin = "6.2.3"
desugarJdkLibs = "2.1.5"
[libraries]
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
material = { module = "com.google.android.material:material", version.ref = "material" }
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidxActivityKtx" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragmentKtx" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" }
viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
async-layoutinflater = { module = "androidx.asynclayoutinflater:asynclayoutinflater", version.ref = "asyncLayoutInflater" }
photo-view = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoView" }
facebook-shimmer = { module = "com.facebook.shimmer:shimmer", version.ref = "facebookShimmer" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
junit4 = { module = "junit:junit", version.ref = "junit4" }
androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "androidxCoreTesting" }
intercom = { module = "io.intercom.android:intercom-sdk", version.ref = "intercom" }
pinwheel = { module = "com.getpinwheel:pinwheel-android", version.ref = "pinwheel" }
room-runtime = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt" }
hilt-navigation-fragment = { module = "androidx.hilt:hilt-navigation-fragment", version.ref = "hilt" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
appspector-sdk = { module = "com.appspector:android-sdk", version.ref = "appspector" }
appspector-sdk-noop = { module = "com.appspector:android-sdk-noop", version.ref = "appspector" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoInline" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Executable file

Binary file not shown.

6
gradle/wrapper/gradle-wrapper.properties vendored Executable file
View File

@@ -0,0 +1,6 @@
#Thu Oct 16 00:26:05 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
presentation/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,48 @@
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
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))
// Modules
implementation(project(":style"))
implementation(project(":domain:domain"))
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.process)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}

21
presentation/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,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.testapp.domain.presentation.screen.MainActivity"
android:exported="true"
android:noHistory="true"
android:theme="@style/Theme.TestProject">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,41 @@
package com.testapp.domain.presentation.models
import android.view.View
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import androidx.viewbinding.ViewBindings
sealed class AdapterItem {
data class DataItem<DATA>(val data: DATA) : AdapterItem()
object EmptyScreen : AdapterItem()
object Skeleton : AdapterItem()
}
class AdapterItemDiff : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
oldItem is AdapterItem.Skeleton && newItem is AdapterItem.Skeleton -> true
oldItem is AdapterItem.EmptyScreen && newItem is AdapterItem.EmptyScreen -> true
oldItem is AdapterItem.DataItem<*> && newItem is AdapterItem.DataItem<*> -> {
oldItem.data?.hashCode() == newItem.data.hashCode()
}
else -> false
}
}
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem): Boolean {
return oldItem.equals(newItem)
}
}
abstract class AbsVH<VIEW : ViewBinding, DATA>(val binding: VIEW) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: DATA)
}

View File

@@ -0,0 +1,30 @@
package com.testapp.domain.presentation.screen
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.testapp.domain.presentation.screen.history.HistoryFragment
import com.testappbank.test.R
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var mainVM: MainVm
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) initDefaultScreen()
}
private fun initDefaultScreen() {
supportFragmentManager.beginTransaction().apply {
add(R.id.content, HistoryFragment.newInstance())
commitAllowingStateLoss()
}
}
}

View File

@@ -0,0 +1,9 @@
package com.testapp.domain.presentation.screen
import androidx.lifecycle.ViewModel
import javax.inject.Inject
class MainVm @Inject constructor() : ViewModel() {
}

View File

@@ -0,0 +1,94 @@
package com.testapp.domain.presentation.screen.compare
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import com.testapp.ViewBindingDialogFragment
import com.testapp.domain.domain.models.Weather
import com.testappbank.test.databinding.DialogFramentCompareHistoryBinding
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
class CompareWeatherDialogFragment :
ViewBindingDialogFragment<DialogFramentCompareHistoryBinding>() {
@Inject
lateinit var factory: DetailWeatherVM.Factory
private val dateFormater = SimpleDateFormat("yyyy-MM-dd hh:mm", Locale.getDefault())
private val detailWeatherVM: DetailWeatherVM by viewModels {
DetailWeatherVM.providesFactory(
assistedFactory = factory,
userId = arguments?.getString(WEATHER_ID) ?: throw RuntimeException("no id")
)
}
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> DialogFramentCompareHistoryBinding
get() = DialogFramentCompareHistoryBinding::inflate
override fun viewSetup() {
binding.apply {
detailWeatherVM.savedWeatherReady.observe(viewLifecycleOwner) { isReady ->
oldWeather.root.visibility = if (isReady) View.VISIBLE else View.GONE
oldWeatherSkeleton.root.visibility = if (isReady) View.GONE else View.VISIBLE
}
detailWeatherVM.newWeatherReady.observe(viewLifecycleOwner) { isReady ->
newWeather.root.visibility = if (isReady) View.VISIBLE else View.GONE
newWeatherSkeleton.root.visibility = if (isReady) View.GONE else View.VISIBLE
}
detailWeatherVM.savedWeather.observe(viewLifecycleOwner) { weather ->
oldWeather.tempratureAndLocation.text = requireContext().getString(
com.testapp.test.R.string.weather_and_place,
weather?.temperature.toString() ?: "",
weather?.location ?: "",
)
weather?.date?.let { oldWeather.txtData.text = dateFormater.format(it) }
}
detailWeatherVM.newWeather.observe(viewLifecycleOwner) { weather ->
newWeather.tempratureAndLocation.text = requireContext().getString(
com.testapp.test.R.string.weather_and_place,
weather?.temperature.toString() ?: "",
weather?.location ?: "",
)
weather?.date?.let { newWeather.txtData.text = dateFormater.format(it) }
}
detailWeatherVM.result.observe(viewLifecycleOwner) { distance ->
tempratureConclusion.text = when {
distance < 0 -> requireContext().getString(
com.testapp.test.R.string.weather_colder,
distance.toString()
)
distance > 0 -> requireContext().getString(
com.testapp.test.R.string.weather_warmer,
distance.toString()
)
else -> requireContext().getString(com.testapp.test.R.string.weather_same)
}
}
}
}
companion object {
const val WEATHER_ID = "weather"
fun newInstance(weather: Weather) =
CompareWeatherDialogFragment().apply {
arguments = bundleOf(
weather.id to WEATHER_ID
)
}
}
}

View File

@@ -0,0 +1,72 @@
package com.testapp.domain.presentation.screen.compare
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.testapp.domain.domain.models.ActionResult
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.domain.usecase.GetWeather
import com.testapp.domain.domain.usecase.RequestNewWeather
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
class DetailWeatherVM @AssistedInject constructor(
val getWeather: GetWeather,
val requestNewWeather: RequestNewWeather,
@Assisted val savedWeatherId: String
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(savedWeatherId: String): DetailWeatherVM
}
@Suppress("UNCHECKED_CAST")
companion object {
fun providesFactory(
assistedFactory: Factory,
userId: String
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(userId) as T
}
}
}
val savedWeather = MutableLiveData<Weather?>(null)
val savedWeatherReady = savedWeather.map { it != null }
val newWeather = MutableLiveData<Weather?>(null)
val newWeatherReady = savedWeather.map { it != null }
val result = MutableLiveData<Double>()
init {
requestWeather()
}
private fun requestWeather() {
viewModelScope.launch {
val weather = getWeather.getWeather(savedWeatherId)
savedWeather.postValue(weather)
val updatedWeather = requestNewWeather.requestNewWeatherForPlace(weather.location)
if (updatedWeather is ActionResult.Success<Weather>) {
newWeather.postValue(updatedWeather.data as Weather?)
val distance =
((weather.temperature - updatedWeather.data.temperature) * 100).roundToInt() / 100.0
result.postValue(distance)
}
}
}
}

View File

@@ -0,0 +1,98 @@
package com.testapp.domain.presentation.screen.history
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.presentation.models.AbsVH
import com.testapp.domain.presentation.models.AdapterItem
import com.testapp.domain.presentation.models.AdapterItemDiff
import com.testappbank.test.databinding.ItemEmptyListBinding
import com.testappbank.test.databinding.ItemSkeletonBinding
import com.testappbank.test.databinding.ItemWeatherBinding
import java.text.SimpleDateFormat
import java.util.Locale
class HistoryAdapter(
private val onWeatherSelected: (Weather) -> Unit
) : RecyclerView.Adapter<AbsVH<*, *>>() {
private val listDiffer = AsyncListDiffer(this, AdapterItemDiff())
private val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm", Locale.getDefault())
override fun getItemViewType(position: Int): Int =
when (listDiffer.currentList[position]) {
is AdapterItem.Skeleton -> SKELETON
is AdapterItem.EmptyScreen -> EMPTY
is AdapterItem.DataItem<*> -> ITEM
else -> throw RuntimeException("Unknown item type!")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbsVH<*, *> {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
SKELETON -> SkeletonVH(ItemSkeletonBinding.inflate(inflater, parent, false))
EMPTY -> EmptyVH(ItemEmptyListBinding.inflate(inflater, parent, false))
ITEM -> WeatherView(ItemWeatherBinding.inflate(inflater, parent, false))
else -> throw RuntimeException("Unknown item type!")
}
}
override fun onBindViewHolder(holder: AbsVH<*, *>, position: Int) {
val item = listDiffer.currentList[position]
when (holder) {
is SkeletonVH -> holder.bind(Unit)
is EmptyVH -> holder.bind(Unit)
is WeatherView -> ((item as? AdapterItem.DataItem<*>)?.data as? Weather)?.let {
holder.bind(it)
}
}
}
override fun getItemCount(): Int = listDiffer.currentList.size
fun updateItems(list: List<AdapterItem>) {
if (list.isEmpty()) {
listDiffer.submitList(listOf(AdapterItem.EmptyScreen))
} else listDiffer.submitList(list)
}
inner class SkeletonVH(binding: ItemSkeletonBinding) :
AbsVH<ItemSkeletonBinding, Unit>(binding) {
override fun bind(item: Unit) {}
}
inner class EmptyVH(binding: ItemEmptyListBinding) :
AbsVH<ItemEmptyListBinding, Unit>(binding) {
override fun bind(item: Unit) {}
}
inner class WeatherView(binding: ItemWeatherBinding) :
AbsVH<ItemWeatherBinding, Weather>(binding) {
override fun bind(item: Weather) {
binding.apply {
val context = binding.root.context
tempratureAndLocation.text = context.getString(
com.testapp.test.R.string.weather_and_place,
item.temperature.toString(),
item.location
)
txtData.text = dateFormat.format(item.date)
root.setOnClickListener {
onWeatherSelected.invoke(item)
}
}
}
}
companion object {
private const val SKELETON = 1
private const val ITEM = 2
private const val EMPTY = 3
}
}

View File

@@ -0,0 +1,65 @@
package com.testapp.domain.presentation.screen.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.testapp.ViewBindingFragment
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.presentation.screen.compare.CompareWeatherDialogFragment
import com.testapp.domain.presentation.screen.request.RequestNewLocationDialog
import com.testappbank.test.databinding.FramentHistoryBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HistoryFragment : ViewBindingFragment<FramentHistoryBinding>() {
private val historyVM: HistoryVM by viewModels()
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FramentHistoryBinding =
FramentHistoryBinding::inflate
private val adapter by lazy { HistoryAdapter(this::onWeatherSelected) }
private val itemDecoration by lazy {
DividerItemDecoration(requireContext(), LinearLayout.HORIZONTAL)
}
override fun viewSetup() {
binding.apply {
setupList(weatherList)
requestNewWeather.setOnClickListener { historyVM.navigateToNewWeather() }
}
historyVM.displayRequestNew.observe(viewLifecycleOwner) {
RequestNewLocationDialog.newInstance().show(childFragmentManager, null)
}
}
private fun setupList(weatherList: RecyclerView) {
val layoutManager = LinearLayoutManager(context)
weatherList.layoutManager = layoutManager
weatherList.adapter = adapter
weatherList.addItemDecoration(itemDecoration)
historyVM.weatherList.observe(viewLifecycleOwner) { list ->
adapter.updateItems(list)
}
}
private fun onWeatherSelected(weather: Weather) {
CompareWeatherDialogFragment.newInstance(weather).show(childFragmentManager, null)
}
companion object {
fun newInstance() = HistoryFragment().apply {
arguments = Bundle()
}
}
}

View File

@@ -0,0 +1,59 @@
package com.testapp.domain.presentation.screen.history
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.testapp.domain.domain.models.Weather
import com.testapp.domain.domain.usecase.GetWeatherList
import com.testapp.domain.presentation.models.AdapterItem
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HistoryVM @Inject constructor(
private val getWeatherList: GetWeatherList,
) : ViewModel() {
fun navigateToNewWeather() {
displayRequestNew.postValue(Unit)
}
val weatherList = MutableLiveData<List<AdapterItem>>()
val diaplyError = MutableLiveData<Throwable?>()
val displayRequestNew = MutableLiveData<Unit>()
init {
GlobalScope.launch {
getWeatherList.subscribeForWeatherList()
.onStart {
weatherList.postValue(
listOf(
AdapterItem.Skeleton, AdapterItem.Skeleton, AdapterItem.Skeleton,
AdapterItem.Skeleton, AdapterItem.Skeleton, AdapterItem.Skeleton,
AdapterItem.Skeleton, AdapterItem.Skeleton, AdapterItem.Skeleton,
)
)
}
.onEach { list ->
weatherList.postValue(list.map { it.toAdapterItem() })
diaplyError.postValue(null)
}
.catch { cause -> diaplyError.postValue(cause) }
.launchIn(viewModelScope)
}
}
}
private fun Weather.toAdapterItem(): AdapterItem.DataItem<Weather> {
return AdapterItem.DataItem(this)
}

View File

@@ -0,0 +1,48 @@
package com.testapp.domain.presentation.screen.request
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.viewModels
import com.testapp.ViewBindingDialogFragment
import com.testappbank.test.databinding.DialogFramentRequestBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RequestNewLocationDialog :
ViewBindingDialogFragment<DialogFramentRequestBinding>() {
val requestNewWeatherVM: RequestNewWeatherVM by viewModels()
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> DialogFramentRequestBinding
get() = DialogFramentRequestBinding::inflate
override fun viewSetup() {
binding.locationField.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
requestForNewLocation()
true
}else false;
}
binding.search.setOnClickListener { requestForNewLocation() }
requestNewWeatherVM.closeDialog.observe(viewLifecycleOwner) {
if (it) dismiss()
}
requestNewWeatherVM.error.observe(viewLifecycleOwner) {
binding.locationLayout.error = it
}
}
private fun requestForNewLocation() {
binding.locationField.text?.toString()?.let { name ->
requestNewWeatherVM.requestWeather(name)
}
}
companion object {
fun newInstance(): RequestNewLocationDialog {
return RequestNewLocationDialog()
}
}
}

View File

@@ -0,0 +1,38 @@
package com.testapp.domain.presentation.screen.request
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.testapp.domain.domain.models.ActionResult
import com.testapp.domain.domain.usecase.RequestNewWeather
import com.testapp.domain.domain.usecase.SaveWeather
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RequestNewWeatherVM @Inject constructor(
private val requestNewWeather: RequestNewWeather,
private val saveWeather: SaveWeather,
) : ViewModel() {
val closeDialog = MutableLiveData(false)
val error = MutableLiveData<String?>()
fun requestWeather(locationName: String) {
viewModelScope.launch {
error.postValue(null)
when (val result = requestNewWeather.requestNewWeatherForPlace(locationName)) {
is ActionResult.Error -> {
error.postValue(result.reason?.message)
}
is ActionResult.Success -> {
saveWeather.saveWeather(result.data)
closeDialog.postValue(true)
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
package com.testapp.domain.presentation.tools
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
* https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes!!
*/
open class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending =
AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp">
<include
android:id="@+id/oldWeatherSkeleton"
layout="@layout/item_skeleton"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include
android:id="@+id/oldWeather"
layout="@layout/item_weather"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/topBarier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="oldWeatherSkeleton,oldWeather" />
<ImageView
android:id="@+id/seporator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_compare_45"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/topBarier" />
<include
android:id="@+id/newWeatherSkeleton"
layout="@layout/item_skeleton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/seporator" />
<include
android:id="@+id/newWeather"
layout="@layout/item_weather"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seporator" />
<TextView
android:id="@+id/temprature_conclusion"
style="@style/TextAppearance.AppCompat.Display1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="5 degres wormar"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/newWeather"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="320dp"
android:padding="12dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/location_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/new_location"
app:hintEnabled="true"
app:endIconMode="none"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/location_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="textAutoComplete"
tools:text="London" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="160dp"
android:text="@string/search"
app:layout_constraintTop_toBottomOf="@+id/location_layout"
app:layout_constraintEnd_toEndOf="@+id/location_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/empty_eror_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBaseline_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/weatherList"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/requestNewWeather"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/add_24"
app:elevation="6dp"
android:importantForAccessibility="no" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/shimmer_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textEmpty"
style="@style/TextAppearance.AppCompat.Display1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/no_saved_weather"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtWeatherLocationName"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:alpha="0.7"
android:gravity="center"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/no_saved_weather_instruction"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textEmpty"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<com.facebook.shimmer.ShimmerFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/shimmer_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:shimmer_auto_start="true"
app:duration="800">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tempratureAndLocation"
style="@style/TextAppearance.AppCompat.Display1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gnt_shadow"
/>
<TextView
android:id="@+id/txtWeatherLocationName"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.7"
android:layout_marginTop="8dp"
android:background="@color/gnt_shadow" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tempratureAndLocation"
style="@style/TextAppearance.AppCompat.Display1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="34 * in london" />
<TextView
android:id="@+id/txtData"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.7"
android:layout_marginTop="8dp"
tools:text="25/05/2025" />
</LinearLayout>

36
settings.gradle.kts Normal file
View File

@@ -0,0 +1,36 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "TestProgect"
include(
":app",
":testUtils",
":style",
":presentation",
":data",
":domain:domain",
":domain:domain_impl",
// features:
// TODO
// auth
// "feature:auth:contract",
// "feature:auth:contract_ui",
// "feature:auth:data",
// "feature:auth:domain",
// "feature:auth:presentation",
//
//
)

1
style/.gitignore vendored Normal file
View File

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

52
style/build.gradle.kts Normal file
View File

@@ -0,0 +1,52 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = Config.SdkVersion.COMPILE
namespace = "com.testapp.test"
defaultConfig {
minSdk = Config.SdkVersion.MIN
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar", "*.aar")))
// UI
api(libs.core.ktx)
api(libs.appcompat)
api(libs.activity.ktx)
api(libs.fragment.ktx)
api(libs.constraintlayout)
api(libs.recyclerview)
api(libs.swiperefreshlayout)
api(libs.cardview)
api(libs.viewpager2)
api(libs.async.layoutinflater)
api(libs.material)
api(libs.facebook.shimmer)
debugApi(libs.leakcanary)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}

21
style/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.
#
# 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,41 @@
package com.testapp
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
abstract class ViewBindingDialogFragment<VB : ViewBinding> : AppCompatDialogFragment() {
private var _binding: ViewBinding? = null
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
@Suppress("UNCHECKED_CAST")
protected val binding: VB
get() = _binding as VB
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater.invoke(inflater, container, false)
return requireNotNull(_binding).root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewSetup()
}
abstract fun viewSetup()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,39 @@
package com.testapp
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
private var _binding: ViewBinding? = null
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
@Suppress("UNCHECKED_CAST")
protected val binding: VB
get() = _binding as VB
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater.invoke(inflater, container, false)
return requireNotNull(_binding).root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewSetup()
}
abstract fun viewSetup()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="45dp"
android:height="45dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,19h3v3h2v-3h3l-4,-4 -4,4zM16,4h-3L13,1h-2v3L8,4l4,4 4,-4zM4,9v2h16L20,9L4,9z" />
<path
android:fillColor="@android:color/white"
android:pathData="M4,12h16v2H4z" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="main_background">#2C2A2A</color>
</resources>

View File

@@ -0,0 +1,16 @@
<resources >
<!-- Base application theme. -->
<style name="Theme.TestProject" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

Some files were not shown because too many files have changed in this diff Show More