Commit a0a0b690 authored by Christophe Henry's avatar Christophe Henry

Refactor subscriptions view for cleaner code and better presentation

- Use only one fragment with different viewmodels
- Add a ViewPager to switch between subscription sections
- Sort all subscriptions by alphabetic title and unread subscriptions by newest item
parent 3539695f
......@@ -27,6 +27,7 @@ android {
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
exclude group: 'com.google.guava', module: 'listenablefuture'
}
applicationVariants.all { variant ->
......@@ -53,10 +54,6 @@ dependencies {
def android_support_version = "28.0.0"
def android_navigation = "1.0.0-rc02"
configurations {
all*.exclude group: 'com.google.guava', module: 'listenablefuture'
}
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
......@@ -70,6 +67,7 @@ dependencies {
implementation "com.android.support:support-core-utils:$android_support_version"
implementation "com.android.support:support-fragment:$android_support_version"
implementation "com.android.support:support-compat:$android_support_version"
implementation "com.android.support:support-core-ui:$android_support_version"
// AndroidX layout
implementation 'androidx.appcompat:appcompat:1.1.0-alpha02'
......
package fr.chenry.android.freshrss.components.subscriptions
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.*
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
class AllSubscriptionsFragment: SubscriptionsFragment() {
override val liveData: LiveData<Subscriptions> = MutableLiveData<Subscriptions>().apply {
this.value = Store.subscriptions.values
Store.subscriptions.observeForever {this.value = Store.subscriptions.values}
}
override fun onClick(subscription: Subscription) {
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
view?.findNavController()?.navigate(direction)
}
}
package fr.chenry.android.freshrss.components.subscriptions
import androidx.lifecycle.MutableLiveData
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
class FavoritesSubscriptionsFragment: SubscriptionsFragment() {
override val liveData = MutableLiveData<Subscriptions>().apply {this.value = listOf()}
override fun onClick(subscription: Subscription) {
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, UNREAD)
view?.findNavController()?.navigate(direction)
}
}
......@@ -3,71 +3,40 @@ package fr.chenry.android.freshrss.components.subscriptions
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.utils.whenNotNull
import kotlinx.android.synthetic.main.fragment_main_subscription.*
import kotlinx.android.synthetic.main.fragment_subscriptions.*
import kotlin.reflect.KClass
class MainSubscriptionFragment: Fragment() {
private val fragment: KClass<out SubscriptionsFragment>
get() = when(Store.subscriptionsSection.value) {
SubscriptionSection.ALL -> AllSubscriptionsFragment::class
SubscriptionSection.FAVORITES -> FavoritesSubscriptionsFragment::class
SubscriptionSection.UNREAD -> UnreadSubscriptionsFragment::class
else -> TODO("Handle bad section")
}
class MainSubscriptionFragment: Fragment(), BottomNavigationView.OnNavigationItemSelectedListener {
private val adapter by lazy {MainSubscriptionPagerAdapter()}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_main_subscription, container, false)
activity.whenNotNull {
it.supportFragmentManager
.beginTransaction()
.add(R.id.subcription_fragment_container, Fragment.instantiate(it, fragment.qualifiedName!!))
.commit()
}
return view
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_main_subscription, container, false)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
subcription_bottom_navigation.selectedItemId = when(Store.subscriptionsSection.value) {
SubscriptionSection.FAVORITES -> R.id.subscriptions_bottom_navigation_favorites
SubscriptionSection.UNREAD -> R.id.subscriptions_bottom_navigation_unread
else -> R.id.subscriptions_bottom_navigation_all
}
subcription_fragment_container.adapter = adapter
subcription_fragment_container.offscreenPageLimit = SubscriptionSection.values().size
subcription_bottom_navigation.setOnNavigationItemSelectedListener {
Store.subscriptionsSection.value = when(it.itemId) {
R.id.subscriptions_bottom_navigation_unread -> SubscriptionSection.UNREAD
R.id.subscriptions_bottom_navigation_favorites -> SubscriptionSection.FAVORITES
else -> SubscriptionSection.ALL
}
activity.whenNotNull {ac ->
ac.supportFragmentManager
.beginTransaction()
.replace(R.id.subcription_fragment_container, Fragment.instantiate(ac, fragment.qualifiedName!!))
.commit()
}
true
Store.subscriptionsSection.value!!.let {
Store.subscriptionsSection.value = it
subcription_bottom_navigation.selectedItemId = it.navigationButtonId
subcription_fragment_container.currentItem = it.ordinal
}
subcription_fragment_container.addOnPageChangeListener(adapter)
subcription_bottom_navigation.setOnNavigationItemSelectedListener(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
......@@ -78,33 +47,46 @@ class MainSubscriptionFragment: Fragment() {
super.onCreateOptionsMenu(menu, inflater)
}
}
abstract class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
abstract val liveData: LiveData<Subscriptions>
val subscriptions: Subscriptions get() = liveData.value ?: listOf()
private val adapter by lazy {RecyclerViewAdapter(this)}
override fun onNavigationItemSelected(it: MenuItem): Boolean {
SubscriptionSection.fromNavigationButton(it.itemId).let {
Store.subscriptionsSection.value = it
subcription_fragment_container.currentItem = it.ordinal
}
return true
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_subscriptions, container, false)
inner class MainSubscriptionPagerAdapter:
FragmentPagerAdapter(activity!!.supportFragmentManager),
OnPageChangeListener
{
private val allFragment by lazy(instantiateFragment(ALL))
private val unreadFragment by lazy(instantiateFragment(UNREAD))
private val favoritesFragment by lazy(instantiateFragment(FAVORITES))
override fun getItem(position: Int) = when(SubscriptionSection.byPosition(position)) {
FAVORITES -> favoritesFragment
ALL -> allFragment
UNREAD -> unreadFragment
}
liveData.observe(this, this)
override fun getCount() = SubscriptionSection.values().size
view.findViewById<RecyclerView>(R.id.subscriptions_list).let {
it.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
it.adapter = this.adapter
}
override fun onPageScrollStateChanged(state: Int) {}
return view
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onChanged(subscriptions: Subscriptions?) {
(subscriptions ?: listOf()).let {
subscriptions_list.visibility = if(it.isEmpty()) View.GONE else View.VISIBLE
fragment_subscriptions_waiting.visibility = if(it.isNotEmpty()) View.GONE else View.VISIBLE
adapter.notifyDataSetChanged()
override fun onPageSelected(position: Int) = SubscriptionSection.byPosition(position).let {
Store.subscriptionsSection.value = it
subcription_bottom_navigation.selectedItemId = it.navigationButtonId
}
}
abstract fun onClick(subscription: Subscription)
private fun instantiateFragment(subscriptionSection: SubscriptionSection) = {
Fragment.instantiate(
activity!!,
SubscriptionsFragment::class.qualifiedName!!,
Bundle().apply {putParcelable(SubscriptionsFragment.argumentKey, subscriptionSection)}
)
}
}
}
\ No newline at end of file
package fr.chenry.android.freshrss.components.subscriptions
import android.os.Parcelable
import fr.chenry.android.freshrss.R
import kotlinx.android.parcel.Parcelize
@Parcelize
enum class SubscriptionSection: Parcelable {
FAVORITES, ALL, UNREAD;
enum class SubscriptionSection(val navigationButtonId: Int): Parcelable {
FAVORITES(R.id.subscriptions_bottom_navigation_favorites),
ALL(R.id.subscriptions_bottom_navigation_all),
UNREAD(R.id.subscriptions_bottom_navigation_unread);
companion object {
fun byPosition(position: Int) = SubscriptionSection.values().let {
if(position > it.size) ALL else it[position]
}
fun fromNavigationButton(buttonId: Int) = when(buttonId) {
R.id.subscriptions_bottom_navigation_favorites -> FAVORITES
R.id.subscriptions_bottom_navigation_all -> ALL
R.id.subscriptions_bottom_navigation_unread -> UNREAD
else -> ALL
}
}
}
\ No newline at end of file
package fr.chenry.android.freshrss.components.subscriptions
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.*
import kotlinx.android.synthetic.main.fragment_subscriptions.*
class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
val subscriptions get() = model.liveData.value ?: listOf()
private val subscriptionSection by lazy {
arguments?.getParcelable(argumentKey) ?: SubscriptionSection.ALL
}
private val model by lazy {
when(subscriptionSection) {
ALL -> ViewModelProviders.of(this).get(AllSubscriptionsVM::class.java)
UNREAD -> ViewModelProviders.of(this).get(UnreadSubscriptionsVM::class.java)
FAVORITES -> ViewModelProviders.of(this).get(FavoritesSubscriptionsVM::class.java)
}
}
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)
model.liveData.observe(this, this)
view.findViewById<RecyclerView>(R.id.subscriptions_list).let {
it.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
it.adapter = this.adapter
}
return view
}
override fun onChanged(subscriptions: Subscriptions?) {
(subscriptions ?: listOf()).let {
subscriptions_list.visibility = if(it.isEmpty()) View.GONE else View.VISIBLE
fragment_subscriptions_waiting.visibility = if(it.isNotEmpty()) View.GONE else View.VISIBLE
adapter.notifyDataSetChanged()
}
}
fun onClick(subscription: Subscription) {
when(subscriptionSection) {
FAVORITES -> MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
ALL -> MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
UNREAD -> MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, ReadStatus.UNREAD)
}.let {view?.findNavController()?.navigate(it)}
}
companion object {
const val argumentKey = "subscriptionSection"
}
}
\ No newline at end of file
package fr.chenry.android.freshrss.components.subscriptions
import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.UnreadSubscriptionsViewModel
class UnreadSubscriptionsFragment: SubscriptionsFragment() {
private lateinit var model: UnreadSubscriptionsViewModel
override val liveData get() = model.subscriptions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.run {
model = ViewModelProviders.of(this).get(UnreadSubscriptionsViewModel::class.java)
}
}
override fun onClick(subscription: Subscription) {
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, UNREAD)
view?.findNavController()?.navigate(direction)
}
}
......@@ -47,7 +47,12 @@ object Store {
api.getUnreadCount() then {
totalUnreadCount.postValue(it.max)
if(!subscriptions.isNullOrEmpty())
it.unreadcounts.forEach {self -> subscriptions[self.id]?.unreadCount = self.count}
it.unreadcounts.forEach {self ->
subscriptions[self.id]?.let {subscription ->
subscription.unreadCount = self.count
subscription.newestItemDate = self.newestItemTimestampUsec
}
}
subscriptions.emit()
}
......
......@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StringDeserializer
import fr.chenry.android.freshrss.BR
import fr.chenry.android.freshrss.utils.unescapeHtml4
import org.joda.time.LocalDateTime
typealias StreamId = String
......@@ -29,6 +30,8 @@ data class Subscription(
field = value
notifyPropertyChanged(BR.unreadCount)
}
@Transient
var newestItemDate: LocalDateTime = LocalDateTime(0)
}
data class SubscriptionCategory(
......
package fr.chenry.android.freshrss.store.databindingsupport.viewmodels
import androidx.lifecycle.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
sealed class SubscriptionsVM: ViewModel() {
val liveData: LiveData<Subscriptions> = MutableLiveData<Subscriptions>().apply {
value = load()
Store.subscriptions.observeForever {value = load()}
}
protected abstract fun load(): Subscriptions
}
class AllSubscriptionsVM: SubscriptionsVM() {
override fun load() = Store.subscriptions.values.sortedWith(Comparator {o1, o2 ->
o1.title.compareTo(o2.title, true)
})
}
class UnreadSubscriptionsVM: SubscriptionsVM() {
override fun load() = Store.subscriptions.values.filter {it.unreadCount > 0}.sortedBy {it.newestItemDate}
}
class FavoritesSubscriptionsVM: SubscriptionsVM() {
override fun load() = listOf<Subscription>()
}
\ No newline at end of file
package fr.chenry.android.freshrss.store.databindingsupport.viewmodels
import androidx.lifecycle.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.databindingsupport.MapList
class UnreadSubscriptionsViewModel: ViewModel() {
private val liveData: MutableLiveData<MapList<String, Subscription>> by lazy {
MutableLiveData<MapList<String, Subscription>>().apply {
this.value = load()
Store.subscriptions.observeForever {this.value = load()}
}
}
val subscriptions = MutableLiveData<Subscriptions>().apply {
this.value = liveData.value?.values ?: listOf()
liveData.observeForever {this.value = liveData.value?.values ?: listOf()}
}
private fun load(): MapList<String, Subscription> {
if(Store.subscriptions.isNullOrEmpty()) return MapList()
return MapList(Store.subscriptions.entries.filter {it.value.unreadCount > 0})
}
}
\ No newline at end of file
......@@ -8,7 +8,8 @@
android:layout_height="match_parent"
tools:context=".components.subscriptions.MainSubscriptionFragment">
<FrameLayout
<androidx.viewpager.widget.ViewPager
android:id="@+id/subcription_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
......@@ -16,7 +17,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
</FrameLayout>
</androidx.viewpager.widget.ViewPager>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/subcription_bottom_navigation"
......
......@@ -4,7 +4,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.subscriptions.UnreadSubscriptionsFragment"
tools:context=".components.subscriptions.SubscriptionsFragment"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/subscriptions_list"
......
package fr.chenry.android.freshrss.components.subscriptions
import org.junit.Test
import org.junit.Assert.*
class SubscriptionSectionTest {
@Test
fun fromNavigationButton() {
SubscriptionSection.values().forEach {
assertEquals("${it.name} was initialized with button id " +
"${it.navigationButtonId} but SubscriptionSection#fromNavigationButton " +
"maps ${it.navigationButtonId} to another section",
SubscriptionSection.fromNavigationButton(it.navigationButtonId), it)
}
}
}
\ No newline at end of file
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