From 955267199ef53e70413bc22e2501ec75d097eb8a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 24 Jun 2021 21:24:04 +0200 Subject: [PATCH] migrate scheduled toots to paging 3 (#2208) --- .../scheduled/ScheduledTootActivity.kt | 93 ++++++++-------- .../scheduled/ScheduledTootAdapter.kt | 4 +- .../scheduled/ScheduledTootDataSource.kt | 102 ------------------ .../scheduled/ScheduledTootPagingSource.kt | 79 ++++++++++++++ .../scheduled/ScheduledTootViewModel.kt | 60 ++++------- 5 files changed, 154 insertions(+), 184 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 4af5771c1..0df191f81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -19,18 +19,25 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import autodispose2.androidx.lifecycle.autoDispose import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus -import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { @@ -38,6 +45,9 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var eventHub: EventHub + private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } private val adapter = ScheduledTootAdapter(this) @@ -64,58 +74,58 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec binding.scheduledTootList.addItemDecoration(divider) binding.scheduledTootList.adapter = adapter - viewModel.data.observe(this) { - adapter.submitList(it) + lifecycleScope.launch { + viewModel.data.collectLatest { pagingData -> + adapter.submitData(pagingData) + } } - viewModel.networkState.observe(this) { (status) -> - when(status) { - Status.SUCCESS -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - if(viewModel.data.value?.loadedCount == 0) { - binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) - binding.errorMessageView.show() - } else { - binding.errorMessageView.hide() - } + adapter.addLoadStateListener { loadState -> + if (loadState.refresh is Error) { + binding.progressBar.hide() + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + refreshStatuses() } - Status.RUNNING -> { + binding.errorMessageView.show() + } + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + if (loadState.refresh is LoadState.NotLoading) { + binding.progressBar.hide() + if(adapter.itemCount == 0) { + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + binding.errorMessageView.show() + } else { binding.errorMessageView.hide() - if(viewModel.data.value?.loadedCount ?: 0 > 0) { - binding.swipeRefreshLayout.isRefreshing = true - } else { - binding.progressBar.show() - } - } - Status.FAILED -> { - if(viewModel.data.value?.loadedCount ?: 0 >= 0) { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { - refreshStatuses() - } - binding.errorMessageView.show() - } } } } + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe { event -> + if (event is StatusScheduledEvent) { + adapter.refresh() + } + } } private fun refreshStatuses() { - viewModel.reload() + adapter.refresh() } override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( - scheduledTootId = item.id, - tootText = item.params.text, - contentWarning = item.params.spoilerText, - mediaAttachments = item.mediaAttachments, - inReplyToId = item.params.inReplyToId, - visibility = item.params.visibility, - scheduledAt = item.scheduledAt, - sensitive = item.params.sensitive + scheduledTootId = item.id, + tootText = item.params.text, + contentWarning = item.params.spoilerText, + mediaAttachments = item.mediaAttachments, + inReplyToId = item.params.inReplyToId, + visibility = item.params.visibility, + scheduledAt = item.scheduledAt, + sensitive = item.params.sensitive )) startActivity(intent) } @@ -125,9 +135,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } companion object { - @JvmStatic - fun newIntent(context: Context): Intent { - return Intent(context, ScheduledTootActivity::class.java) - } + fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index 414130ddb..e21019ee7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.scheduled import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding import com.keylesspalace.tusky.entity.ScheduledStatus @@ -31,7 +31,7 @@ interface ScheduledTootActionListener { class ScheduledTootAdapter( val listener: ScheduledTootActionListener -) : PagedListAdapter>( +) : PagingDataAdapter>( object: DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { return oldItem.id == newItem.id diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt deleted file mode 100644 index 09d05bcc5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright 2019 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.scheduled - -import android.util.Log -import androidx.lifecycle.MutableLiveData -import androidx.paging.DataSource -import androidx.paging.ItemKeyedDataSource -import com.keylesspalace.tusky.entity.ScheduledStatus -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.addTo - -class ScheduledTootDataSourceFactory( - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable -): DataSource.Factory() { - - private val scheduledTootsCache = mutableListOf() - - private var dataSource: ScheduledTootDataSource? = null - - val networkState = MutableLiveData() - - override fun create(): DataSource { - return ScheduledTootDataSource(mastodonApi, disposables, scheduledTootsCache, networkState).also { - dataSource = it - } - } - - fun reload() { - scheduledTootsCache.clear() - dataSource?.invalidate() - } - - fun remove(status: ScheduledStatus) { - scheduledTootsCache.remove(status) - dataSource?.invalidate() - } - -} - - -class ScheduledTootDataSource( - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable, - private val scheduledTootsCache: MutableList, - private val networkState: MutableLiveData -): ItemKeyedDataSource() { - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - if(scheduledTootsCache.isNotEmpty()) { - callback.onResult(scheduledTootsCache.toList()) - } else { - networkState.postValue(NetworkState.LOADING) - mastodonApi.scheduledStatuses(limit = params.requestedLoadSize) - .subscribe({ newData -> - scheduledTootsCache.addAll(newData) - callback.onResult(newData) - networkState.postValue(NetworkState.LOADED) - }, { throwable -> - Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) - networkState.postValue(NetworkState.error(throwable.message)) - }) - .addTo(disposables) - } - } - - override fun loadAfter(params: LoadParams, callback: LoadCallback) { - mastodonApi.scheduledStatuses(limit = params.requestedLoadSize, maxId = params.key) - .subscribe({ newData -> - scheduledTootsCache.addAll(newData) - callback.onResult(newData) - }, { throwable -> - Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) - networkState.postValue(NetworkState.error(throwable.message)) - }) - .addTo(disposables) - } - - override fun loadBefore(params: LoadParams, callback: LoadCallback) { - // we are always loading from beginning to end - } - - override fun getKey(item: ScheduledStatus): String { - return item.id - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt new file mode 100644 index 000000000..0578c5b12 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt @@ -0,0 +1,79 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.rx3.await + +class ScheduledTootPagingSourceFactory( + private val mastodonApi: MastodonApi +): () -> ScheduledTootPagingSource { + + private val scheduledTootsCache = mutableListOf() + + private var pagingSource: ScheduledTootPagingSource? = null + + override fun invoke(): ScheduledTootPagingSource { + return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also { + pagingSource = it + } + } + + fun remove(status: ScheduledStatus) { + scheduledTootsCache.remove(status) + pagingSource?.invalidate() + } +} + +class ScheduledTootPagingSource( + private val mastodonApi: MastodonApi, + private val scheduledTootsCache: MutableList +): PagingSource() { + + override fun getRefreshKey(state: PagingState): String? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + return if (params is LoadParams.Refresh && scheduledTootsCache.isNotEmpty()) { + LoadResult.Page( + data = scheduledTootsCache, + prevKey = null, + nextKey = scheduledTootsCache.lastOrNull()?.id + ) + } else { + try { + val result = mastodonApi.scheduledStatuses( + maxId = params.key, + limit = params.loadSize + ).await() + + LoadResult.Page( + data = result, + prevKey = null, + nextKey = result.lastOrNull()?.id + ) + } catch (e: Exception) { + Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e) + LoadResult.Error(e) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt index f07ca2b97..6890b15b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -16,53 +16,39 @@ package com.keylesspalace.tusky.components.scheduled import android.util.Log -import androidx.paging.Config -import androidx.paging.toLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.RxAwareViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await import javax.inject.Inject class ScheduledTootViewModel @Inject constructor( val mastodonApi: MastodonApi, val eventHub: EventHub -): RxAwareViewModel() { +): ViewModel() { - private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables) + private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi) - val data = dataSourceFactory.toLiveData( - config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false) - ) - - val networkState = dataSourceFactory.networkState - - init { - eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { event -> - if (event is StatusScheduledEvent) { - reload() - } - } - .autoDispose() - } - - fun reload() { - dataSourceFactory.reload() - } + val data = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20), + pagingSourceFactory = pagingSourceFactory + ).flow + .cachedIn(viewModelScope) fun deleteScheduledStatus(status: ScheduledStatus) { - mastodonApi.deleteScheduledStatus(status.id) - .subscribe({ - dataSourceFactory.remove(status) - },{ throwable -> - Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) - }) - .autoDispose() - + viewModelScope.launch { + try { + mastodonApi.deleteScheduledStatus(status.id).await() + pagingSourceFactory.remove(status) + } catch (throwable: Throwable) { + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + } + } } - -} \ No newline at end of file +}