@@ -20,8 +20,6 @@ package org.akanework.gramophone.ui.adapters
2020import android.annotation.SuppressLint
2121import android.content.res.Configuration
2222import android.net.Uri
23- import android.os.Handler
24- import android.os.Looper
2523import android.util.Log
2624import android.view.LayoutInflater
2725import android.view.View
@@ -45,16 +43,13 @@ import com.google.android.material.button.MaterialButton
4543import kotlinx.coroutines.CoroutineScope
4644import kotlinx.coroutines.Dispatchers
4745import kotlinx.coroutines.ExperimentalCoroutinesApi
46+ import kotlinx.coroutines.NonCancellable
4847import kotlinx.coroutines.cancel
4948import kotlinx.coroutines.flow.Flow
5049import kotlinx.coroutines.flow.MutableStateFlow
51- import kotlinx.coroutines.flow.SharedFlow
52- import kotlinx.coroutines.flow.SharingStarted
50+ import kotlinx.coroutines.flow.combine
5351import kotlinx.coroutines.flow.first
5452import kotlinx.coroutines.flow.flatMapLatest
55- import kotlinx.coroutines.flow.map
56- import kotlinx.coroutines.flow.onEach
57- import kotlinx.coroutines.flow.shareIn
5853import kotlinx.coroutines.launch
5954import kotlinx.coroutines.runBlocking
6055import kotlinx.coroutines.sync.Semaphore
@@ -84,16 +79,14 @@ abstract class BaseAdapter<T>(
8479 val ownsView : Boolean ,
8580 defaultLayoutType : LayoutType ,
8681 private val isSubFragment : Boolean = false ,
87- private val rawOrderExposed : Boolean = false ,
82+ rawOrderExposed : Boolean = false ,
8883 private val allowDiffUtils : Boolean = false ,
8984 private val canSort : Boolean = true ,
9085 private val fallbackSpans : Int = 1
9186) : AdapterFragment.BaseInterface<BaseAdapter<T>.ViewHolder>(), PopupTextProvider, ItemHeightHelper {
9287
9388 val context = fragment.requireContext()
9489 protected val liveDataAgent = MutableStateFlow (liveData)
95- @OptIn(ExperimentalCoroutinesApi ::class )
96- private val flow = liveDataAgent.flatMapLatest { it }
9790 protected inline val mainActivity
9891 get() = context as MainActivity
9992 internal inline val layoutInflater: LayoutInflater
@@ -110,10 +103,8 @@ abstract class BaseAdapter<T>(
110103 override val itemHeightHelper by lazy {
111104 DefaultItemHeightHelper .concatItemHeightHelper(decorAdapter, { 1 }, this )
112105 }
113- private var lastList: List <T >? = null
114- protected val list = ArrayList <T >((flow as ? SharedFlow <List <T >>)?.replayCache?.lastOrNull()?.size ? : 0 )
106+ protected var list: Pair <List <T >, List <T >>
115107 private var layoutManager: RecyclerView .LayoutManager ? = null
116- private var listLock = Semaphore (1 )
117108 protected var recyclerView: MyRecyclerView ? = null
118109 private set
119110
@@ -173,14 +164,27 @@ abstract class BaseAdapter<T>(
173164 else
174165 initialSortType
175166 )
176- private val comparator: SharedFlow <Sorter .HintedComparator <T >? > = sortType.map {
177- sorter.getComparator(it)
178- }.shareIn(CoroutineScope (Dispatchers .Default ), SharingStarted .WhileSubscribed (5000 ))
167+ @OptIn(ExperimentalCoroutinesApi ::class )
168+ private val flow = liveDataAgent.flatMapLatest { it }.combine(sortType) { it, st ->
169+ it to ArrayList (it).apply {
170+ val cmp = sorter.getComparator(st)
171+ if (st == Sorter .Type .NativeOrderDescending ) {
172+ reverse()
173+ } else if (st != Sorter .Type .NativeOrder ) {
174+ sortWith { o1, o2 ->
175+ if (isPinned(o1) && ! isPinned(o2)) - 1
176+ else if (! isPinned(o1) && isPinned(o2)) 1
177+ else cmp?.compare(o1, o2) ? : 0
178+ }
179+ }
180+ }.toList()
181+ }
179182 val sortTypes: Set <Sorter .Type >
180183 get() = if (canSort) sorter.getSupportedTypes() else setOf (Sorter .Type .None )
181184
182185 init {
183- updateListInternal(runBlocking { flow.first() }, now = true , canDiff = false )
186+ list = runBlocking { flow.first() }
187+ onListUpdated()
184188 layoutType =
185189 if (prefLayoutType != LayoutType .NONE && prefLayoutType != defaultLayoutType && ! isSubFragment)
186190 prefLayoutType
@@ -210,11 +214,27 @@ abstract class BaseAdapter<T>(
210214 applyLayoutManager()
211215 }
212216 }
213- this .scope = CoroutineScope (Dispatchers .Default )
214- this .scope!! .launch {
217+ if (scope != null )
218+ throw IllegalStateException (" scope != null in onAttachedToRecyclerView" )
219+ scope = CoroutineScope (Dispatchers .Default )
220+ scope!! .launch {
215221 flow.collect {
216- withContext(Dispatchers .Main ) {
217- updateListInternal(it, now = false , canDiff = true )
222+ // The replay cache may cause us seeing the same list more than one. Make sure to
223+ // use === (reference equals) to avoid performance hit.
224+ if (list == = it) return @collect
225+ val diff = if ((list.second.isNotEmpty<T >() && it.second.isNotEmpty<T >())
226+ || allowDiffUtils)
227+ DiffUtil .calculateDiff(SongDiffCallback (list.second, it.second))
228+ else null
229+ val sizeChanged = list.second.size != it.second.size
230+ withContext(Dispatchers .Main + NonCancellable ) {
231+ list = it
232+ if (diff != null )
233+ diff.dispatchUpdatesTo(this @BaseAdapter)
234+ else
235+ notifyDataSetChanged()
236+ if (sizeChanged) decorAdapter.updateSongCounter()
237+ onListUpdated()
218238 }
219239 }
220240 }
@@ -225,8 +245,8 @@ abstract class BaseAdapter<T>(
225245 if (layoutType == LayoutType .GRID ) {
226246 recyclerView.removeItemDecoration(gridPaddingDecoration)
227247 }
228- this . scope!! .cancel()
229- this . scope = null
248+ scope!! .cancel()
249+ scope = null
230250 this .recyclerView = null
231251 if (ownsView) {
232252 recyclerView.layoutManager = null
@@ -246,7 +266,7 @@ abstract class BaseAdapter<T>(
246266 recyclerView?.scrollToPosition(scrollPosition)
247267 }
248268
249- override fun getItemCount (): Int = list.size
269+ override fun getItemCount (): Int = list.second. size
250270
251271 override fun onCreateViewHolder (
252272 parent : ViewGroup ,
@@ -258,64 +278,6 @@ abstract class BaseAdapter<T>(
258278
259279 fun sort (selector : Sorter .Type ) {
260280 sortType.value = selector
261- // updateListInternal(null, now = false, canDiff = true) TODO
262- }
263-
264- @SuppressLint(" NotifyDataSetChanged" )
265- private suspend fun sort (srcList : List <T >, canDiff : Boolean ) {
266- val newList = ArrayList (srcList)
267- if (! listLock.tryAcquire()) {
268- throw IllegalStateException (" listLock already held, add now = true to the caller (I am ${javaClass.name} )" )
269- }
270- try {
271- val diff = withContext(Dispatchers .Default ) {
272- val st = sortType.first()
273- val cmp = comparator.first()
274- if (st == Sorter .Type .NativeOrderDescending ) {
275- newList.reverse()
276- } else if (st != Sorter .Type .NativeOrder ) {
277- newList.sortWith { o1, o2 ->
278- if (isPinned(o1) && ! isPinned(o2)) - 1
279- else if (! isPinned(o1) && isPinned(o2)) 1
280- else cmp?.compare(o1, o2) ? : 0
281- }
282- }
283- if (((list.isNotEmpty() && newList.isNotEmpty()) || allowDiffUtils) && canDiff)
284- DiffUtil .calculateDiff(SongDiffCallback (list, newList)) else null
285- }
286- list.clear()
287- list.addAll(newList)
288- if (diff != null )
289- diff.dispatchUpdatesTo(this @BaseAdapter)
290- else
291- notifyDataSetChanged()
292- if (list.size != newList.size) decorAdapter.updateSongCounter()
293- onListUpdated()
294- } finally {
295- listLock.release()
296- }
297- }
298-
299- fun updateListInternal (newList : List <T >? = null, now : Boolean , canDiff : Boolean ) {
300- // The replay cache may cause us seeing the same list more than one.
301- if (newList != null ) {
302- if (lastList == = newList) return
303- lastList = newList
304- }
305- val list = lastList
306- if (list == null )
307- throw IllegalArgumentException (" updateListInternal called with null value but no value is cached" )
308- if (now || scope == null ) {
309- runBlocking {
310- sort(list, canDiff)
311- }
312- } else {
313- scope!! .launch {
314- withContext(Dispatchers .Main ) {
315- sort(list, canDiff)
316- }
317- }
318- }
319281 }
320282
321283 protected open fun onListUpdated () {}
@@ -346,7 +308,7 @@ abstract class BaseAdapter<T>(
346308 }
347309 }
348310 }
349- val item = list[position]
311+ val item = list.second [position]
350312 holder.title.text = titleOf(item) ? : virtualTitleOf(item)
351313 holder.subTitle.text = subTitleOf(item)
352314 holder.songCover.load(coverOf(item)) {
@@ -466,7 +428,7 @@ abstract class BaseAdapter<T>(
466428 }
467429
468430 protected fun toRawPos (item : T ): Int {
469- return lastList !! .indexOf(item)
431+ return list.first .indexOf(item)
470432 }
471433
472434 final override fun getPopupText (view : View , position : Int ): CharSequence {
@@ -475,7 +437,7 @@ abstract class BaseAdapter<T>(
475437 // if this crashes with IndexOutOfBoundsException, list access isn't guarded enough?
476438 // lib only ever gets popup text for what RecyclerView believes to be the first view
477439 return (if (position >= 1 )
478- sorter.getFastScrollHintFor(list[position - 1 ], sortType.value)
440+ sorter.getFastScrollHintFor(list.second [position - 1 ], sortType.value)
479441 else null ) ? : " -"
480442 }
481443
0 commit comments