- mid commit

This commit is contained in:
2023-08-10 23:12:37 +01:00
parent ffa7edf25d
commit 9aaf98a655
56 changed files with 1420 additions and 682 deletions

View File

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

View File

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

View File

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

View File

@@ -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]
// *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,4 +0,0 @@
{
"cod": 401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."
}

View File

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

View File

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