1
0
mirror of https://github.com/accelforce/Yuito synced 2025-01-30 23:54:53 +01:00

move to androidx paging

This commit is contained in:
Konrad Pozniak 2019-12-30 20:40:27 +01:00
parent 63d6fe7270
commit 84a3280964
8 changed files with 224 additions and 104 deletions

View File

@ -3,9 +3,10 @@ package com.keylesspalace.tusky.components.scheduled
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.MenuItem import android.view.MenuItem
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
@ -14,8 +15,9 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi 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 com.uber.autodispose.AutoDispose.autoDisposable import com.uber.autodispose.AutoDispose.autoDisposable
@ -23,13 +25,8 @@ import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_scheduled_toot.* import kotlinx.android.synthetic.main.activity_scheduled_toot.*
import kotlinx.android.synthetic.main.toolbar_basic.* import kotlinx.android.synthetic.main.toolbar_basic.*
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject import javax.inject.Inject
class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable {
companion object { companion object {
@ -41,10 +38,12 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable {
lateinit var adapter: ScheduledTootAdapter lateinit var adapter: ScheduledTootAdapter
@Inject
lateinit var mastodonApi: MastodonApi
@Inject @Inject
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
@Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: ScheduledTootViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -67,6 +66,8 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable {
adapter = ScheduledTootAdapter(this) adapter = ScheduledTootAdapter(this)
scheduledTootList.adapter = adapter scheduledTootList.adapter = adapter
viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java]
loadStatuses() loadStatuses()
eventHub.events eventHub.events
@ -90,55 +91,42 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable {
} }
fun loadStatuses() { fun loadStatuses() {
progressBar.visibility = View.VISIBLE viewModel.data.observe(this, Observer {
mastodonApi.scheduledStatuses() adapter.submitList(it)
.enqueue(object : Callback<List<ScheduledStatus>> { })
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
progressBar.visibility = View.GONE
if (response.body().isNullOrEmpty()) {
errorMessageView.show()
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
} else {
show(response.body()!!)
}
}
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) { viewModel.networkState.observe(this, Observer { (status) ->
progressBar.visibility = View.GONE when(status) {
errorMessageView.show() Status.SUCCESS -> {
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { progressBar.hide()
errorMessageView.hide() swipeRefreshLayout.isRefreshing = false
loadStatuses() errorMessageView.hide()
} }
Status.RUNNING -> {
errorMessageView.hide()
if(viewModel.data.value?.loadedCount ?: 0 > 0) {
swipeRefreshLayout.isRefreshing = true
} else {
progressBar.show()
} }
}) }
Status.FAILED -> {
if(viewModel.data.value?.loadedCount ?: 0 >= 0) {
progressBar.hide()
swipeRefreshLayout.isRefreshing = false
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
refreshStatuses()
}
errorMessageView.show()
}
}
}
})
} }
private fun refreshStatuses() { private fun refreshStatuses() {
swipeRefreshLayout.isRefreshing = true viewModel.reload()
mastodonApi.scheduledStatuses()
.enqueue(object : Callback<List<ScheduledStatus>> {
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
swipeRefreshLayout.isRefreshing = false
if (response.body().isNullOrEmpty()) {
errorMessageView.show()
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
} else {
show(response.body()!!)
}
}
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) {
swipeRefreshLayout.isRefreshing = false
}
})
}
fun show(statuses: List<ScheduledStatus>) {
adapter.setItems(statuses)
adapter.notifyDataSetChanged()
} }
override fun edit(position: Int, item: ScheduledStatus?) { override fun edit(position: Int, item: ScheduledStatus?) {
@ -162,15 +150,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable {
if (item == null) { if (item == null) {
return return
} }
mastodonApi.deleteScheduledStatus(item.id)
.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
adapter.removeItem(position)
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) { viewModel.deleteScheduledStatus(item)
}
})
} }
} }

View File

