init
This commit is contained in:
1
presentation/.gitignore
vendored
Normal file
1
presentation/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
48
presentation/build.gradle.kts
Normal file
48
presentation/build.gradle.kts
Normal 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
21
presentation/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
20
presentation/src/main/AndroidManifest.xml
Normal file
20
presentation/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.testapp.domain.presentation.screen
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainVm @Inject constructor() : ViewModel() {
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
12
presentation/src/main/res/layout/activity_main.xml
Normal file
12
presentation/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
@@ -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>
|
||||
42
presentation/src/main/res/layout/dialog_frament_request.xml
Normal file
42
presentation/src/main/res/layout/dialog_frament_request.xml
Normal 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>
|
||||
42
presentation/src/main/res/layout/frament_history.xml
Normal file
42
presentation/src/main/res/layout/frament_history.xml
Normal 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>
|
||||
38
presentation/src/main/res/layout/item_empty_list.xml
Normal file
38
presentation/src/main/res/layout/item_empty_list.xml
Normal 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>
|
||||
35
presentation/src/main/res/layout/item_skeleton.xml
Normal file
35
presentation/src/main/res/layout/item_skeleton.xml
Normal 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>
|
||||
26
presentation/src/main/res/layout/item_weather.xml
Normal file
26
presentation/src/main/res/layout/item_weather.xml
Normal 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>
|
||||
Reference in New Issue
Block a user