Commit 83ac0c15 authored by Christophe Henry's avatar Christophe Henry

Merge branch 'refacto-subscriptions-adapter' into 'develop'

Refactor recycler view adapters to use FlexibleAdapter and drop SectionnedRecyclerViewAdapter

Closes #51 and #50

See merge request !17
parents 8b7e3871 c0a077fd
# Development
## Features
* Implements [#54](https://git.feneas.org/christophehenry/freshrss-android/issues/54): Add fast scroller bar with section display ([!17](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/17))
## Bug fixes
* [#50](https://git.feneas.org/christophehenry/freshrss-android/issues/50): Categories are not alphabetically sorted
* [#51](https://git.feneas.org/christophehenry/freshrss-android/issues/51): Unread subscription time groups are not time-sorted
# 1.1.0
## Features
......@@ -18,7 +27,7 @@
* Fix service lately binded to application causing stacktrace ([a58c00dd](https://git.feneas.org/christophehenry/freshrss-android/commit/a58c00dd8c8c6e292a0210a64a38238b253978a4))
* Fix loader displaying infinitely when a subscription section stays empty after refresh by displaying a hint text stating section is empty ([13b7c02c](https://git.feneas.org/christophehenry/freshrss-android/commit/13b7c02c28bfdfd543de668006dff161330e3b75))
* Fix [#38](https://git.feneas.org/christophehenry/freshrss-android/issues/38]: empty screen when going back from the initial screen ([4f84e6b5](https://git.feneas.org/christophehenry/freshrss-android/commit/4f84e6b5f6de7d42a24d541e099a402d034792da))
* Fix [#38](<https://git.feneas.org/christophehenry/freshrss-android/issues/38>]: empty screen when going back from the initial screen ([4f84e6b5](https://git.feneas.org/christophehenry/freshrss-android/commit/4f84e6b5f6de7d42a24d541e099a402d034792da))
* Fix [#13](https://git.feneas.org/christophehenry/freshrss-android/issues/13) and [#14](https://git.feneas.org/christophehenry/freshrss-android/issues/14): erratic notification behavior ([0c1b5e76](https://git.feneas.org/christophehenry/freshrss-android/commit/0c1b5e7600888d905f67dc28b0389229b52b67f7))
* Fix crash hapening when object is returned from `unread-counts` endpoint without `newestItemTimestampUsec` property ([8cfcd932](https://git.feneas.org/christophehenry/freshrss-android/commit/8cfcd932619b601371b6f0d0a4bcb7d4b92ce3dd))
* Fix spinner infinitely loading when comming back from a feed with a single unread article ([3088922f](https://git.feneas.org/christophehenry/freshrss-android/commit/3088922f9405ee173d6a6cb81b8868a35759aedd))
......
......@@ -125,8 +125,10 @@ dependencies {
implementation "org.apache.commons:commons-text:1.4"
implementation "joda-time:joda-time:2.10.1"
implementation "com.squareup.picasso:picasso:2.71828"
implementation "io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:2.0.0"
implementation "com.x5dev:chunk-templates:3.4.0"
implementation "eu.davidea:flexible-adapter:5.1.0"
implementation "eu.davidea:flexible-adapter-ui:1.0.0"
// Tests
testImplementation "junit:junit:4.12"
......
......@@ -5,6 +5,8 @@ import android.app.Service
import android.content.Intent
import android.os.Binder
import androidx.core.app.NotificationCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.utils.NotificationChanels
import fr.chenry.android.freshrss.utils.NotificationHelper
......
......@@ -16,6 +16,7 @@ import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.VoidCategory
import fr.chenry.android.freshrss.utils.whenNotNull
import kotlinx.android.synthetic.main.fragment_main_subscription.*
import kotlinx.android.synthetic.main.fragment_subscriptions.*
import kotlinx.android.synthetic.main.menu_badge.*
import nl.komponents.kovenant.ui.alwaysUi
......@@ -30,12 +31,6 @@ class MainSubscriptionFragment: Fragment(), BottomNavigationView.OnNavigationIte
setupBadgeCount()
subcription_pull_to_refresh.setOnRefreshListener {
FreshRSSApplication.application.refresherService.value.whenNotNull {
it.refresh().alwaysUi {subcription_pull_to_refresh.isRefreshing = false}
}
}
subcription_fragment_container.adapter = adapter
subcription_fragment_container.offscreenPageLimit = SubscriptionSection.values().size
......
package fr.chenry.android.freshrss.components.subscriptions
import android.view.View
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.*
import fr.chenry.android.freshrss.*
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.databinding.FragmentSubscriptionBinding
import fr.chenry.android.freshrss.store.database.models.*
import io.github.luizgrp.sectionedrecyclerviewadapter.*
class RecyclerViewAdapter(private val fragment: SubscriptionsFragment): SectionedRecyclerViewAdapter() {
private val categorySectionTag = Char.MIN_VALUE.toString()
init {
addSection(categorySectionTag, RecyclerViewAdapterSection2(listOf()))
fragment.model.sectionnedLiveData.observe(fragment, Observer {
it.entries.forEach {self ->
if(getSection(self.key) == null) addSection(self.key, RecyclerViewAdapterSection(self.key, self.value))
else {
val oldItems: Subscriptions = getSection(self.key)
?.let {s ->
s as RecyclerViewAdapterSection
val result = s.subscriptions
s.subscriptions = it[self.key] ?: listOf()
result
} ?: listOf()
val newItems: Subscriptions = it[self.key] ?: oldItems
DiffUtil
.calculateDiff(SectionDiffUtilsCallback(oldItems, newItems))
.dispatchUpdatesTo(SectionUpdateCallback(self.key))
}
}
})
fragment.model.subscriptionCategoriesLiveData.observe(fragment, Observer {
val oldItems = getSection(categorySectionTag)
?.let {s ->
s as RecyclerViewAdapterSection2
val result = s.subscriptionCategories
s.subscriptionCategories = it
result
} ?: listOf()
if(getSection(categorySectionTag).hasHeader() != it.isNotEmpty()) {
getSection(categorySectionTag).setHasHeader(it.isNotEmpty())
if(it.isNotEmpty()) notifyHeaderInsertedInSection(categorySectionTag)
else notifyHeaderRemovedFromSection(categorySectionTag)
}
DiffUtil
.calculateDiff(SectionDiffUtilsCallback(oldItems, it))
.dispatchUpdatesTo(SectionUpdateCallback(categorySectionTag))
})
}
inner class ViewHolder(val binding: FragmentSubscriptionBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(subscription: Subscription) {
binding.setVariable(BR.subscription, subscription)
binding.executePendingBindings()
}
}
inner class RecyclerViewAdapterSection(private val sectionTag: String, var subscriptions: Subscriptions):
StatelessSection(getSectionParameters()) {
override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val subscription = subscriptions[position]
(holder as ViewHolder).apply {
bind(subscription)
binding.root.setOnClickListener {fragment.onClick(subscription)}
}
}
override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder) {
holder.itemView.findViewById<TextView>(R.id.subscription_section_header_title).text = sectionTag
}
override fun getItemViewHolder(view: View) = ViewHolder(DataBindingUtil.bind(view)!!)
override fun getContentItemsTotal() = subscriptions.size
}
inner class RecyclerViewAdapterSection2(var subscriptionCategories: SubscriptionCategories):
StatelessSection(getSectionParameters()) {
override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val subscriptionCategory = subscriptionCategories[position]
(holder as ViewHolder).apply {
bind(Subscription("", subscriptionCategory.label, "", -1, listOf()))
binding.root.setOnClickListener {fragment.onClick(subscriptionCategory)}
}
}
override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder) {
holder.itemView.findViewById<TextView>(R.id.subscription_section_header_title).text =
FreshRSSApplication.getStringR(R.string.subscription_categories)
}
override fun getItemViewHolder(view: View) = ViewHolder(DataBindingUtil.bind(view)!!)
override fun getContentItemsTotal() = subscriptionCategories.size
}
inner class SectionUpdateCallback(private val sectionTag: String): ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) =
notifyItemChangedInSection(sectionTag, position)
override fun onMoved(fromPosition: Int, toPosition: Int) =
notifyItemMovedInSection(sectionTag, fromPosition, toPosition)
override fun onInserted(position: Int, count: Int) = notifyItemInsertedInSection(sectionTag, position)
override fun onRemoved(position: Int, count: Int) = notifyItemRemovedFromSection(sectionTag, position)
}
inner class SectionDiffUtilsCallback<T>(private val oldItems: List<T>, private val newItems: List<T>):
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems.getOrNull(oldItemPosition)
val newitem = newItems.getOrNull(newItemPosition)
return oldItem != null && newitem != null && oldItem.hashCode() == newitem.hashCode()
}
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldItems.getOrNull(oldItemPosition)?.equals(newItems.getOrNull(newItemPosition)) ?: false
}
companion object {
fun getSectionParameters(): SectionParameters =
SectionParameters.builder()
.headerResourceId(R.layout.fragment_subscription_header)
.itemResourceId(R.layout.fragment_subscription)
.build()
}
}
......@@ -10,18 +10,25 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import eu.davidea.fastscroller.FastScroller
import fr.chenry.android.freshrss.*
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection.*
import fr.chenry.android.freshrss.components.subscriptions.adapters.SubscriptionsFlexibleAdapter
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.viewmodels.*
import fr.chenry.android.freshrss.utils.whenNotNull
import kotlinx.android.synthetic.main.fast_scroller.*
import kotlinx.android.synthetic.main.fragment_subscriptions.*
import nl.komponents.kovenant.Promise
class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
private val args: SubscriptionsFragmentArgs by navArgs()
private val section by lazy {args.section}
private val category by lazy {args.category}
private val adapter by lazy {SubscriptionsFlexibleAdapter(this)}
val model by lazy {
if(category != VoidCategory)
......@@ -35,17 +42,21 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
}
}
private val adapter by lazy {RecyclerViewAdapter(this)}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_subscriptions, container, false)
Store.refreshingPromise.observe(this, Observer {toggleProgressCircle(model.liveData.value ?: listOf())})
model.liveData.observe(this, this)
view.findViewById<RecyclerView>(R.id.subscriptions_list).let {
view.findViewById<RecyclerView>(R.id.fragment_subscriptions_list).let {
it.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
it.adapter = this.adapter
it.adapter = adapter
it.addOnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom ->
if(top == oldTop && bottom == oldBottom) return@addOnLayoutChangeListener
fast_scroller.visibility =
if(fragment_subscriptions_list.height >= fragment_subscriptions_list_container.height) View.VISIBLE
else View.GONE
}
}
val contentDescription = if(category != VoidCategory)
......@@ -59,16 +70,25 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
view.findViewById<TextView>(R.id.fragment_subscriptions_empty_list).text =
FreshRSSApplication.getStringR(R.string.empty_subscription_section_template, contentDescription)
return view
}
view.findViewById<FastScroller>(R.id.fast_scroller).let {
it.isAutoHideEnabled = false
adapter.fastScroller = it
}
override fun onChanged(subscriptions: Subscriptions?) {
(subscriptions ?: listOf()).let {
toggleProgressCircle(it)
adapter.notifyDataSetChanged()
view.findViewById<SwipeRefreshLayout>(R.id.subcription_pull_to_refresh)?.let {
Store.refreshingPromise.observe(this, Observer<Promise<Unit, Exception>?> {v ->
it.isRefreshing = v != null
})
it.setOnRefreshListener {
FreshRSSApplication.application.refresherService.value.whenNotNull {rs -> rs.refresh()}
}
}
return view
}
override fun onChanged(subscriptions: Subscriptions?) = toggleProgressCircle((subscriptions ?: listOf()))
fun onClick(subscription: Subscription) = when(section) {
FAVORITES -> MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
ALL -> MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
......@@ -81,11 +101,13 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
private fun toggleProgressCircle(subscriptions: Subscriptions) {
if(Store.refreshingPromise.value == null) {
subscriptions_list.visibility = if(subscriptions.isNotEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_list_container.visibility =
if(subscriptions.isNotEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_empty_list.visibility = if(subscriptions.isEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_waiting.visibility = View.GONE
} else {
subscriptions_list.visibility = if(subscriptions.isNotEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_list_container.visibility =
if(subscriptions.isNotEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_waiting.visibility = if(subscriptions.isEmpty()) View.VISIBLE else View.GONE
fragment_subscriptions_empty_list.visibility = View.GONE
}
......
package fr.chenry.android.freshrss.components.subscriptions.adapters
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import fr.chenry.android.freshrss.BR
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.adapters.AbstractSubscriptionViewItem.SubscriptionViewHolder
import fr.chenry.android.freshrss.databinding.FragmentSubscriptionBinding
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory
sealed class AbstractSubscriptionViewItem(header: SubscriptionViewHeaderItem):
AbstractSectionableItem<SubscriptionViewHolder, SubscriptionViewHeaderItem>(header) {
abstract val virtualSubscription: Subscription
init {
isDraggable = false
isSelectable = false
isSwipeable = false
}
override fun createViewHolder(
view: View?,
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?
): SubscriptionViewHolder = SubscriptionViewHolder(DataBindingUtil.bind(view!!)!!, adapter)
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?,
holder: SubscriptionViewHolder?, position: Int,
payloads: MutableList<Any>?
) = holder?.bind(virtualSubscription).let {Unit}
override fun getLayoutRes() = R.layout.fragment_subscription
inner class SubscriptionViewHolder(
private val binding: FragmentSubscriptionBinding,
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?
): FlexibleViewHolder(binding.root, adapter) {
fun bind(subscription: Subscription) {
binding.setVariable(BR.subscription, subscription)
binding.executePendingBindings()
}
}
}
class SubscriptionViewItem(val subscription: Subscription, header: String):
AbstractSubscriptionViewItem(SubscriptionViewHeaderItem(header)) {
override val virtualSubscription: Subscription get() = subscription
override fun equals(other: Any?): Boolean =
if(other !is SubscriptionViewItem) false else subscription == other.subscription
override fun hashCode(): Int = subscription.hashCode()
}
class SubscriptionCategoryViewItem(val subscriptionCategory: SubscriptionCategory):
AbstractSubscriptionViewItem(SubscriptionCategoryViewHeaderItem) {
override val virtualSubscription: Subscription =
Subscription(subscriptionCategory.id, subscriptionCategory.label, "", unreadCount = -1)
override fun equals(other: Any?): Boolean =
if(other !is SubscriptionCategoryViewItem) false else subscriptionCategory == other.subscriptionCategory
override fun hashCode(): Int = subscriptionCategory.hashCode()
}
\ No newline at end of file
package fr.chenry.android.freshrss.components.subscriptions.adapters
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
open class SubscriptionViewHeaderItem(val title: String):
AbstractHeaderItem<SubscriptionViewHeaderItem.SubscriptionViewHeaderHolder>() {
init {
isDraggable = false
isSelectable = false
isSwipeable = false
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?,
holder: SubscriptionViewHeaderHolder,
position: Int,
payloads: MutableList<Any>?
) = holder.bind(title)
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<ViewHolder>>?):
SubscriptionViewHeaderHolder =
SubscriptionViewHeaderHolder(view!!, adapter)
override fun getLayoutRes(): Int = R.layout.fragment_subscription_header
override fun equals(other: Any?): Boolean =
if(other !is SubscriptionViewHeaderItem) false else title == other.title
override fun hashCode(): Int = title.hashCode()
inner class SubscriptionViewHeaderHolder(
val view: View,
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?
): FlexibleViewHolder(view, adapter, true) {
fun bind(header: String) {
view.findViewById<TextView>(R.id.subscription_section_header_title).text = header
}
}
}
object SubscriptionCategoryViewHeaderItem:
SubscriptionViewHeaderItem(FreshRSSApplication.getStringR(R.string.subscription_categories))
\ No newline at end of file
package fr.chenry.android.freshrss.components.subscriptions.adapters
import android.view.View
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener
import eu.davidea.flexibleadapter.items.IFlexible
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionsFragment
import fr.chenry.android.freshrss.store.database.models.*
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
class SubscriptionsFlexibleAdapter(private val fragment: SubscriptionsFragment):
FlexibleAdapter<AbstractSubscriptionViewItem>(initFlexAdapter(fragment)), OnItemClickListener {
private val mediator = MediatorLiveData<List<AbstractSubscriptionViewItem>>()
private val subscriptionCategories
get() = fragment.model.subscriptionCategoriesLiveData.value ?: listOf()
private val subscriptions
get() = fragment.model.liveData.value ?: listOf()
private val grouper get() = fragment.model.grouper
init {
setStickyHeaders(true)
setDisplayHeadersAtStartUp(true)
mediator.observe(fragment, Observer {updateDataSet(it)})
mediator.addSource(fragment.model.liveData) {
computeNewDataSet(it, subscriptionCategories) success {res -> mediator.postValue(res)}
}
mediator.addSource(fragment.model.subscriptionCategoriesLiveData) {
computeNewDataSet(subscriptions, it) success {res -> mediator.postValue(res)}
}
addListener(this)
}
override fun isEmpty(): Boolean = fragment.model.liveData.value.isNullOrEmpty()
override fun onCreateBubbleText(position: Int): String? {
val item = getGenericItem(position)
return when(item) {
is AbstractSubscriptionViewItem -> item.header.title
is SubscriptionViewHeaderItem -> item.title
else -> null
}
}
override fun onItemClick(view: View?, position: Int): Boolean = getGenericItem(position)?.let {
when(it) {
is SubscriptionViewItem -> fragment.onClick(it.subscription)
is SubscriptionCategoryViewItem -> fragment.onClick(it.subscriptionCategory)
}
true
} ?: false
// This is probably one of the stupidest API design I've ever met
// This is actually known and the author refuses to fix
// WTF: https://github.com/davideas/FlexibleAdapter/issues/604
private fun getGenericItem(position: Int): IFlexible<*>? = (this as FlexibleAdapter<*>).getItem(position)
private fun computeNewDataSet(
subscriptions: Subscriptions,
subscriptionCategories: SubscriptionCategories
): Promise<ArrayList<AbstractSubscriptionViewItem>, Exception> = task {
val targetSize = subscriptionCategories.size + subscriptions.size
val result = ArrayList<AbstractSubscriptionViewItem>(targetSize)
(0 until targetSize).forEach {
val newElt = if(it < subscriptionCategories.size)
SubscriptionCategoryViewItem(subscriptionCategories[it]) else
SubscriptionViewItem(
subscriptions[it - subscriptionCategories.size],
grouper(subscriptions[it - subscriptionCategories.size])
)
result.add(newElt)
}
result
}
companion object {
private fun initFlexAdapter(fragment: SubscriptionsFragment) =
getSubscriptionViewItems(fragment.model.liveData.value ?: listOf(), fragment.model.grouper)
private fun getSubscriptionViewItems(subscriptions: Subscriptions, grouper: (Subscription) -> String) =
subscriptions.map {SubscriptionViewItem(it, grouper(it))}
}
}
......@@ -9,28 +9,21 @@ import fr.chenry.android.freshrss.store.database.models.*
import org.joda.time.LocalDateTime
sealed class SubscriptionsVM: ViewModel() {
abstract val subscriptionsLiveData: LiveData<Subscriptions>
abstract val subscriptionCategoriesLiveData: LiveData<SubscriptionCategories>
abstract val comparator: Comparator<Subscription>
abstract val grouper: (Subscription) -> String
protected abstract val comparator: Comparator<Subscription>
protected abstract val subscriptionsLiveData: LiveData<Subscriptions>
val liveData: LiveData<Subscriptions> by lazy {
MutableLiveData<Subscriptions>().apply {
value = load()
subscriptionsLiveData.observeForever {value = load()}
}
}
val sectionnedLiveData: LiveData<Map<String, Subscriptions>> by lazy {
MutableLiveData<Map<String, Subscriptions>>().apply {
liveData.observeForever {value = groupBy(it)}
value = (subscriptionsLiveData.value ?: listOf()).sortedWith(comparator)
subscriptionsLiveData.observeForever {value = it.sortedWith(comparator)}
}
}
private fun groupBy(subscriptions: Subscriptions): Map<String, Subscriptions> = subscriptions.groupBy(grouper)
private fun load(): Subscriptions = subscriptionsLiveData.value?.sortedWith(comparator) ?: listOf()
companion object {
val labelComparator = Comparator<SubscriptionCategory> {o1, o2 -> o1.label.compareTo(o2.label, true)}
val titleComparator = Comparator<Subscription> {o1, o2 -> o1.title.compareTo(o2.title, true)}
val dateComparator = Comparator<Subscription> {o1, o2 ->
if(o1.newestArticleDate.isEqual(o2.newestArticleDate))
......@@ -58,9 +51,21 @@ class AllSubscriptionsVM: SubscriptionsVM() {
override val comparator: Comparator<Subscription> get() = titleComparator
override val grouper: (Subscription) -> String get() = titleGrouper
override val subscriptionsLiveData = FreshRSSApplication.database.getAllSubcriptions().toLiveData()
override val subscriptionCategoriesLiveData by lazy {
FreshRSSApplication.database.getAllCategories().toLiveData()
override val subscriptionCategoriesLiveData: LiveData<SubscriptionCategories>
private val _subscriptionCategoriesLiveData: LiveData<SubscriptionCategories>
init {
val flowable = FreshRSSApplication.database.getAllCategories()
_subscriptionCategoriesLiveData = flowable.toLiveData()
subscriptionCategoriesLiveData = MutableLiveData<SubscriptionCategories>().apply {
value = flowable.blockingFirst().orEmpty().sortedWith(labelComparator)
}
_subscriptionCategoriesLiveData.observeForever {
subscriptionCategoriesLiveData.value = it.sortedWith(labelComparator)
}
}
}
class UnreadSubscriptionsVM: SubscriptionsVM() {
......@@ -72,7 +77,7 @@ class UnreadSubscriptionsVM: SubscriptionsVM() {
MutableLiveData<SubscriptionCategories>().apply {
val observer = Observer<Subscriptions> {
val categories = (it ?: listOf()).flatMap {self -> self.subscriptionCategories}
postValue(FreshRSSApplication.database.getCategoriesById(categories).blockingFirst())
postValue(FreshRSSApplication.database.getCategoriesById(categories).blockingFirst().sortedWith(labelComparator))
}
observer.onChanged(subscriptionsLiveData.value ?: listOf())
subscriptionsLiveData.observeForever(observer)
......
......@@ -17,6 +17,7 @@ class Try<T: Any>(body: () -> T) {
}
fun <U: Any>map(body: (Try<T>) -> U): Try<U> = Try{body(this)}
fun <U: Any>flatMap(body: () -> U): Try<U> = Try{body()}
fun <U: Any>whenSuccess(body: (T) -> U): Try<T> = if(isSuccess) body(value).let{this} else this
fun <U: Any>whenError(body: (Throwable) -> U): Try<T> = if(!isSuccess) body(error).let{this} else this
fun orElseDo(body: () -> Unit) = if(!isSuccess) body() else Unit
......
<?xml version="1.0" encoding="utf-8"?>
<eu.davidea.fastscroller.FastScroller
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_alignTop="@+id/subcription_pull_to_refresh"
android:layout_alignBottom="@+id/subcription_pull_to_refresh"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
tools:visibility="visible"
tools:ignore="RtlHardcoded" />
\ No newline at end of file
......@@ -8,20 +8,14 @@
android:layout_height="match_parent"
tools:context=".components.subscriptions.MainSubscriptionFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/subcription_pull_to_refresh"
tools:context=".components.subscriptions.SubscriptionsFragment"
<androidx.viewpager.widget.ViewPager
android:id="@+id/subcription_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/subcription_bottom_navigation">
<androidx.viewpager.widget.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/subcription_fragment_container" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
app:layout_constraintBottom_toTopOf="@id/subcription_bottom_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/subcription_bottom_navigation"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/subscription_section_header"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/subscription_section_header_title"
android:text="A"
android:background="@color/colorAccent"
android:layout_width="match_parent"
android:layout_height="wrap_content"