Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh feeds based on when they are last updated when app is opened #104

Merged
merged 3 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import dev.sasikanth.rss.reader.refresh.LastUpdatedAt
import dev.sasikanth.rss.reader.repository.RssRepository
import io.sentry.kotlin.multiplatform.Sentry
import java.lang.Exception
Expand All @@ -30,7 +31,8 @@ import java.time.Duration
class FeedsRefreshWorker(
context: Context,
workerParameters: WorkerParameters,
private val rssRepository: RssRepository
private val rssRepository: RssRepository,
private val lastUpdatedAt: LastUpdatedAt
) : CoroutineWorker(context, workerParameters) {

companion object {
Expand All @@ -53,6 +55,7 @@ class FeedsRefreshWorker(
override suspend fun doWork(): Result {
return try {
rssRepository.updateFeeds()
lastUpdatedAt.refresh()
Result.success()
} catch (e: Exception) {
Sentry.captureException(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ class ReaderApplication : Application(), Configuration.Provider {
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker {
return FeedsRefreshWorker(appContext, workerParameters, appComponent.rssRepository)
return FeedsRefreshWorker(
context = appContext,
workerParameters = workerParameters,
rssRepository = appComponent.rssRepository,
lastUpdatedAt = appComponent.lastUpdatedAt
)
}
}
)
Expand Down
5 changes: 5 additions & 0 deletions iosApp/iosApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func refreshFeeds(task: BGAppRefreshTask) {
scheduledRefreshFeeds()
applicationComponent.rssRepository.updateFeeds { error in
if error != nil {
self.applicationComponent.lastUpdatedAt.refresh { error in
// no-op
}
}
task.setTaskCompleted(success: error == nil)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.lifecycle.doOnStart
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import dev.sasikanth.rss.reader.bookmarks.BookmarksPresenter
import dev.sasikanth.rss.reader.di.scopes.ActivityScope
import dev.sasikanth.rss.reader.home.HomePresenter
import dev.sasikanth.rss.reader.refresh.LastUpdatedAt
import dev.sasikanth.rss.reader.repository.RssRepository
import dev.sasikanth.rss.reader.repository.SettingsRepository
import dev.sasikanth.rss.reader.search.SearchPresenter
import dev.sasikanth.rss.reader.settings.SettingsPresenter
Expand All @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject

private typealias HomePresenterFactory =
Expand Down Expand Up @@ -77,14 +81,18 @@ class AppPresenter(
private val homePresenter: HomePresenterFactory,
private val searchPresenter: SearchPresentFactory,
private val bookmarksPresenter: BookmarkPresenterFactory,
private val settingsPresenter: SettingsPresenterFactory
private val settingsPresenter: SettingsPresenterFactory,
private val lastUpdatedAt: LastUpdatedAt,
private val rssRepository: RssRepository
) : ComponentContext by componentContext {

private val presenterInstance =
instanceKeeper.getOrCreate {
PresenterInstance(
dispatchersProvider = dispatchersProvider,
settingsRepository = settingsRepository
settingsRepository = settingsRepository,
lastUpdatedAt = lastUpdatedAt,
rssRepository = rssRepository
)
}

Expand All @@ -99,6 +107,10 @@ class AppPresenter(
childFactory = ::createScreen,
)

init {
lifecycle.doOnStart { presenterInstance.refreshFeedsIfExpired() }
}

fun onBackClicked() {
navigation.pop()
}
Expand Down Expand Up @@ -129,7 +141,9 @@ class AppPresenter(

private class PresenterInstance(
dispatchersProvider: DispatchersProvider,
private val settingsRepository: SettingsRepository
settingsRepository: SettingsRepository,
private val lastUpdatedAt: LastUpdatedAt,
private val rssRepository: RssRepository
) : InstanceKeeper.Instance {

private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main)
Expand All @@ -148,6 +162,15 @@ class AppPresenter(
.launchIn(coroutineScope)
}

fun refreshFeedsIfExpired() {
coroutineScope.launch {
if (lastUpdatedAt.hasExpired()) {
rssRepository.updateFeeds()
lastUpdatedAt.refresh()
}
}
}

override fun onDestroy() {
coroutineScope.cancel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import dev.sasikanth.rss.reader.components.image.ImageLoader
import dev.sasikanth.rss.reader.di.scopes.AppScope
import dev.sasikanth.rss.reader.initializers.Initializer
import dev.sasikanth.rss.reader.network.NetworkComponent
import dev.sasikanth.rss.reader.refresh.LastUpdatedAt
import dev.sasikanth.rss.reader.sentry.SentryComponent
import dev.sasikanth.rss.reader.utils.DefaultDispatchersProvider
import dev.sasikanth.rss.reader.utils.DispatchersProvider
Expand All @@ -31,5 +32,7 @@ abstract class SharedApplicationComponent :

abstract val initializers: Set<Initializer>

abstract val lastUpdatedAt: LastUpdatedAt

@Provides @AppScope fun DefaultDispatchersProvider.bind(): DispatchersProvider = this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.sasikanth.rss.reader.refresh

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.sasikanth.rss.reader.di.scopes.AppScope
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock
import kotlinx.datetime.toInstant
import me.tatarka.inject.annotations.Inject

@Inject
@AppScope
class LastUpdatedAt(private val dataStore: DataStore<Preferences>) {

companion object {
private val UPDATE_DURATION = 60.minutes
}

private val lastUpdatedAtKey = stringPreferencesKey("pref_last_updated_at")

suspend fun refresh() {
dataStore.edit { preferences -> preferences[lastUpdatedAtKey] = Clock.System.now().toString() }
}

suspend fun hasExpired(): Boolean {
val lastUpdatedAt = fetchLastUpdatedAt() ?: return true
val currentTime = Clock.System.now()
val lastUpdateDuration = currentTime - lastUpdatedAt

return lastUpdateDuration > UPDATE_DURATION
}

private suspend fun fetchLastUpdatedAt() =
dataStore.data
.map { preferences -> preferences[lastUpdatedAtKey] ?: return@map null }
.first()
?.toInstant()
}