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

View File

@ -1,8 +1,21 @@
package de.mm20.launcher2.weather 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.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val weatherModule = 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.content.Context
import android.util.Log import android.util.Log
import androidx.datastore.dataStore
import androidx.work.* import androidx.work.*
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.flow.map import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.stateIn 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.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class WeatherRepository( interface WeatherRepository {
val context: Context, val forecasts: Flow<List<DailyForecast>>
val database: AppDatabase,
) {
val forecasts = database.weatherDao().getForecasts() suspend fun lookupLocation(query: String): List<WeatherLocation>
.map { it.map { Forecast(it) } }
.map { val lastLocation: Flow<WeatherLocation?>
groupForecastsPerDay(it) 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 { init {
val weatherRequest = val weatherRequest =
@ -28,6 +100,28 @@ class WeatherRepository(
"weather", "weather",
ExistingPeriodicWorkPolicy.KEEP, weatherRequest 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> { private fun groupForecastsPerDay(forecasts: List<Forecast>): List<DailyForecast> {
@ -71,30 +165,36 @@ class WeatherRepository(
} }
fun requestUpdate(context: Context) { private fun requestUpdate() {
val provider = WeatherProvider.getInstance(context) ?: return val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
if (provider.isUpdateRequired()) { .addTag("weather")
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java) .build()
.addTag("weather") WorkManager.getInstance(context).enqueue(weatherRequest)
.build()
WorkManager.getInstance(context).enqueue(weatherRequest)
} else {
Log.d("MM20", "No weather update required")
}
} }
} }
class WeatherUpdateWorker(val context: Context, params: WorkerParameters) : class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) { CoroutineWorker(context, params), KoinComponent {
val repository: WeatherRepository by inject()
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val provider = WeatherProvider.getInstance(context) ?: return Result.failure() Log.d("MM20", "Requesting weather data")
if (!provider.isAvailable()) return Result.failure() val providerPref = repository.selectedProvider.first()
if (!provider.isUpdateRequired()) return Result.failure() 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() val weatherData = provider.fetchNewWeatherData()
return if (weatherData == null) { return if (weatherData == null) {
Log.d("MM20", "Weather update failed") Log.d("MM20", "Weather update failed")
Result.retry() Result.retry()
} else { } else {
repository.setLastLocation(provider.getLastLocation())
Log.d("MM20", "Weather update succeeded") Log.d("MM20", "Weather update succeeded")
AppDatabase.getInstance(applicationContext).weatherDao() AppDatabase.getInstance(applicationContext).weatherDao()
.replaceAll(weatherData.map { it.toDatabaseEntity() }) .replaceAll(weatherData.map { it.toDatabaseEntity() })

View File

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