migrate scheduled toots to paging 3 (#2208)

This commit is contained in:
Konrad Pozniak 2021-06-24 21:24:04 +02:00 committed by GitHub
parent f6dd131b95
commit 955267199e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 184 deletions

View File

@ -19,18 +19,25 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import autodispose2.androidx.lifecycle.autoDispose
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R 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.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.Status
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show 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 import javax.inject.Inject
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
@ -38,6 +45,9 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var eventHub: EventHub
private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory }
private val adapter = ScheduledTootAdapter(this) private val adapter = ScheduledTootAdapter(this)
@ -64,58 +74,58 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
binding.scheduledTootList.addItemDecoration(divider) binding.scheduledTootList.addItemDecoration(divider)
binding.scheduledTootList.adapter = adapter binding.scheduledTootList.adapter = adapter
viewModel.data.observe(this) { lifecycleScope.launch {
adapter.submitList(it) viewModel.data.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
} }
viewModel.networkState.observe(this) { (status) -> adapter.addLoadStateListener { loadState ->
when(status) { if (loadState.refresh is Error) {
Status.SUCCESS -> { binding.progressBar.hide()
binding.progressBar.hide() binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
binding.swipeRefreshLayout.isRefreshing = false refreshStatuses()
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()
}
} }
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() 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() { private fun refreshStatuses() {
viewModel.reload() adapter.refresh()
} }
override fun edit(item: ScheduledStatus) { override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
scheduledTootId = item.id, scheduledTootId = item.id,
tootText = item.params.text, tootText = item.params.text,
contentWarning = item.params.spoilerText, contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments, mediaAttachments = item.mediaAttachments,
inReplyToId = item.params.inReplyToId, inReplyToId = item.params.inReplyToId,
visibility = item.params.visibility, visibility = item.params.visibility,
scheduledAt = item.scheduledAt, scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive sensitive = item.params.sensitive
)) ))
startActivity(intent) startActivity(intent)
} }
@ -125,9 +135,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
} }
companion object { companion object {
@JvmStatic fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java)
fun newIntent(context: Context): Intent {
return Intent(context, ScheduledTootActivity::class.java)
}
} }
} }

View File

@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.scheduled
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.paging.PagedListAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
@ -31,7 +31,7 @@ interface ScheduledTootActionListener {
class ScheduledTootAdapter( class ScheduledTootAdapter(
val listener: ScheduledTootActionListener val listener: ScheduledTootActionListener
) : PagedListAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>( ) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>(
object: DiffUtil.ItemCallback<ScheduledStatus>(){ object: DiffUtil.ItemCallback<ScheduledStatus>(){
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id

View File

@ -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 <http://www.gnu.org/licenses>. */
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<String, ScheduledStatus>() {
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
private var dataSource: ScheduledTootDataSource? = null
val networkState = MutableLiveData<NetworkState>()
override fun create(): DataSource<String, ScheduledStatus> {
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<ScheduledStatus>,
private val networkState: MutableLiveData<NetworkState>
): ItemKeyedDataSource<String, ScheduledStatus>() {
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<ScheduledStatus>) {
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<String>, callback: LoadCallback<ScheduledStatus>) {
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<String>, callback: LoadCallback<ScheduledStatus>) {
// we are always loading from beginning to end
}
override fun getKey(item: ScheduledStatus): String {
return item.id
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<ScheduledStatus>()
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<ScheduledStatus>
): PagingSource<String, ScheduledStatus>() {
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
return null
}
override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> {
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)
}
}
}
}

View File

@ -16,53 +16,39 @@
package com.keylesspalace.tusky.components.scheduled package com.keylesspalace.tusky.components.scheduled
import android.util.Log import android.util.Log
import androidx.paging.Config import androidx.lifecycle.ViewModel
import androidx.paging.toLiveData 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.EventHub
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.RxAwareViewModel import kotlinx.coroutines.launch
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.rx3.await
import javax.inject.Inject import javax.inject.Inject
class ScheduledTootViewModel @Inject constructor( class ScheduledTootViewModel @Inject constructor(
val mastodonApi: MastodonApi, val mastodonApi: MastodonApi,
val eventHub: EventHub val eventHub: EventHub
): RxAwareViewModel() { ): ViewModel() {
private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables) private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi)
val data = dataSourceFactory.toLiveData( val data = Pager(
config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false) config = PagingConfig(pageSize = 20, initialLoadSize = 20),
) pagingSourceFactory = pagingSourceFactory
).flow
val networkState = dataSourceFactory.networkState .cachedIn(viewModelScope)
init {
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.subscribe { event ->
if (event is StatusScheduledEvent) {
reload()
}
}
.autoDispose()
}
fun reload() {
dataSourceFactory.reload()
}
fun deleteScheduledStatus(status: ScheduledStatus) { fun deleteScheduledStatus(status: ScheduledStatus) {
mastodonApi.deleteScheduledStatus(status.id) viewModelScope.launch {
.subscribe({ try {
dataSourceFactory.remove(status) mastodonApi.deleteScheduledStatus(status.id).await()
},{ throwable -> pagingSourceFactory.remove(status)
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) } catch (throwable: Throwable) {
}) Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
.autoDispose() }
}
} }
}
}