@ -20,6 +20,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
@ -31,9 +33,18 @@ interface ScheduledTootAction {
class ScheduledTootAdapter( class ScheduledTootAdapter(
val listener: ScheduledTootAction val listener: ScheduledTootAction
) : RecyclerView.Adapter<ScheduledTootAdapter.TootViewHolder>() { ) : PagedListAdapter<ScheduledStatus, ScheduledTootAdapter.TootViewHolder>(
object: DiffUtil.ItemCallback<ScheduledStatus>(){
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem.id == newItem.id
}
private var items: MutableList<ScheduledStatus> = mutableListOf() override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
@ -42,25 +53,12 @@ class ScheduledTootAdapter(
} }
override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) { override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) {
viewHolder.bind(items[position]) getItem(position)?.let{
} viewHolder.bind(it)
override fun getItemCount() = items.size
fun setItems(newItems: List<ScheduledStatus>) {
items = newItems.toMutableList()
notifyDataSetChanged()
}
fun removeItem(position: Int): ScheduledStatus? {
if (position < 0 || position >= items.size) {
return null
} }
val toot = items.removeAt(position)
notifyItemRemoved(position)
return toot
} }
inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) { inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val text: TextView = view.findViewById(R.id.text) private val text: TextView = view.findViewById(R.id.text)
@ -70,15 +68,15 @@ class ScheduledTootAdapter(
fun bind(item: ScheduledStatus) { fun bind(item: ScheduledStatus) {
edit.isEnabled = true edit.isEnabled = true
delete.isEnabled = true delete.isEnabled = true
text.text = item.params.text text.text = item.params.text
edit.setOnClickListener { v: View -> edit.setOnClickListener { v: View ->
v.isEnabled = false v.isEnabled = false
listener.edit(adapterPosition, item) listener.edit(adapterPosition, item)
} }
delete.setOnClickListener { v: View -> delete.setOnClickListener { v: View ->
v.isEnabled = false v.isEnabled = false
listener.delete(adapterPosition, item) listener.delete(adapterPosition, item)
} }
} }

View File

@ -0,0 +1,87 @@
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.disposables.CompositeDisposable
import io.reactivex.rxkotlin.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,46 @@
package com.keylesspalace.tusky.components.scheduled
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.paging.Config
import androidx.paging.toLiveData
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import javax.inject.Inject
class ScheduledTootViewModel @Inject constructor(
val mastodonApi: MastodonApi
): ViewModel() {
private val disposables = CompositeDisposable()
private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables)
val data = dataSourceFactory.toLiveData(
config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false)
)
val networkState = dataSourceFactory.networkState
fun reload() {
dataSourceFactory.reload()
}
fun deleteScheduledStatus(status: ScheduledStatus) {
mastodonApi.deleteScheduledStatus(status.id)
.subscribe({
dataSourceFactory.remove(status)
},{ throwable ->
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
})
.addTo(disposables)
}
override fun onCleared() {
disposables.clear()
}
}

View File

@ -77,7 +77,7 @@ class NetworkModule {
.apply { .apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
} }
} }
.build() .build()

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
@ -79,5 +80,10 @@ abstract class ViewModelModule {
@ViewModelKey(ComposeViewModel::class) @ViewModelKey(ComposeViewModel::class)
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ScheduledTootViewModel::class)
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
//Add more ViewModels here //Add more ViewModels here
} }

View File

@ -201,12 +201,15 @@ interface MastodonApi {
): Single<Status> ): Single<Status>
@GET("api/v1/scheduled_statuses") @GET("api/v1/scheduled_statuses")
fun scheduledStatuses(): Call<List<ScheduledStatus>> fun scheduledStatuses(
@Query("limit") limit: Int? = null,
@Query("max_id") maxId: String? = null
): Single<List<ScheduledStatus>>
@DELETE("api/v1/scheduled_statuses/{id}") @DELETE("api/v1/scheduled_statuses/{id}")
fun deleteScheduledStatus( fun deleteScheduledStatus(
@Path("id") scheduledStatusId: String @Path("id") scheduledStatusId: String
): Call<ResponseBody> ): Single<ResponseBody>
@GET("api/v1/accounts/verify_credentials") @GET("api/v1/accounts/verify_credentials")
fun accountVerifyCredentials(): Single<Account> fun accountVerifyCredentials(): Single<Account>

View File

@ -23,6 +23,18 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scheduledTootList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.keylesspalace.tusky.view.BackgroundMessageView <com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/errorMessageView" android:id="@+id/errorMessageView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -36,18 +48,6 @@
tools:src="@drawable/elephant_error" tools:src="@drawable/elephant_error"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scheduledTootList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>