Commit 04391fb5 authored by Christophe Henry's avatar Christophe Henry Committed by Christophe Henry

Fix ViewModel not retained in memory

parent 693a3371
......@@ -12,6 +12,7 @@
* Fix a bug preventing to refresh when subscription lists are empty ([!62](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/62/))
* Fix [#82](https://git.feneas.org/christophehenry/freshrss-android/issues/82): home and back buttons are sometimes not displayed correctly or not displayed at all ([!71](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/71))
* Fix [#80](https://git.feneas.org/christophehenry/freshrss-android/issues/80): multiple performance bugs in the subscriptions and subscription's articles pages display ([!65](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/65))
## Refactoring
......@@ -105,4 +106,4 @@ Basic implementation using FreshRSS' GReader HTTP API implmentation. This lets y
* set an article as read/unread
* share an article to other android application
* open the original page of the article
* refresh your feed
\ No newline at end of file
* refresh your feed
......@@ -27,8 +27,8 @@ import nl.komponents.kovenant.ui.failUi
class SubscriptionArticlesDetailFragment : Fragment() {
private lateinit var articleId: String
private val model: SubscriptionArticleVM by lazy {
ViewModelProvider(this, SubscriptionArticleVMF(articleId))
.get(SubscriptionArticleVM::class.java)
ViewModelProvider(activity!!.viewModelStore, SubscriptionArticleVMF(articleId))
.get("articleId=$articleId", SubscriptionArticleVM::class.java)
}
private var isFetching = MutableLiveData<Boolean>().apply { value = false }
private val article: Article get() = model.liveData.value!!
......@@ -108,7 +108,7 @@ class SubscriptionArticlesDetailFragment : Fragment() {
}
mutateUi(article)
model.liveData.observe(this, Observer(::mutateUi))
model.liveData.observe(viewLifecycleOwner, Observer(::mutateUi))
it.setOnMenuItemClickListener { setReadStatus(article.readStatus.toggle()) }
}
}
......
......@@ -3,7 +3,8 @@ package fr.chenry.android.freshrss.components.subscriptionarticles
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.*
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
......@@ -28,9 +29,9 @@ class SubscriptionArticlesFragment: Fragment(), Observer<List<SubscriptionArticl
private val readStatus: ReadStatus by lazy {args.readStatus}
private val model: SubscriptionArticlesVM by lazy {
ViewModelProvider(this, SubscriptionArticlesVMF(streamId, readStatus))
.get(SubscriptionArticlesVM::class.java).apply {
liveData.observe(this@SubscriptionArticlesFragment, this@SubscriptionArticlesFragment)
ViewModelProvider(activity!!.viewModelStore, SubscriptionArticlesVMF(streamId, readStatus))
.get("stream=$streamId:readStatus=$readStatus", SubscriptionArticlesVM::class.java).apply {
liveData.observe(viewLifecycleOwner, this@SubscriptionArticlesFragment)
}
}
......
......@@ -14,29 +14,27 @@ import eu.davidea.fastscroller.FastScroller
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection.*
import fr.chenry.android.freshrss.components.subscriptions.adapters.SubscriptionViewItems
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.store.viewmodels.SubscriptionsFragmentCategoryVMF
import fr.chenry.android.freshrss.store.viewmodels.SubscriptionsVM
import fr.chenry.android.freshrss.utils.EmotionnalImageSubtext
import kotlinx.android.synthetic.main.fragment_subscriptions.*
import nl.komponents.kovenant.Promise
class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
class SubscriptionsFragment: Fragment(), Observer<SubscriptionViewItems> {
private val args: SubscriptionsFragmentArgs by navArgs()
private val section by lazy {args.section}
private val category by lazy {args.category}
val model by lazy {
if(category != VoidCategory)
ViewModelProvider(this, SubscriptionsFragmentCategoryVMF(category, section))
.get(AllSubscriptionsVM::class.java)
else when(section) {
ALL -> ViewModelProvider(this).get(AllSubscriptionsVM::class.java)
UNREAD -> ViewModelProvider(this).get(UnreadSubscriptionsVM::class.java)
FAVORITES -> ViewModelProvider(this).get(FavoritesSubscriptionsVM::class.java)
}
val key = "section=${section.name}${if(category != VoidCategory) ":category=${category.id}" else ""}"
ViewModelProvider(activity!!.viewModelStore, SubscriptionsFragmentCategoryVMF(category, section))
.get(key, SubscriptionsVM::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
......@@ -46,7 +44,7 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
Store.refreshingPromise.observe(
viewLifecycleOwner,
Observer {toggleProgressCircle(model.subscriptions.value ?: listOf())})
Observer {toggleProgressCircle(model.isEmpty())})
model.subscriptions.observe(viewLifecycleOwner, this)
val contentDescription = if(category != VoidCategory) {
......@@ -86,7 +84,8 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
return view
}
override fun onChanged(subscriptions: Subscriptions?) = toggleProgressCircle((subscriptions ?: listOf()))
override fun onChanged(subscriptions: SubscriptionViewItems?)
= toggleProgressCircle((subscriptions.isNullOrEmpty()))
fun onClick(subscription: Subscription) = when(section) {
FAVORITES -> subscription.id to READ
......@@ -117,8 +116,8 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
}
}
private fun toggleProgressCircle(subscriptions: Subscriptions) {
if(subscriptions.isNotEmpty()) {
private fun toggleProgressCircle(isEmpty: Boolean) {
if(!isEmpty) {
fragment_subscriptions_list.visibility = View.VISIBLE
fast_scroller.visibility = View.VISIBLE
......
......@@ -14,6 +14,7 @@ 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
typealias SubscriptionViewItems = List<AbstractSubscriptionViewItem>
sealed class AbstractSubscriptionViewItem(header: SubscriptionViewHeaderItem) :
AbstractSectionableItem<SubscriptionViewHolder, SubscriptionViewHeaderItem>(header) {
abstract val virtualSubscription: Subscription
......
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.SubscriptionCategories
import fr.chenry.android.freshrss.store.database.models.Subscriptions
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
class SubscriptionsFlexibleAdapter(private val fragment: SubscriptionsFragment) :
FlexibleAdapter<AbstractSubscriptionViewItem>(listOf(), null, true),
OnItemClickListener {
private val mediator = MediatorLiveData<List<AbstractSubscriptionViewItem>>()
private val subscriptionCategories
get() = fragment.model.subscriptionCategories.value ?: listOf()
private val subscriptions
get() = fragment.model.subscriptions.value ?: listOf()
private val grouper get() = fragment.model.grouper
init {
setStickyHeaders(true)
setDisplayHeadersAtStartUp(true)
mediator.observe(fragment.viewLifecycleOwner, Observer { updateDataSet(it, true) })
mediator.addSource(fragment.model.subscriptions) {
computeNewDataSet(it, subscriptionCategories) success { res -> mediator.postValue(res) }
}
mediator.addSource(fragment.model.subscriptionCategories) {
computeNewDataSet(subscriptions, it) success { res -> mediator.postValue(res) }
}
fragment.model.subscriptions.observe(fragment.viewLifecycleOwner, Observer { updateDataSet(it, true) })
addListener(this)
}
override fun isEmpty(): Boolean = fragment.model.subscriptions.value.isNullOrEmpty()
override fun isEmpty(): Boolean = fragment.model.isEmpty()
override fun onCreateBubbleText(position: Int): String? {
return when (val item = getGenericItem(position)) {
......@@ -55,26 +39,6 @@ class SubscriptionsFlexibleAdapter(private val fragment: SubscriptionsFragment)
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
}
override fun getItemId(position: Int): Long = getGenericItem(position)?.let {
when (it) {
is SubscriptionViewItem -> it.subscription.id.hashCode().toLong()
......
......@@ -4,13 +4,7 @@ import android.graphics.Bitmap
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.databinding.library.baseAdapters.BR
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.*
import com.squareup.picasso.Picasso
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.api.models.SubscriptionApiItem
......@@ -29,7 +23,7 @@ data class Subscription(
val unreadCount: Int = 0,
val subscriptionCategories: List<String> = listOf(),
val newestArticleDate: LocalDateTime = LocalDateTime(0)
) : BaseObservable() {
) : BaseObservable(), SubscriptionDisplayable {
@get:Bindable
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var imageBitmap: Bitmap? = null
......
package fr.chenry.android.freshrss.store.database.models
import android.os.Parcelable
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.*
import fr.chenry.android.freshrss.store.api.models.SubscriptionCategoryApiItem
import io.reactivex.Flowable
import kotlinx.android.parcel.Parcelize
......@@ -19,7 +14,7 @@ data class SubscriptionCategory(
@PrimaryKey
val id: String,
val label: String
) : Parcelable {
) : Parcelable, SubscriptionDisplayable {
companion object {
fun fromApiItem(item: SubscriptionCategoryApiItem) = SubscriptionCategory(item.id, item.label)
}
......
package fr.chenry.android.freshrss.store.database.models
interface SubscriptionDisplayable
typealias SubscriptionDisplayables = List<SubscriptionDisplayable>
package fr.chenry.android.freshrss.store.viewmodels
import android.annotation.SuppressLint
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory
import org.joda.time.LocalDateTime
object LabelComparator: Comparator<SubscriptionCategory> {
override fun compare(o1: SubscriptionCategory, o2: SubscriptionCategory): Int =
o1.label.compareTo(o2.label, true)
}
object TitleComparator: Comparator<Subscription> {
override fun compare(o1: Subscription, o2: Subscription): Int =
o1.title.compareTo(o2.title, true)
}
object DateComparator: Comparator<Subscription> {
override fun compare(o1: Subscription, o2: Subscription): Int =
if(o1.newestArticleDate.isEqual(o2.newestArticleDate))
o1.title.compareTo(o2.title, true)
else
o2.newestArticleDate.compareTo(o1.newestArticleDate)
}
interface Grouper {
operator fun invoke(subscription: Subscription): String
}
object TitleGrouper: Grouper {
@SuppressLint("DefaultLocale")
override operator fun invoke(subscription: Subscription): String =
subscription.title[0].toString().toUpperCase()
}
object DateGrouper: Grouper {
override operator fun invoke(subscription: Subscription): String {
val today = LocalDateTime.now().withTime(0, 0, 0, 0)
return when {
subscription.newestArticleDate.isAfter(today.minusDays(1)) ->
R.string.human_time_grouping_today
subscription.newestArticleDate.isAfter(today.minusDays(2)) ->
R.string.human_time_grouping_yesterday
subscription.newestArticleDate.isAfter(today.withDayOfWeek(1)) ->
R.string.human_time_grouping_this_week
subscription.newestArticleDate.isAfter(today.withDayOfWeek(1).minusWeeks(1)) ->
R.string.human_time_grouping_last_week
subscription.newestArticleDate.isAfter(today.withDayOfMonth(1)) ->
R.string.human_time_grouping_this_month
subscription.newestArticleDate.isAfter(today.withDayOfMonth(1).minusMonths(1)) ->
R.string.human_time_grouping_last_month
subscription.newestArticleDate.isAfter(today.withDayOfYear(1)) ->
R.string.human_time_grouping_this_year
else -> R.string.human_time_grouping_old_articles
}.let {id -> F.getString(id)}
}
}
......@@ -4,6 +4,8 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.joda.JodaModule
......@@ -12,6 +14,7 @@ import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.*
import com.github.kittinunf.fuel.jackson.responseObject
import fr.chenry.android.freshrss.store.database.models.Account
import kotlinx.coroutines.*
import nl.komponents.kovenant.Kovenant.deferred
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
......@@ -110,3 +113,16 @@ fun URL.queryParameters(): Map<String, String> = this.query
?.split("&")
?.map {it.split("=").let {split -> split[0] to split[1]}}
?.toMap() ?: mapOf()
fun <T> LiveData<T>.debounce(scope: CoroutineScope, duration: Long = 1000L) = MediatorLiveData<T>().also {mld ->
val source = this
var job: Job? = null
mld.addSource(source) {
job?.cancel()
job = scope.launch {
delay(duration)
mld.value = source.value
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment