mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2026-03-18 15:36:04 +00:00
- mid commit
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package com.appttude.h_mal.atlas_weather.base
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.inflate
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModelLazy
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||
import com.appttude.h_mal.atlas_weather.utils.hide
|
||||
import com.appttude.h_mal.atlas_weather.utils.show
|
||||
import com.appttude.h_mal.atlas_weather.utils.triggerAnimation
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.BaseViewModel
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), KodeinAware {
|
||||
|
||||
private lateinit var loadingView: View
|
||||
|
||||
override val kodein by kodein()
|
||||
|
||||
|
||||
/**
|
||||
* Creates a loading view which to be shown during async operations
|
||||
*
|
||||
* #setOnClickListener(null) is an ugly work around to prevent under being clicked during
|
||||
* loading
|
||||
*/
|
||||
private fun instantiateLoadingView() {
|
||||
loadingView = inflate(this, R.layout.progress_layout, null)
|
||||
loadingView.setOnClickListener(null)
|
||||
addContentView(loadingView, LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
||||
loadingView.hide()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
instantiateLoadingView()
|
||||
}
|
||||
|
||||
fun <A : AppCompatActivity> startActivity(activity: Class<A>) {
|
||||
val intent = Intent(this, activity)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of success or some data emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onStarted() {
|
||||
loadingView.fadeIn()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of success or some data emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onSuccess(data: Any?) {
|
||||
loadingView.fadeOut()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of failure or some error emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onFailure(error: Any?) {
|
||||
if (error is String) displayToast(error)
|
||||
loadingView.fadeOut()
|
||||
}
|
||||
|
||||
private fun View.fadeIn() = apply {
|
||||
show()
|
||||
triggerAnimation(R.anim.nav_default_enter_anim) {}
|
||||
}
|
||||
|
||||
private fun View.fadeOut() = apply {
|
||||
hide()
|
||||
triggerAnimation(R.anim.nav_default_exit_anim) {}
|
||||
}
|
||||
|
||||
|
||||
override fun onBackPressed() {
|
||||
loadingView.hide()
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
@@ -24,33 +24,32 @@ class RepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
|
||||
db.getSimpleDao().upsertFullWeather(entityItem)
|
||||
db.getWeatherDao().upsertFullWeather(entityItem)
|
||||
}
|
||||
|
||||
override suspend fun saveWeatherListToRoom(
|
||||
list: List<EntityItem>
|
||||
) {
|
||||
db.getSimpleDao().upsertListOfFullWeather(list)
|
||||
db.getWeatherDao().upsertListOfFullWeather(list)
|
||||
}
|
||||
|
||||
override fun loadRoomWeatherLiveData() = db.getSimpleDao().getAllFullWeatherWithoutCurrent()
|
||||
override fun loadRoomWeatherLiveData() = db.getWeatherDao().getAllFullWeatherWithoutCurrent()
|
||||
|
||||
override suspend fun loadWeatherList(): List<String> {
|
||||
return db.getSimpleDao()
|
||||
return db.getWeatherDao()
|
||||
.getWeatherListWithoutCurrent()
|
||||
.map { it.id }
|
||||
}
|
||||
|
||||
override fun loadCurrentWeatherFromRoom(id: String) =
|
||||
db.getSimpleDao().getCurrentFullWeather(id)
|
||||
db.getWeatherDao().getCurrentFullWeather(id)
|
||||
|
||||
override suspend fun loadSingleCurrentWeatherFromRoom(id: String) =
|
||||
db.getSimpleDao().getCurrentFullWeatherSingle(id)
|
||||
db.getWeatherDao().getCurrentFullWeatherSingle(id)
|
||||
|
||||
override fun isSearchValid(locationName: String): Boolean {
|
||||
val lastSaved = prefs
|
||||
.getLastSavedAt("$LOCATION_CONST$locationName")
|
||||
?: return true
|
||||
val difference = System.currentTimeMillis() - lastSaved
|
||||
|
||||
return difference > FALLBACK_TIME
|
||||
@@ -62,7 +61,7 @@ class RepositoryImpl(
|
||||
|
||||
override suspend fun deleteSavedWeatherEntry(locationName: String): Boolean {
|
||||
prefs.deleteLocation(locationName)
|
||||
return db.getSimpleDao().deleteEntry(locationName) > 0
|
||||
return db.getWeatherDao().deleteEntry(locationName) > 0
|
||||
}
|
||||
|
||||
override fun getSavedLocations(): List<String> {
|
||||
@@ -70,7 +69,7 @@ class RepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
||||
return db.getSimpleDao().getCurrentFullWeatherSingle(locationName)
|
||||
return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
@TypeConverters(Converter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun getSimpleDao(): WeatherDao
|
||||
abstract fun getWeatherDao(): WeatherDao
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ object GenericsHelper {
|
||||
?.kotlin
|
||||
?: throw IllegalStateException("Can not find class from generic argument")
|
||||
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
// fun <CLASS : Any> Any.getGenericClassInMethod(position: Int): KClass<CLASS> =
|
||||
// ((javaClass.methods as? ParameterizedType)
|
||||
// ?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
|
||||
// ?.kotlin
|
||||
// ?: throw IllegalStateException("Can not find class from generic argument")
|
||||
|
||||
// /**
|
||||
// * Create a view binding out of the the generic [VB]
|
||||
// *
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.model
|
||||
|
||||
sealed class ViewState {
|
||||
object HasStarted : ViewState()
|
||||
class HasData<T : Any>(val data: T) : ViewState()
|
||||
class HasError<T : Any>(val error: T) : ViewState()
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.appttude.h_mal.monoWeather.ui
|
||||
package com.appttude.h_mal.atlas_weather.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
@@ -9,16 +7,17 @@ import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.fragment.app.createViewModelLazy
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
|
||||
import com.appttude.h_mal.atlas_weather.base.BaseActivity
|
||||
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import com.appttude.h_mal.atlas_weather.utils.Event
|
||||
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||
import com.appttude.h_mal.atlas_weather.utils.hide
|
||||
import com.appttude.h_mal.atlas_weather.utils.show
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.BaseViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -28,13 +27,18 @@ import org.kodein.di.generic.instance
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("EmptyMethod", "EmptyMethod")
|
||||
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId),
|
||||
abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int) :
|
||||
Fragment(contentLayoutId),
|
||||
KodeinAware {
|
||||
|
||||
override val kodein by kodein()
|
||||
val factory by instance<ApplicationViewModelFactory>()
|
||||
|
||||
inline fun <reified VM : ViewModel> getFragmentViewModel(): Lazy<VM> = viewModels { factory }
|
||||
val viewModel: V by getFragmentViewModel()
|
||||
|
||||
var mActivity: BaseActivity? = null
|
||||
private fun getFragmentViewModel(): Lazy<V> =
|
||||
createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory })
|
||||
|
||||
private var shortAnimationDuration by Delegates.notNull<Int>()
|
||||
|
||||
@@ -43,25 +47,42 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
|
||||
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||
}
|
||||
|
||||
// toggle visibility of progress spinner while async operations are taking place
|
||||
fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> {
|
||||
it.getContentIfNotHandled()?.let { i ->
|
||||
if (i)
|
||||
progressBar.fadeIn()
|
||||
else
|
||||
progressBar.fadeOut()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mActivity = activity as BaseActivity
|
||||
configureObserver()
|
||||
}
|
||||
|
||||
private fun configureObserver() {
|
||||
viewModel.uiState.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
is ViewState.HasStarted -> onStarted()
|
||||
is ViewState.HasData<*> -> onSuccess(it.data)
|
||||
is ViewState.HasError<*> -> onFailure(it.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// display a toast when operation fails
|
||||
fun errorObserver() = Observer<Event<String>> {
|
||||
it.getContentIfNotHandled()?.let { message ->
|
||||
displayToast(message)
|
||||
}
|
||||
/**
|
||||
* Called in case of starting operation liveData in viewModel
|
||||
*/
|
||||
open fun onStarted() {
|
||||
mActivity?.onStarted()
|
||||
}
|
||||
|
||||
fun refreshObserver(refresher: SwipeRefreshLayout) = Observer<Event<Boolean>> {
|
||||
refresher.isRefreshing = false
|
||||
/**
|
||||
* Called in case of success or some data emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onSuccess(data: Any?) {
|
||||
mActivity?.onSuccess(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of failure or some error emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onFailure(error: Any?) {
|
||||
mActivity?.onFailure(error)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,46 +111,6 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.fadeIn() {
|
||||
apply {
|
||||
// Set the content view to 0% opacity but visible, so that it is visible
|
||||
// (but fully transparent) during the animation.
|
||||
alpha = 0f
|
||||
hide()
|
||||
|
||||
// Animate the content view to 100% opacity, and clear any animation
|
||||
// listener set on the view.
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(shortAnimationDuration.toLong())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.fadeOut() {
|
||||
apply {
|
||||
// Set the content view to 0% opacity but visible, so that it is visible
|
||||
// (but fully transparent) during the animation.
|
||||
alpha = 1f
|
||||
show()
|
||||
|
||||
// Animate the content view to 100% opacity, and clear any animation
|
||||
// listener set on the view.
|
||||
animate()
|
||||
.alpha(0f)
|
||||
.setDuration(shortAnimationDuration.toLong())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
|
||||
@@ -9,10 +9,12 @@ import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.base.BaseActivity
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
lateinit var navHost: NavHostFragment
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AnimRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.squareup.picasso.Picasso
|
||||
@@ -42,4 +45,14 @@ fun ImageView.loadImage(url: String?) {
|
||||
fun Fragment.hideKeyboard() {
|
||||
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
|
||||
val animation = AnimationUtils.loadAnimation(context, id)
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation)
|
||||
override fun onAnimationStart(a: Animation?) {}
|
||||
override fun onAnimationRepeat(a: Animation?) {}
|
||||
})
|
||||
startAnimation(animation)
|
||||
}
|
||||
@@ -2,13 +2,11 @@ package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import android.Manifest
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.utils.Event
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -19,48 +17,40 @@ class MainViewModel(
|
||||
private val repository: Repository
|
||||
) : WeatherViewModel() {
|
||||
|
||||
val weatherLiveData = MutableLiveData<WeatherDisplay>()
|
||||
|
||||
val operationState = MutableLiveData<Event<Boolean>>()
|
||||
val operationError = MutableLiveData<Event<String>>()
|
||||
val operationRefresh = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
init {
|
||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
|
||||
it?.let {
|
||||
val weather = WeatherDisplay(it)
|
||||
weatherLiveData.postValue(weather)
|
||||
onSuccess(weather)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
fun fetchData() {
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
operationRefresh.postValue(Event(false))
|
||||
return
|
||||
}
|
||||
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
|
||||
val currentLocation =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
val fullWeather = createFullWeather(weather, currentLocation)
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
// Has the search been conducted in the last 5 minutes
|
||||
val entityItem = if (repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
|
||||
val currentLocation =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
val fullWeather = createFullWeather(weather, currentLocation)
|
||||
EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
} else {
|
||||
repository.getSingleWeather(CURRENT_LOCATION)
|
||||
}
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
onSuccess(Unit)
|
||||
} catch (e: Exception) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
} finally {
|
||||
operationState.postValue(Event(false))
|
||||
operationRefresh.postValue(Event(false))
|
||||
onError(e.message!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.utils.Event
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,15 +19,6 @@ class WorldViewModel(
|
||||
private val repository: Repository
|
||||
) : WeatherViewModel() {
|
||||
|
||||
val weatherLiveData = MutableLiveData<List<WeatherDisplay>>()
|
||||
val singleWeatherLiveData = MutableLiveData<WeatherDisplay>()
|
||||
|
||||
val operationState = MutableLiveData<Event<Boolean>>()
|
||||
val operationError = MutableLiveData<Event<String>>()
|
||||
val operationRefresh = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val operationComplete = MutableLiveData<Event<String>>()
|
||||
|
||||
private val weatherListLiveData = repository.loadRoomWeatherLiveData()
|
||||
|
||||
init {
|
||||
@@ -37,7 +26,7 @@ class WorldViewModel(
|
||||
val list = it.map { data ->
|
||||
WeatherDisplay(data)
|
||||
}
|
||||
weatherLiveData.postValue(list)
|
||||
onSuccess(list)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,36 +34,34 @@ class WorldViewModel(
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val entity = repository.getSingleWeather(locationName)
|
||||
val item = WeatherDisplay(entity)
|
||||
singleWeatherLiveData.postValue(item)
|
||||
onSuccess(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchDataForSingleLocation(locationName: String) {
|
||||
if (!repository.isSearchValid(locationName)) {
|
||||
operationRefresh.postValue(Event(false))
|
||||
return
|
||||
}
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
val weatherEntity = createWeatherEntity(locationName)
|
||||
val weatherEntity = if (repository.isSearchValid(locationName)) {
|
||||
createWeatherEntity(locationName)
|
||||
} else {
|
||||
repository.getSingleWeather(locationName)
|
||||
}
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
repository.saveLastSavedAt(locationName)
|
||||
onSuccess(Unit)
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
} finally {
|
||||
operationState.postValue(Event(false))
|
||||
operationRefresh.postValue(Event(false))
|
||||
onError(e.message!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchDataForSingleLocationSearch(locationName: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
onStart()
|
||||
// Check if location exists
|
||||
if (repository.getSavedLocations().contains(locationName)) {
|
||||
operationError.postValue(Event("$locationName already exists"))
|
||||
onError("$locationName already exists")
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -89,29 +76,26 @@ class WorldViewModel(
|
||||
LocationType.City
|
||||
)
|
||||
if (repository.getSavedLocations().contains(retrievedLocation)) {
|
||||
operationError.postValue(Event("$retrievedLocation already exists"))
|
||||
onError("$retrievedLocation already exists")
|
||||
return@launch
|
||||
}
|
||||
// Save data if not null
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
repository.saveLastSavedAt(retrievedLocation)
|
||||
operationComplete.postValue(Event("$retrievedLocation saved"))
|
||||
|
||||
onSuccess("$retrievedLocation saved")
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
} finally {
|
||||
operationState.postValue(Event(false))
|
||||
onError(e.message!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchAllLocations() {
|
||||
onStart()
|
||||
if (!repository.isSearchValid(ALL_LOADED)) {
|
||||
operationRefresh.postValue(Event(false))
|
||||
onSuccess(Unit)
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
val list = mutableListOf<EntityItem>()
|
||||
repository.loadWeatherList().forEach { locationName ->
|
||||
@@ -128,25 +112,25 @@ class WorldViewModel(
|
||||
repository.saveWeatherListToRoom(list)
|
||||
repository.saveLastSavedAt(ALL_LOADED)
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
onError(e.message!!)
|
||||
} finally {
|
||||
operationState.postValue(Event(false))
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocation(locationName: String) {
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
val success = repository.deleteSavedWeatherEntry(locationName)
|
||||
if (!success) {
|
||||
operationError.postValue(Event("Failed to delete"))
|
||||
onError("Failed to delete")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
onError(e.message!!)
|
||||
} finally {
|
||||
operationState.postValue(Event(false))
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
|
||||
open class BaseViewModel: ViewModel() {
|
||||
|
||||
private val _uiState = MutableLiveData<ViewState>()
|
||||
val uiState: LiveData<ViewState> = _uiState
|
||||
|
||||
|
||||
fun onStart() {
|
||||
_uiState.postValue(ViewState.HasStarted)
|
||||
}
|
||||
|
||||
fun <T : Any> onSuccess(result: T) {
|
||||
_uiState.postValue(ViewState.HasData(result))
|
||||
}
|
||||
|
||||
protected fun <E : Any> onError(error: E) {
|
||||
_uiState.postValue(ViewState.HasError(error))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
|
||||
abstract class WeatherViewModel : ViewModel() {
|
||||
abstract class WeatherViewModel : BaseViewModel() {
|
||||
|
||||
fun createFullWeather(
|
||||
weather: WeatherResponse,
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -7,31 +7,17 @@
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/forecast_listview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:backgroundTint="@color/colorPrimaryDark"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/db_list_item"></androidx.recyclerview.widget.RecyclerView>
|
||||
tools:listitem="@layout/db_list_item" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
13
app/src/main/res/layout/progress_layout.xml
Normal file
13
app/src/main/res/layout/progress_layout.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="0.2dp" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"cod": 401,
|
||||
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
{
|
||||
"lat": 51.51,
|
||||
"lon": -0.13,
|
||||
"timezone": "Europe/London",
|
||||
"timezone_offset": 3600,
|
||||
"current": {
|
||||
"dt": 1648932980,
|
||||
"sunrise": 1648877626,
|
||||
"sunset": 1648924449,
|
||||
"temp": 2.14,
|
||||
"feels_like": 0.13,
|
||||
"pressure": 1025,
|
||||
"humidity": 79,
|
||||
"dew_point": -0.99,
|
||||
"uvi": 0,
|
||||
"clouds": 72,
|
||||
"visibility": 10000,
|
||||
"wind_speed": 1.92,
|
||||
"wind_deg": 69,
|
||||
"wind_gust": 4.81,
|
||||
"weather": [
|
||||
{
|
||||
"id": 803,
|
||||
"main": "Clouds",
|
||||
"description": "broken clouds",
|
||||
"icon": "04n"
|
||||
}
|
||||
]
|
||||
},
|
||||
"daily": [
|
||||
{
|
||||
"dt": 1648900800,
|
||||
"sunrise": 1648877626,
|
||||
"sunset": 1648924449,
|
||||
"moonrise": 1648880220,
|
||||
"moonset": 1648930140,
|
||||
"moon_phase": 0.04,
|
||||
"temp": {
|
||||
"day": 8.5,
|
||||
"min": 1,
|
||||
"max": 8.97,
|
||||
"night": 2.43,
|
||||
"eve": 4.76,
|
||||
"morn": 1
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 6.21,
|
||||
"night": 0.54,
|
||||
"eve": 2.22,
|
||||
"morn": -1.74
|
||||
},
|
||||
"pressure": 1022,
|
||||
"humidity": 35,
|
||||
"dew_point": -6.02,
|
||||
"wind_speed": 4.07,
|
||||
"wind_deg": 41,
|
||||
"wind_gust": 7.58,
|
||||
"weather": [
|
||||
{
|
||||
"id": 804,
|
||||
"main": "Clouds",
|
||||
"description": "overcast clouds",
|
||||
"icon": "04d"
|
||||
}
|
||||
],
|
||||
"clouds": 100,
|
||||
"pop": 0.27,
|
||||
"uvi": 3.03
|
||||
},
|
||||
{
|
||||
"dt": 1648987200,
|
||||
"sunrise": 1648963890,
|
||||
"sunset": 1649010949,
|
||||
"moonrise": 1648967460,
|
||||
"moonset": 1649020980,
|
||||
"moon_phase": 0.07,
|
||||
"temp": {
|
||||
"day": 8.92,
|
||||
"min": 0.99,
|
||||
"max": 9.47,
|
||||
"night": 5.62,
|
||||
"eve": 8.19,
|
||||
"morn": 1.01
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 7.63,
|
||||
"night": 2.63,
|
||||
"eve": 6.61,
|
||||
"morn": -0.71
|
||||
},
|
||||
"pressure": 1026,
|
||||
"humidity": 38,
|
||||
"dew_point": -4.76,
|
||||
"wind_speed": 3.99,
|
||||
"wind_deg": 250,
|
||||
"wind_gust": 9.75,
|
||||
"weather": [
|
||||
{
|
||||
"id": 802,
|
||||
"main": "Clouds",
|
||||
"description": "scattered clouds",
|
||||
"icon": "03d"
|
||||
}
|
||||
],
|
||||
"clouds": 30,
|
||||
"pop": 0,
|
||||
"uvi": 3.04
|
||||
},
|
||||
{
|
||||
"dt": 1649073600,
|
||||
"sunrise": 1649050154,
|
||||
"sunset": 1649097449,
|
||||
"moonrise": 1649054880,
|
||||
"moonset": 1649111880,
|
||||
"moon_phase": 0.1,
|
||||
"temp": {
|
||||
"day": 9.43,
|
||||
"min": 4.54,
|
||||
"max": 12.17,
|
||||
"night": 10.64,
|
||||
"eve": 11.38,
|
||||
"morn": 4.91
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 6.79,
|
||||
"night": 10.01,
|
||||
"eve": 10.87,
|
||||
"morn": 0.46
|
||||
},
|
||||
"pressure": 1011,
|
||||
"humidity": 93,
|
||||
"dew_point": 8.41,
|
||||
"wind_speed": 6.87,
|
||||
"wind_deg": 245,
|
||||
"wind_gust": 15.86,
|
||||
"weather": [
|
||||
{
|
||||
"id": 500,
|
||||
"main": "Rain",
|
||||
"description": "light rain",
|
||||
"icon": "10d"
|
||||
}
|
||||
],
|
||||
"clouds": 100,
|
||||
"pop": 0.92,
|
||||
"rain": 1.95,
|
||||
"uvi": 1.07
|
||||
},
|
||||
{
|
||||
"dt": 1649160000,
|
||||
"sunrise": 1649136419,
|
||||
"sunset": 1649183949,
|
||||
"moonrise": 1649142480,
|
||||
"moonset": 0,
|
||||
"moon_phase": 0.13,
|
||||
"temp": {
|
||||
"day": 13.7,
|
||||
"min": 9.71,
|
||||
"max": 14.06,
|
||||
"night": 9.71,
|
||||
"eve": 11.64,
|
||||
"morn": 10.05
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 12.85,
|
||||
"night": 6.63,
|
||||
"eve": 10.66,
|
||||
"morn": 9.25
|
||||
},
|
||||
"pressure": 1009,
|
||||
"humidity": 66,
|
||||
"dew_point": 7.47,
|
||||
"wind_speed": 6.8,
|
||||
"wind_deg": 258,
|
||||
"wind_gust": 13,
|
||||
"weather": [
|
||||
{
|
||||
"id": 500,
|
||||
"main": "Rain",
|
||||
"description": "light rain",
|
||||
"icon": "10d"
|
||||
}
|
||||
],
|
||||
"clouds": 99,
|
||||
"pop": 0.36,
|
||||
"rain": 0.13,
|
||||
"uvi": 2.59
|
||||
},
|
||||
{
|
||||
"dt": 1649246400,
|
||||
"sunrise": 1649222684,
|
||||
"sunset": 1649270449,
|
||||
"moonrise": 1649230500,
|
||||
"moonset": 1649202540,
|
||||
"moon_phase": 0.17,
|
||||
"temp": {
|
||||
"day": 12.97,
|
||||
"min": 8.53,
|
||||
"max": 13.85,
|
||||
"night": 8.53,
|
||||
"eve": 9.68,
|
||||
"morn": 9.51
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 11.94,
|
||||
"night": 4.36,
|
||||
"eve": 6.3,
|
||||
"morn": 6.57
|
||||
},
|
||||
"pressure": 996,
|
||||
"humidity": 62,
|
||||
"dew_point": 5.84,
|
||||
"wind_speed": 9.57,
|
||||
"wind_deg": 260,
|
||||
"wind_gust": 18.43,
|
||||
"weather": [
|
||||
{
|
||||
"id": 500,
|
||||
"main": "Rain",
|
||||
"description": "light rain",
|
||||
"icon": "10d"
|
||||
}
|
||||
],
|
||||
"clouds": 100,
|
||||
"pop": 0.86,
|
||||
"rain": 1.95,
|
||||
"uvi": 3.29
|
||||
},
|
||||
{
|
||||
"dt": 1649332800,
|
||||
"sunrise": 1649308949,
|
||||
"sunset": 1649356949,
|
||||
"moonrise": 1649319000,
|
||||
"moonset": 1649292960,
|
||||
"moon_phase": 0.2,
|
||||
"temp": {
|
||||
"day": 9.25,
|
||||
"min": 4.63,
|
||||
"max": 10.29,
|
||||
"night": 7.17,
|
||||
"eve": 8.81,
|
||||
"morn": 4.63
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 6.57,
|
||||
"night": 5.9,
|
||||
"eve": 7.06,
|
||||
"morn": -0.26
|
||||
},
|
||||
"pressure": 1000,
|
||||
"humidity": 33,
|
||||
"dew_point": -6.33,
|
||||
"wind_speed": 9.1,
|
||||
"wind_deg": 258,
|
||||
"wind_gust": 21.32,
|
||||
"weather": [
|
||||
{
|
||||
"id": 803,
|
||||
"main": "Clouds",
|
||||
"description": "broken clouds",
|
||||
"icon": "04d"
|
||||
}
|
||||
],
|
||||
"clouds": 66,
|
||||
"pop": 0,
|
||||
"uvi": 3.59
|
||||
},
|
||||
{
|
||||
"dt": 1649419200,
|
||||
"sunrise": 1649395215,
|
||||
"sunset": 1649443449,
|
||||
"moonrise": 1649408100,
|
||||
"moonset": 1649382900,
|
||||
"moon_phase": 0.23,
|
||||
"temp": {
|
||||
"day": 8,
|
||||
"min": 3.94,
|
||||
"max": 8.91,
|
||||
"night": 4.79,
|
||||
"eve": 7.09,
|
||||
"morn": 3.94
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 6.02,
|
||||
"night": 1.81,
|
||||
"eve": 4.59,
|
||||
"morn": 2.23
|
||||
},
|
||||
"pressure": 1000,
|
||||
"humidity": 39,
|
||||
"dew_point": -5.12,
|
||||
"wind_speed": 4.52,
|
||||
"wind_deg": 354,
|
||||
"wind_gust": 9.29,
|
||||
"weather": [
|
||||
{
|
||||
"id": 500,
|
||||
"main": "Rain",
|
||||
"description": "light rain",
|
||||
"icon": "10d"
|
||||
}
|
||||
],
|
||||
"clouds": 100,
|
||||
"pop": 0.8,
|
||||
"rain": 1.51,
|
||||
"uvi": 4
|
||||
},
|
||||
{
|
||||
"dt": 1649505600,
|
||||
"sunrise": 1649481482,
|
||||
"sunset": 1649529949,
|
||||
"moonrise": 1649497920,
|
||||
"moonset": 1649472180,
|
||||
"moon_phase": 0.25,
|
||||
"temp": {
|
||||
"day": 8.7,
|
||||
"min": 1.91,
|
||||
"max": 9.35,
|
||||
"night": 5.29,
|
||||
"eve": 7.66,
|
||||
"morn": 1.91
|
||||
},
|
||||
"feels_like": {
|
||||
"day": 5.76,
|
||||
"night": 2.55,
|
||||
"eve": 4.78,
|
||||
"morn": -1.89
|
||||
},
|
||||
"pressure": 1016,
|
||||
"humidity": 36,
|
||||
"dew_point": -5.69,
|
||||
"wind_speed": 5.65,
|
||||
"wind_deg": 314,
|
||||
"wind_gust": 11.58,
|
||||
"weather": [
|
||||
{
|
||||
"id": 500,
|
||||
"main": "Rain",
|
||||
"description": "light rain",
|
||||
"icon": "10d"
|
||||
}
|
||||
],
|
||||
"clouds": 55,
|
||||
"pop": 0.3,
|
||||
"rain": 0.22,
|
||||
"uvi": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,4 +30,5 @@
|
||||
<string name="loading_nforecast">Loading \nforecast…</string>
|
||||
<string name="retrieve_warning">Unable to retrieve weather</string>
|
||||
<string name="empty_retrieve_warning">Make sure you are connected to the internet and have location permissions granted</string>
|
||||
<string name="no_weather_to_display">No weather to display</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user