Refactor weather to use new datastore preferences

This commit is contained in:
MM20 2022-01-02 15:52:17 +01:00
parent d18feccfe4
commit 2b3323bf7f
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
4 changed files with 162 additions and 75 deletions

View File

@ -1,39 +1,24 @@
package de.mm20.launcher2.ui.settings.weather
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.WeatherSettings
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProvider
import de.mm20.launcher2.weather.brightsky.BrightskyProvider
import de.mm20.launcher2.weather.here.HereProvider
import de.mm20.launcher2.weather.metno.MetNoProvider
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext
class WeatherScreenVM(private val context: Application) : AndroidViewModel(context), KoinComponent {
val dataStore: LauncherDataStore by inject()
val weatherProvider = MutableLiveData<WeatherSettings.WeatherProvider?>(null)
fun setWeatherProvider(provider: WeatherSettings.WeatherProvider) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWeather(it.weather.toBuilder().setProvider(provider))
.build()
}
}
}
class WeatherScreenVM : ViewModel(), KoinComponent {
private val repository: WeatherRepository by inject()
private val dataStore: LauncherDataStore by inject()
val imperialUnits = dataStore.data.map { it.weather.imperialUnits }.asLiveData()
fun setImperialUnits(imperialUnits: Boolean) {
@ -46,30 +31,23 @@ class WeatherScreenVM(private val context: Application) : AndroidViewModel(conte
}
}
private var provider: WeatherProvider<out WeatherLocation>? = null
set(value) {
field = value
if (value != null) {
val autoLocation = value.autoLocation
this.autoLocation.postValue(autoLocation)
location.postValue(if (autoLocation) value.getLastLocation() else value.getLocation())
}
}
val weatherProvider = repository.selectedProvider.asLiveData()
fun setWeatherProvider(provider: WeatherSettings.WeatherProvider) {
repository.selectProvider(provider)
}
val autoLocation = MutableLiveData(false)
val autoLocation = repository.autoLocation.asLiveData()
fun setAutoLocation(autoLocation: Boolean) {
provider?.autoLocation = autoLocation
location.postValue(if (autoLocation) provider?.getLastLocation() else provider?.getLocation())
this.autoLocation.postValue(autoLocation)
repository.setAutoLocation(autoLocation)
}
val location = MutableLiveData<WeatherLocation?>(null)
fun setLocation(location: WeatherLocation) {
provider?.setLocation(location)
this.location.postValue(location)
locationResults.postValue(emptyList())
repository.setLocation(location)
}
private var debounceSearchJob : Job? = null
private var debounceSearchJob: Job? = null
suspend fun searchLocation(query: String) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
@ -80,11 +58,8 @@ class WeatherScreenVM(private val context: Application) : AndroidViewModel(conte
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
Log.d("MM20", "Searching for $query")
val provider = provider ?: return@launch
isSearchingLocation.value = true
val results = provider
locationResults.value = results.lookupLocation(query)
locationResults.value = repository.lookupLocation(query)
isSearchingLocation.value = false
}
}
@ -95,14 +70,14 @@ class WeatherScreenVM(private val context: Application) : AndroidViewModel(conte
init {
viewModelScope.launch {
dataStore.data.map { it.weather.provider }.collectLatest {
weatherProvider.postValue(it)
provider = when (it) {
WeatherSettings.WeatherProvider.OpenWeatherMap -> OpenWeatherMapProvider(context)
WeatherSettings.WeatherProvider.Here -> HereProvider(context)
WeatherSettings.WeatherProvider.BrightSky -> BrightskyProvider(context)
else -> MetNoProvider(context)
}
val autoLocation = repository.autoLocation
val location = repository.location
val lastLocation = repository.lastLocation
combine(autoLocation, lastLocation, location) { autoLoc, lastLoc, loc ->
if (autoLoc) lastLoc
else loc
}.collectLatest {
this@WeatherScreenVM.location.postValue(it)
}
}
}

View File

@ -1,8 +1,21 @@
package de.mm20.launcher2.weather
import de.mm20.launcher2.preferences.Settings.WeatherSettings
import de.mm20.launcher2.weather.brightsky.BrightskyProvider
import de.mm20.launcher2.weather.here.HereProvider
import de.mm20.launcher2.weather.metno.MetNoProvider
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val weatherModule = module {
single { WeatherRepository(androidContext(), get()) }
single<WeatherRepository> { WeatherRepositoryImpl(androidContext(), get(), get()) }
factory { (selectedProvider: WeatherSettings.WeatherProvider) ->
when (selectedProvider) {
WeatherSettings.WeatherProvider.OpenWeatherMap -> OpenWeatherMapProvider(androidContext())
WeatherSettings.WeatherProvider.Here -> HereProvider(androidContext())
WeatherSettings.WeatherProvider.BrightSky -> BrightskyProvider(androidContext())
else -> MetNoProvider(androidContext())
}
}
}

View File

@ -2,23 +2,95 @@ package de.mm20.launcher2.weather
import android.content.Context
import android.util.Log
import androidx.datastore.dataStore
import androidx.work.*
import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.WeatherSettings
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
import java.util.*
import java.util.concurrent.TimeUnit
class WeatherRepository(
val context: Context,
val database: AppDatabase,
) {
interface WeatherRepository {
val forecasts: Flow<List<DailyForecast>>
val forecasts = database.weatherDao().getForecasts()
.map { it.map { Forecast(it) } }
.map {
groupForecastsPerDay(it)
suspend fun lookupLocation(query: String): List<WeatherLocation>
val lastLocation: Flow<WeatherLocation?>
val location: Flow<WeatherLocation?>
val autoLocation: Flow<Boolean>
fun setLocation(location: WeatherLocation)
fun setAutoLocation(autoLocation: Boolean)
fun setLastLocation(lastLocation: WeatherLocation?)
fun selectProvider(provider: WeatherSettings.WeatherProvider)
val selectedProvider: Flow<WeatherSettings.WeatherProvider>
}
class WeatherRepositoryImpl(
private val context: Context,
private val database: AppDatabase,
private val dataStore: LauncherDataStore,
) : WeatherRepository, KoinComponent {
private val scope = CoroutineScope(Dispatchers.Main + Job())
private var provider: WeatherProvider<out WeatherLocation>
override val selectedProvider = dataStore.data.map { it.weather.provider }
override val forecasts: Flow<List<DailyForecast>>
get() = database.weatherDao().getForecasts()
.map { it.map { Forecast(it) } }
.map {
groupForecastsPerDay(it)
}
override val lastLocation = MutableStateFlow<WeatherLocation?>(null)
override val location = MutableStateFlow<WeatherLocation?>(null)
override val autoLocation = MutableStateFlow(false)
override fun setLocation(location: WeatherLocation) {
provider.setLocation(location)
this.location.value = location
provider.resetLastUpdate()
requestUpdate()
}
override fun setAutoLocation(autoLocation: Boolean) {
provider.autoLocation = autoLocation
this.autoLocation.value = autoLocation
provider.resetLastUpdate()
requestUpdate()
}
override fun setLastLocation(lastLocation: WeatherLocation?) {
this.lastLocation.value = lastLocation
}
override suspend fun lookupLocation(query: String): List<WeatherLocation> {
return provider.lookupLocation(query)
}
override fun selectProvider(provider: WeatherSettings.WeatherProvider) {
scope.launch {
dataStore.updateData {
it.toBuilder()
.setWeather(
it.weather.toBuilder()
.setProvider(provider)
)
.build()
}
}
}
init {
val weatherRequest =
@ -28,6 +100,28 @@ class WeatherRepository(
"weather",
ExistingPeriodicWorkPolicy.KEEP, weatherRequest
)
provider = runBlocking {
val selectedProvider = selectedProvider.first()
get { parametersOf(selectedProvider) }
}
scope.launch {
var providerSetting: WeatherSettings.WeatherProvider? = null
selectedProvider.collectLatest {
if (it != providerSetting) {
providerSetting = it
provider = get { parametersOf(it) }
location.value = provider.getLocation()
lastLocation.value = provider.getLastLocation()
autoLocation.value = provider.autoLocation
provider.resetLastUpdate()
requestUpdate()
}
}
}
requestUpdate()
}
private fun groupForecastsPerDay(forecasts: List<Forecast>): List<DailyForecast> {
@ -71,30 +165,36 @@ class WeatherRepository(
}
fun requestUpdate(context: Context) {
val provider = WeatherProvider.getInstance(context) ?: return
if (provider.isUpdateRequired()) {
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
.addTag("weather")
.build()
WorkManager.getInstance(context).enqueue(weatherRequest)
} else {
Log.d("MM20", "No weather update required")
}
private fun requestUpdate() {
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
.addTag("weather")
.build()
WorkManager.getInstance(context).enqueue(weatherRequest)
}
}
class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
CoroutineWorker(context, params), KoinComponent {
val repository: WeatherRepository by inject()
override suspend fun doWork(): Result {
val provider = WeatherProvider.getInstance(context) ?: return Result.failure()
if (!provider.isAvailable()) return Result.failure()
if (!provider.isUpdateRequired()) return Result.failure()
Log.d("MM20", "Requesting weather data")
val providerPref = repository.selectedProvider.first()
val provider: WeatherProvider<out WeatherLocation> = get { parametersOf(providerPref) }
if (!provider.isAvailable()) {
Log.d("MM20", "Weather provider is not available")
return Result.failure()
}
if (!provider.isUpdateRequired()) {
Log.d("MM20", "No weather update required")
return Result.failure()
}
val weatherData = provider.fetchNewWeatherData()
return if (weatherData == null) {
Log.d("MM20", "Weather update failed")
Result.retry()
} else {
repository.setLastLocation(provider.getLastLocation())
Log.d("MM20", "Weather update succeeded")
AppDatabase.getInstance(applicationContext).weatherDao()
.replaceAll(weatherData.map { it.toDatabaseEntity() })

View File

@ -16,6 +16,5 @@ class WeatherViewModel(application: Application) : AndroidViewModel(application)
}
fun requestUpdate(context: Context) {
repository.requestUpdate(context)
}
}