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