Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
[Reader] Implement "More from <tag>" row
UI already implemented on https://github.com/wordpress-mobile/WordPress-Android/pull/20686


Discussion hints:
Thanks for reporting! 👍 <!-- generated_by_dangermattic -->


Pull request: [Tags IA] Integrate subfilters and post list in Tags Feed
Fixes #20669 

- Refactor `SubFilterViewModel` to move relevant observers to `ReaderFragment` and be able to reuse it properly in Tags Feed
- Fix subfilter in the top bar for Tags Feed
- Implement the tag pills and "view more" feed button actions
- Implement functionality to show the post list for selected/filtered tag

-----

## To Test:

1. Make sure the `reader-tags-feed` feature flag is on
2. Go to Reader
3. Go to the Tags Feed
4. **Verify** the top bar shows the "X Tags" pill
5. Tap the filter chip in the top bar
6. **Verify** it opens the filter bottom sheet
7. Select a tag
8. **Verify** it opens the Post List for that tag
9. Clear the filter
10. **Verify** it returns to the Tags Feed
11. Tap the "header" pill for a tag in the feed
12. **Verify** it opens the Post List for that tag
13. **Verify** it updates de top bar subfilter pill
14. Clear the filter
15. **Verify** it returns to the Tags Feed
16. Scroll horizontally to the end of a tag post row
17. Tap "More from <tag>"
18. **Verify** steps 12-15 happen 

-----

## Regression Notes

1. Potential unintended areas of impact

    - Breakage of top bar filters in other filterable screens (subscriptions/P2/automattic)

19. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing

20. What automated tests I added (or what prevented me from doing so)

    - WIP

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProviderTest" --tests "org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapperTest" --tests "org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModelTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt
index eb8a55d29038..474ef518d722 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
 import androidx.recyclerview.widget.RecyclerView
 import dagger.hilt.android.AndroidEntryPoint
 import org.greenrobot.eventbus.EventBus
@@ -22,23 +23,34 @@ import org.greenrobot.eventbus.ThreadMode.MAIN
 import org.wordpress.android.R
 import org.wordpress.android.databinding.ReaderFragmentLayoutBinding
 import org.wordpress.android.models.JetpackPoweredScreen
+import org.wordpress.android.models.ReaderTag
 import org.wordpress.android.ui.ScrollableViewInitializedListener
 import org.wordpress.android.ui.compose.theme.AppTheme
 import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment
 import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType
-import org.wordpress.android.ui.main.WPMainActivity
 import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener
 import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER
 import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment
 import org.wordpress.android.ui.pages.SnackbarMessageHolder
 import org.wordpress.android.ui.quickstart.QuickStartEvent
+import org.wordpress.android.ui.reader.SubfilterBottomSheetFragment.Companion.newInstance
 import org.wordpress.android.ui.reader.discover.ReaderDiscoverFragment
 import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment
+import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.FOLLOWED_BLOGS
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.TAGS
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter
+import org.wordpress.android.ui.reader.subfilter.ActionType
+import org.wordpress.android.ui.reader.subfilter.ActionType.OpenLoginPage
+import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSearchPage
+import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSubsAtPage
+import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSuggestedTagsPage
+import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState
+import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState.BottomSheetVisible
 import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel
+import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider
 import org.wordpress.android.ui.reader.subfilter.SubfilterCategory
+import org.wordpress.android.ui.reader.subfilter.SubfilterListItem
 import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel
 import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState
 import org.wordpress.android.ui.reader.views.compose.ReaderTopAppBar
@@ -46,18 +58,21 @@ import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType
 import org.wordpress.android.ui.utils.UiHelpers
 import org.wordpress.android.ui.utils.UiString.UiStringText
 import org.wordpress.android.util.JetpackBrandingUtils
+import org.wordpress.android.util.NetworkUtils
 import org.wordpress.android.util.QuickStartUtilsWrapper
 import org.wordpress.android.util.SnackbarItem
 import org.wordpress.android.util.SnackbarItem.Action
 import org.wordpress.android.util.SnackbarItem.Info
 import org.wordpress.android.util.SnackbarSequencer
+import org.wordpress.android.viewmodel.Event
+import org.wordpress.android.viewmodel.main.WPMainActivityViewModel
 import org.wordpress.android.viewmodel.observeEvent
 import java.util.EnumSet
 import javax.inject.Inject
 
 @AndroidEntryPoint
 class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableViewInitializedListener,
-    WPMainActivity.OnScrollToTopListener {
+    OnScrollToTopListener, SubFilterViewModelProvider {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
@@ -72,12 +87,15 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
 
     @Inject
     lateinit var snackbarSequencer: SnackbarSequencer
+
     private lateinit var viewModel: ReaderViewModel
 
     private var binding: ReaderFragmentLayoutBinding? = null
 
     private var readerSearchResultLauncher: ActivityResultLauncher<Intent>? = null
 
+    private var readerSubsActivityResultLauncher: ActivityResultLauncher<Intent>? = null
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         binding = ReaderFragmentLayoutBinding.bind(view).apply {
             initTopAppBar()
@@ -103,6 +121,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
     override fun onAttach(context: Context) {
         super.onAttach(context)
         initReaderSearchActivityResultLauncher()
+        initReaderSubsActivityResultLauncher()
     }
 
     private fun initReaderSearchActivityResultLauncher() {
@@ -122,6 +141,25 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
         }
     }
 
+    private fun initReaderSubsActivityResultLauncher() {
+        readerSubsActivityResultLauncher = registerForActivityResult(
+            ActivityResultContracts.StartActivityForResult()
+        ) { result: ActivityResult ->
+            if (result.resultCode == Activity.RESULT_OK) {
+                val data = result.data
+                if (data != null) {
+                    val shouldRefreshSubscriptions = data.getBooleanExtra(
+                        ReaderSubsActivity.RESULT_SHOULD_REFRESH_SUBSCRIPTIONS,
+                        false
+                    )
+                    if (shouldRefreshSubscriptions) {
+                        getSubFilterViewModel()?.loadSubFilters()
+                    }
+                }
+            }
+        }
+    }
+
     private fun ReaderFragmentLayoutBinding.initTopAppBar() {
         readerTopBarComposeView.apply {
             setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
@@ -144,7 +182,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
     }
 
     private fun ReaderFragmentLayoutBinding.initViewModel(savedInstanceState: Bundle?) {
-        viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory).get(ReaderViewModel::class.java)
+        viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java]
         startReaderViewModel(savedInstanceState)
     }
 
@@ -353,19 +391,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
         return childFragmentManager.findFragmentById(R.id.container)
     }
 
-    // The view model is started by the ReaderPostListFragment for feeds that support filtering
-    private fun getSubFilterViewModel(): SubFilterViewModel? {
-        val currentFragment = getCurrentFeedFragment()
-        val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag
-
-        if (currentFragment == null || selectedTag == null) return null
-
-        return ViewModelProvider(currentFragment, viewModelFactory).get(
-            SubFilterViewModel.getViewModelKeyForTag(selectedTag),
-            SubFilterViewModel::class.java
-        )
-    }
-
     private fun tryOpenFilterList(type: ReaderFilterType) {
         val viewModel = getSubFilterViewModel() ?: return
 
@@ -390,4 +415,143 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
             currentFragment.onScrollToTop()
         }
     }
+
+    /**
+     * The owner of the SubFilterViewModel should be the current feed Fragment, so it can be properly cleared when the
+     * feed is changed, since it will be properly tied to the expected feed Fragment lifecycle instead of the
+     * [ReaderFragment] lifecycle.
+     *
+     * This method exists mainly for readability purposes and to avoid passing the Fragment as a parameter.
+     *
+     * Note: it can cause a crash if the current feed Fragment is not available for any reason, which should never
+     * happen since the calling methods are always called by the feed Fragment or their children.
+     */
+    private fun getSubFilterViewModelOwner(): ViewModelStoreOwner {
+        return getCurrentFeedFragment() as ViewModelStoreOwner
+    }
+
+    private fun getSubFilterViewModel(): SubFilterViewModel? {
+        val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag ?: return null
+        return getSubFilterViewModelForTag(selectedTag)
+    }
+
+    /**
+     * Get the SubFilterViewModel for the given key. It doesn't initialize the ViewModel if it's not already started, so
+     * should only be used for getting a ViewModel that's already been started.
+     */
+    override fun getSubFilterViewModelForKey(key: String): SubFilterViewModel {
+        return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[key, SubFilterViewModel::class.java]
+    }
+
+    override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel {
+        return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[
+            SubFilterViewModel.getViewModelKeyForTag(tag),
+            SubFilterViewModel::class.java
+        ].also {
+            it.initSubFilterViewModel(tag, savedInstanceState)
+        }
+    }
+
+    private fun SubFilterViewModel.initSubFilterViewModel(startedTag: ReaderTag, savedInstanceState: Bundle?) {
+        setupSubFilterBottomSheetObservers(startedTag)
+
+        currentSubFilter.observe(
+            viewLifecycleOwner
+        ) { subfilterListItem: SubfilterListItem ->
+            onSubfilterSelected(subfilterListItem)
+            viewModel.onSubFilterItemSelected(subfilterListItem)
+        }
+
+
+        updateTagsAndSites.observe(
+            viewLifecycleOwner
+        ) { event: Event<EnumSet<UpdateTask>> ->
+            event.applyIfNotHandled {
+                if (NetworkUtils.isNetworkAvailable(activity)) {
+                    ReaderUpdateServiceStarter.startService(activity, this)
+                }
+            }
+        }
+
+        if (startedTag.isFilterable) {
+            subFilters.observe(
+                viewLifecycleOwner
+            ) { subFilters: List<SubfilterListItem> ->
+                viewModel.showTopBarFilterGroup(
+                    startedTag,
+                    subFilters
+                )
+            }
+
+            updateTagsAndSites()
+        } else {
+            viewModel.hideTopBarFilterGroup(startedTag)
+        }
+
+        // thomashortadev: not sure if always passing the same tags can cause problems
+        start(startedTag, startedTag, savedInstanceState)
+    }
+
+    private fun SubFilterViewModel.setupSubFilterBottomSheetObservers(startedTag: ReaderTag) {
+        val wpMainActivityViewModel = ViewModelProvider(
+            requireActivity(),
+            viewModelFactory
+        )[WPMainActivityViewModel::class.java]
+
+        val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(startedTag)
+
+        bottomSheetUiState.observe(
+            viewLifecycleOwner
+        ) { event: Event<BottomSheetUiState> ->
+            event.applyIfNotHandled {
+                val fm = childFragmentManager
+                var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment?
+                if (isVisible && bottomSheet == null) {
+                    loadSubFilters()
+                    val (title, category) = this as BottomSheetVisible
+                    bottomSheet = newInstance(
+                        viewModelKey,
+                        category,
+                        uiHelpers.getTextOfUiString(requireContext(), title)
+                    )
+                    bottomSheet.show(childFragmentManager, SUBFILTER_BOTTOM_SHEET_TAG)
+                } else if (!isVisible && bottomSheet != null) {
+                    bottomSheet.dismiss()
+                }
+            }
+        }
+
+        bottomSheetAction.observe(
+            viewLifecycleOwner
+        ) { event: Event<ActionType> ->
+            event.applyIfNotHandled {
+                when (this) {
+                    is OpenSubsAtPage -> {
+                        readerSubsActivityResultLauncher?.launch(
+                            ReaderActivityLauncher.createIntentShowReaderSubs(
+                                requireActivity(),
+                                tabIndex
+                            )
+                        )
+                    }
+
+                    is OpenLoginPage -> {
+                        wpMainActivityViewModel.onOpenLoginPage()
+                    }
+
+                    is OpenSearchPage -> {
+                        ReaderActivityLauncher.showReaderSearch(requireActivity())
+                    }
+
+                    is OpenSuggestedTagsPage -> {
+                        ReaderActivityLauncher.showReaderInterests(requireActivity())
+                    }
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val SUBFILTER_BOTTOM_SHEET_TAG = "SUBFILTER_BOTTOM_SHEET_TAG"
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java
index 6b2e3aa4ba6c..822a2aa2f2be 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java
@@ -29,7 +29,6 @@
 import androidx.core.content.ContextCompat;
 import androidx.core.text.HtmlCompat;
 import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -110,12 +109,8 @@
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask;
 import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter;
 import org.wordpress.android.ui.reader.services.update.TagUpdateClientUtilsProvider;
-import org.wordpress.android.ui.reader.subfilter.ActionType.OpenLoginPage;
-import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSearchPage;
-import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSubsAtPage;
-import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSuggestedTagsPage;
-import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState.BottomSheetVisible;
 import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel;
+import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider;
 import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.Site;
 import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.SiteAll;
 import org.wordpress.android.ui.reader.tracker.ReaderTracker;
@@ -145,7 +140,6 @@
 import org.wordpress.android.util.config.ReaderImprovementsFeatureConfig;
 import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig;
 import org.wordpress.android.util.image.ImageManager;
-import org.wordpress.android.viewmodel.main.WPMainActivityViewModel;
 import org.wordpress.android.widgets.AppRatingDialog;
 import org.wordpress.android.widgets.RecyclerItemDecoration;
 import org.wordpress.android.widgets.WPSnackbar;
@@ -607,24 +601,15 @@ private void addWebViewCachingFragment(Long blogId, Long postId) {
     }
 
     private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) {
-        WPMainActivityViewModel wpMainActivityViewModel = new ViewModelProvider(requireActivity(), mViewModelFactory)
-                .get(WPMainActivityViewModel.class);
-
-        mSubFilterViewModel = new ViewModelProvider(this, mViewModelFactory).get(
-                SubFilterViewModel.getViewModelKeyForTag(mTagFragmentStartedWith),
-                SubFilterViewModel.class
-        );
+        mSubFilterViewModel = SubFilterViewModelProvider.
+                getSubFilterViewModelForTag(this, mTagFragmentStartedWith, savedInstanceState);
 
         mSubFilterViewModel.getCurrentSubFilter().observe(getViewLifecycleOwner(), subfilterListItem -> {
             if (getPostListType() != ReaderPostListType.SEARCH_RESULTS) {
-                mSubFilterViewModel.onSubfilterSelected(subfilterListItem);
-
                 if (shouldShowEmptyViewForSelfHostedCta()) {
                     setEmptyTitleDescriptionAndButton(false);
                     showEmptyView();
                 }
-
-                if (mReaderViewModel != null) mReaderViewModel.onSubFilterItemSelected(subfilterListItem);
             }
         });
 
@@ -633,70 +618,6 @@ private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) {
                 changeReaderMode(readerModeInfo, true);
             }
         });
-
-        mSubFilterViewModel.getBottomSheetUiState().observe(getViewLifecycleOwner(), event -> {
-            event.applyIfNotHandled(uiState -> {
-                FragmentManager fm = getChildFragmentManager();
-                if (fm != null) {
-                    SubfilterBottomSheetFragment bottomSheet =
-                            (SubfilterBottomSheetFragment) fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG);
-                    if (uiState.isVisible() && bottomSheet == null) {
-                        mSubFilterViewModel.loadSubFilters();
-                        BottomSheetVisible visibleState = (BottomSheetVisible) uiState;
-                        bottomSheet = SubfilterBottomSheetFragment.newInstance(
-                                SubFilterViewModel.getViewModelKeyForTag(mTagFragmentStartedWith),
-                                visibleState.getCategory(),
-                                mUiHelpers.getTextOfUiString(requireContext(), visibleState.getTitle())
-                        );
-                        bottomSheet.show(getChildFragmentManager(), SUBFILTER_BOTTOM_SHEET_TAG);
-                    } else if (!uiState.isVisible() && bottomSheet != null) {
-                        bottomSheet.dismiss();
-                    }
-                }
-                return null;
-            });
-        });
-
-        mSubFilterViewModel.getBottomSheetAction().observe(getViewLifecycleOwner(), event -> {
-            event.applyIfNotHandled(action -> {
-                if (action instanceof OpenSubsAtPage) {
-                    mReaderSubsActivityResultLauncher.launch(
-                            ReaderActivityLauncher.createIntentShowReaderSubs(
-                                    requireActivity(),
-                                    ((OpenSubsAtPage) action).getTabIndex()
-                            )
-                    );
-                } else if (action instanceof OpenLoginPage) {
-                    wpMainActivityViewModel.onOpenLoginPage();
-                } else if (action instanceof OpenSearchPage) {
-                    ReaderActivityLauncher.showReaderSearch(requireActivity());
-                } else if (action instanceof OpenSuggestedTagsPage) {
-                    ReaderActivityLauncher.showReaderInterests(requireActivity());
-                }
-
-                return null;
-            });
-        });
-
-        mSubFilterViewModel.getUpdateTagsAndSites().observe(getViewLifecycleOwner(), event -> {
-            event.applyIfNotHandled(tasks -> {
-                if (NetworkUtils.isNetworkAvailable(getActivity())) {
-                    ReaderUpdateServiceStarter.startService(getActivity(), tasks);
-                }
-                return null;
-            });
-        });
-
-        if (mIsFilterableScreen) {
-            mSubFilterViewModel.getSubFilters().observe(getViewLifecycleOwner(), subFilters -> {
-                mReaderViewModel.showTopBarFilterGroup(mTagFragmentStartedWith, subFilters);
-            });
-            mSubFilterViewModel.updateTagsAndSites();
-        } else {
-            mReaderViewModel.hideTopBarFilterGroup(mTagFragmentStartedWith);
-        }
-
-        mSubFilterViewModel.start(mTagFragmentStartedWith, mCurrentTag, savedInstanceState);
     }
 
     private void changeReaderMode(ReaderModeInfo readerModeInfo, boolean onlyOnChanges) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt
index b54fdb83f60f..6f597948169e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt
@@ -4,6 +4,9 @@ import android.os.Bundle
 import android.view.View
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.core.view.ViewCompat.animate
+import androidx.core.view.isVisible
+import androidx.fragment.app.commitNow
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.ViewModelProvider
 import dagger.hilt.android.AndroidEntryPoint
@@ -13,14 +16,12 @@ import org.wordpress.android.models.ReaderTag
 import org.wordpress.android.ui.ViewPagerFragment
 import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground
 import org.wordpress.android.ui.main.WPMainActivity
-import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter
 import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel
-import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel.Companion.getViewModelKeyForTag
+import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider
 import org.wordpress.android.ui.reader.subfilter.SubfilterListItem
 import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel
-import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel
+import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent
 import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed
-import org.wordpress.android.util.NetworkUtils
 import org.wordpress.android.util.extensions.getSerializableCompat
 import javax.inject.Inject
 
@@ -46,9 +47,6 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme
     private lateinit var subFilterViewModel: SubFilterViewModel
 
     private val viewModel: ReaderTagsFeedViewModel by viewModels()
-    private val readerViewModel: ReaderViewModel by viewModels(
-        ownerProducer = { requireParentFragment() }
-    )
 
     // binding
     private lateinit var binding: ReaderTagFeedFragmentLayoutBinding
@@ -64,35 +62,102 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme
             }
         }
 
-        initViewModels(savedInstanceState)
+        observeSubFilterViewModel(savedInstanceState)
+        observeActionEvents()
     }
 
-    private fun initViewModels(savedInstanceState: Bundle?) {
-        subFilterViewModel = ViewModelProvider(this, viewModelFactory).get(
-            getViewModelKeyForTag(tagsFeedTag),
-            SubFilterViewModel::class.java
+    private fun observeSubFilterViewModel(savedInstanceState: Bundle?) {
+        subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag(
+            this,
+            tagsFeedTag,
+            savedInstanceState
         )
-        subFilterViewModel.start(tagsFeedTag, tagsFeedTag, savedInstanceState)
 
-        subFilterViewModel.updateTagsAndSites.observe(viewLifecycleOwner) { event ->
-            event.applyIfNotHandled {
-                if (NetworkUtils.isNetworkAvailable(activity)) {
-                    ReaderUpdateServiceStarter.startService(activity, this)
+        // TODO not triggered when there's no internet, so the error/no connection UI is not shown.
+        subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters ->
+            val tags = subFilters.filterIsInstance<SubfilterListItem.Tag>().map { it.tag }
+            viewModel.start(tags)
+        }
+
+        subFilterViewModel.currentSubFilter.observe(viewLifecycleOwner) { subFilter ->
+            if (subFilter is SubfilterListItem.Tag) {
+                showTagPostList(subFilter.tag)
+            } else {
+                hideTagPostList()
+            }
+        }
+    }
+
+    private fun observeActionEvents() {
+        viewModel.actionEvents.observe(viewLifecycleOwner) {
+            when (it) {
+                is ActionEvent.OpenTagPostsFeed -> {
+                    subFilterViewModel.setSubfilterFromTag(it.readerTag)
                 }
             }
         }
+    }
 
-        // TODO not triggered when there's no internet, so the error/no connection UI is not shown.
-        subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters ->
-            readerViewModel.showTopBarFilterGroup(
-                tagsFeedTag,
-                subFilters
-            )
+    private fun showTagPostList(tag: ReaderTag) {
+        startPostListFragment(tag)
+        binding.postListContainer.fadeIn(
+            withEndAction = { binding.composeView.isVisible = false },
+        )
+    }
 
-            val tags = subFilters.filterIsInstance<SubfilterListItem.Tag>().map { it.tag }
-            viewModel.start(tags)
+    private fun hideTagPostList() {
+        binding.composeView.isVisible = true
+        binding.postListContainer.fadeOut(
+            withEndAction = { removeCurrentPostListFragment() },
+        )
+    }
+
+    private fun startPostListFragment(tag: ReaderTag) {
+        val tagPostListFragment = ReaderPostListFragment.newInstanceForTag(
+            tag,
+            ReaderTypes.ReaderPostListType.TAG_FOLLOWED
+        )
+
+        childFragmentManager.commitNow {
+            replace(R.id.post_list_container, tagPostListFragment)
+        }
+    }
+
+    private fun removeCurrentPostListFragment() {
+        childFragmentManager.run {
+            findFragmentById(R.id.post_list_container)?.let {
+                commitNow {
+                    remove(it)
+                }
+            }
         }
-        subFilterViewModel.updateTagsAndSites()
+    }
+
+    private fun View.fadeIn(
+        withEndAction: (() -> Unit)? = null
+    ) {
+        alpha = 0f
+        isVisible = true
+
+        animate(this)
+            // add quick delay to give time for the fragment to be added and load some content
+            .setStartDelay(POST_LIST_FADE_IN_DELAY)
+            .setDuration(POST_LIST_FADE_DURATION)
+            .withEndAction { withEndAction?.invoke() }
+            .alpha(1f)
+    }
+
+    private fun View.fadeOut(
+        withEndAction: (() -> Unit)? = null,
+    ) {
+        animate(this)
+            .withEndAction {
+                isVisible = false
+                alpha = 1f
+                withEndAction?.invoke()
+            }
+            .setDuration(POST_LIST_FADE_DURATION)
+            .alpha(0f)
     }
 
     override fun getScrollableViewForUniqueIdProvision(): View {
@@ -105,6 +170,8 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme
 
     companion object {
         private const val ARG_TAGS_FEED_TAG = "tags_feed_tag"
+        private const val POST_LIST_FADE_DURATION = 250L
+        private const val POST_LIST_FADE_IN_DELAY = 300L
 
         fun newInstance(
             feedTag: ReaderTag
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt
index 0aa90d286fbd..262c6f2d119d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt
@@ -10,7 +10,6 @@ import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.core.view.isVisible
 import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.ViewModelStoreOwner
 import androidx.viewpager.widget.ViewPager
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -19,6 +18,7 @@ import org.wordpress.android.R
 import org.wordpress.android.WordPress
 import org.wordpress.android.ui.reader.subfilter.ActionType
 import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel
+import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider
 import org.wordpress.android.ui.reader.subfilter.SubfilterCategory
 import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.SITES
 import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.TAGS
@@ -75,10 +75,7 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() {
             return
         }
 
-        viewModel = ViewModelProvider(
-            parentFragment as ViewModelStoreOwner,
-            viewModelFactory
-        )[subfilterVmKey, SubFilterViewModel::class.java]
+        viewModel = SubFilterViewModelProvider.getSubFilterViewModelForKey(this, subfilterVmKey)
 
         // TODO remove the pager and support only one category
         val pager = view.findViewById<ViewPager>(R.id.view_pager)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt
new file mode 100644
index 000000000000..33057daf33cd
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt
@@ -0,0 +1,66 @@
+package org.wordpress.android.ui.reader.subfilter
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import org.wordpress.android.models.ReaderTag
+
+interface SubFilterViewModelProvider {
+    fun getSubFilterViewModelForKey(key: String): SubFilterViewModel
+    fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle? = null): SubFilterViewModel
+
+    companion object {
+        /**
+         * Helper function to get the [SubFilterViewModel] for a given [ReaderTag] from a [Fragment]. Note that the
+         * [Fragment] must be a child or descendant of a Fragment that implements [SubFilterViewModelProvider],
+         * otherwise this function will throw an [IllegalStateException].
+         *
+         * @param fragment the [Fragment] to get the [SubFilterViewModel] from
+         * @param tag the [ReaderTag] to get the [SubFilterViewModel] for
+         * @param savedInstanceState the [Bundle] to pass to the [SubFilterViewModel] when it is created
+         * @return the [SubFilterViewModel] for the given [ReaderTag]
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun getSubFilterViewModelForTag(
+            fragment: Fragment,
+            tag: ReaderTag,
+            savedInstanceState: Bundle? = null
+        ): SubFilterViewModel {
+            // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner
+            var possibleProvider: Fragment? = fragment
+            while (possibleProvider != null) {
+                if (possibleProvider is SubFilterViewModelProvider) {
+                    return possibleProvider.getSubFilterViewModelForTag(tag, savedInstanceState)
+                }
+                possibleProvider = possibleProvider.parentFragment
+            }
+            error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner")
+        }
+
+        /**
+         * Helper function to get the [SubFilterViewModel] for a given key from a [Fragment]. Note that the [Fragment]
+         * must be a child or descendant of a Fragment that implements [SubFilterViewModelProvider], otherwise this
+         * function will throw an [IllegalStateException].
+         *
+         * @param fragment the [Fragment] to get the [SubFilterViewModel] from
+         * @param key the key to get the [SubFilterViewModel] for
+         * @return the [SubFilterViewModel] for the given key, or null if the [Fragment] is not a child or descendant
+         * of a Fragment that implements [SubFilterViewModelProvider]
+         */
+        @JvmStatic
+        fun getSubFilterViewModelForKey(
+            fragment: Fragment,
+            key: String,
+        ): SubFilterViewModel {
+            // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner
+            var possibleProvider: Fragment? = fragment
+            while (possibleProvider != null) {
+                if (possibleProvider is SubFilterViewModelProvider) {
+                    return possibleProvider.getSubFilterViewModelForKey(key)
+                }
+                possibleProvider = possibleProvider.parentFragment
+            }
+            error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner")
+        }
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt
index 4820df8c4599..120886d85947 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt
@@ -14,7 +14,6 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentPagerAdapter
 import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.ViewModelStoreOwner
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import dagger.hilt.android.AndroidEntryPoint
@@ -101,10 +100,7 @@ class SubfilterPageFragment : Fragment() {
         primaryButton = emptyStateContainer.findViewById(R.id.action_button_primary)
         secondaryButton = emptyStateContainer.findViewById(R.id.action_button_secondary)
 
-        subFilterViewModel = ViewModelProvider(
-            requireParentFragment().parentFragment as ViewModelStoreOwner,
-            viewModelFactory
-        )[subfilterVmKey, SubFilterViewModel::class.java]
+        subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForKey(this, subfilterVmKey)
 
         subFilterViewModel.subFilters.observe(viewLifecycleOwner) {
             (recyclerView.adapter as? SubfilterListAdapter)?.let { adapter ->
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt
index b591258043f1..5f010ac199fd 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt
@@ -15,7 +15,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(
     fun mapLoadedTagFeedItem(
         tag: ReaderTag,
         posts: ReaderPostList,
-        onTagClick: () -> Unit,
+        onTagClick: (ReaderTag) -> Unit,
         onSiteClick: () -> Unit,
         onPostImageClick: () -> Unit,
         onPostLikeClick: () -> Unit,
@@ -54,7 +54,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(
     fun mapErrorTagFeedItem(
         tag: ReaderTag,
         errorType: ReaderTagsFeedViewModel.ErrorType,
-        onTagClick: () -> Unit,
+        onTagClick: (ReaderTag) -> Unit,
         onRetryClick: () -> Unit,
     ): ReaderTagsFeedViewModel.TagFeedItem =
         ReaderTagsFeedViewModel.TagFeedItem(
@@ -70,7 +70,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(
 
     fun mapLoadingPostsUiState(
         tags: List<ReaderTag>,
-        onTagClick: () -> Unit,
+        onTagClick: (ReaderTag) -> Unit,
     ): ReaderTagsFeedViewModel.UiState.Loaded =
         ReaderTagsFeedViewModel.UiState.Loaded(
             tags.map { tag ->
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt
index de71d090c6c1..cdd636a933e7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt
@@ -1,5 +1,6 @@
 package org.wordpress.android.ui.reader.viewmodels.tagsfeed
 
+import androidx.lifecycle.LiveData
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -11,6 +12,7 @@ import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException
 import org.wordpress.android.ui.reader.repository.ReaderPostRepository
 import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem
 import org.wordpress.android.viewmodel.ScopedViewModel
+import org.wordpress.android.viewmodel.SingleLiveEvent
 import javax.inject.Inject
 import javax.inject.Named
 
@@ -23,12 +25,22 @@ class ReaderTagsFeedViewModel @Inject constructor(
     private val _uiStateFlow: MutableStateFlow<UiState> = MutableStateFlow(UiState.Initial)
     val uiStateFlow: StateFlow<UiState> = _uiStateFlow
 
+    private val _actionEvents = SingleLiveEvent<ActionEvent>()
+    val actionEvents: LiveData<ActionEvent> = _actionEvents
+
     /**
      * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of
      * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following
      * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty].
      */
     fun start(tags: List<ReaderTag>) {
+        // don't start again if the tags match
+        if (_uiStateFlow.value is UiState.Loaded &&
+            tags == (_uiStateFlow.value as UiState.Loaded).data.map { it.tagChip.tag }
+        ) {
+            return
+        }
+
         if (tags.isEmpty()) {
             _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick)
             return
@@ -115,8 +127,8 @@ class ReaderTagsFeedViewModel @Inject constructor(
         // TODO
     }
 
-    private fun onTagClick() {
-        // TODO
+    private fun onTagClick(readerTag: ReaderTag) {
+        _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag)
     }
 
     private fun onRetryClick() {
@@ -139,6 +151,10 @@ class ReaderTagsFeedViewModel @Inject constructor(
         // TODO
     }
 
+    sealed class ActionEvent {
+        data class OpenTagPostsFeed(val readerTag: ReaderTag) : ActionEvent()
+    }
+
     sealed class UiState {
         object Initial : UiState()
         data class Loaded(val data: List<TagFeedItem>) : UiState()
@@ -155,7 +171,7 @@ class ReaderTagsFeedViewModel @Inject constructor(
 
     data class TagChip(
         val tag: ReaderTag,
-        val onTagClick: () -> Unit,
+        val onTagClick: (ReaderTag) -> Unit,
     )
 
     sealed class PostList {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt
index 471e00631133..cfe442922533 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt
@@ -96,7 +96,7 @@ private fun Loaded(uiState: UiState.Loaded) {
                     start = Margin.Large.value,
                 ),
                 text = UiString.UiStringText(tagChip.tag.tagTitle),
-                onClick = tagChip.onTagClick,
+                onClick = { tagChip.onTagClick(tagChip.tag) },
                 height = 36.dp,
             )
             Spacer(modifier = Modifier.height(Margin.Large.value))
@@ -300,7 +300,7 @@ private fun PostListLoaded(
                             interactionSource = remember { MutableInteractionSource() },
                             indication = rememberRipple(bounded = false),
                             onClick = {
-                                tagChip.onTagClick()
+                                tagChip.onTagClick(tagChip.tag)
                                 AppLog.e(AppLog.T.READER, "RL-> Tag clicked")
                             }
                         ),
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt
index f3c724e30b98..1d34cd3c7233 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt
@@ -78,7 +78,7 @@ fun ReaderTagsFeedPostListItem(
             // Site name
             Text(
                 modifier = Modifier
-                    .weight(1F)
+                    .weight(1f, fill = false)
                     .clickable(
                         interactionSource = remember { MutableInteractionSource() },
                         indication = null,


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt
new file mode 100644
index 000000000000..99c7fe6e3ac5
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt
@@ -0,0 +1,17 @@
+package org.wordpress.android.ui.reader
+
+import org.wordpress.android.models.ReaderTag
+import org.wordpress.android.models.ReaderTagType
+
+object ReaderTestUtils {
+    fun createTag(
+        slug: String,
+        type: ReaderTagType = ReaderTagType.FOLLOWED,
+    ): ReaderTag = ReaderTag(
+        slug,
+        slug,
+        slug,
+        "endpoint/$slug",
+        type,
+    )
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt
new file mode 100644
index 000000000000..ab0827aaaf68
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.reader.subfilter
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.wordpress.android.models.ReaderTag
+import org.wordpress.android.ui.reader.ReaderTestUtils
+
+// fragment implementing SubFilterViewModelProvider for testing purposes only
+@Suppress("MemberVisibilityCanBePrivate")
+private open class SubFilterViewModelProviderFakeFragment(
+    val viewModelKeyMap: Map<String, SubFilterViewModel> = emptyMap(),
+    val viewModelTagMap: Map<ReaderTag, SubFilterViewModel> = emptyMap(),
+) : Fragment(), SubFilterViewModelProvider {
+    override fun getSubFilterViewModelForKey(key: String): SubFilterViewModel {
+        return viewModelKeyMap[key] ?: error("No SubFilterViewModel found for key: $key")
+    }
+
+    override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel {
+        return viewModelTagMap[tag] ?: error("No SubFilterViewModel found for tag: $tag")
+    }
+}
+
+class SubFilterViewModelProviderTest {
+    @Test
+    fun `getSubFilterViewModelForTag should use given tag for retrieving the appropriate ViewModel`() {
+        // Given
+        val tag1 = ReaderTestUtils.createTag("tag1")
+        val viewModel1: SubFilterViewModel = mock()
+
+        val tag2 = ReaderTestUtils.createTag("tag2")
+        val viewModel2: SubFilterViewModel = mock()
+
+        val fragment = SubFilterViewModelProviderFakeFragment(
+            viewModelTagMap = mapOf(tag1 to viewModel1, tag2 to viewModel2)
+        )
+
+        // When
+        val result1 = SubFilterViewModelProvider.getSubFilterViewModelForTag(fragment, tag1)
+        val result2 = SubFilterViewModelProvider.getSubFilterViewModelForTag(fragment, tag2)
+
+        // Then
+        assertThat(result1).isEqualTo(viewModel1)
+        assertThat(result2).isEqualTo(viewModel2)
+    }
+
+    @Test
+    fun `getSubFilterViewModelForKey should use given key for retrieving the appropriate ViewModel`() {
+        // Given
+        val key1 = "key1"
+        val viewModel1: SubFilterViewModel = mock()
+
+        val key2 = "key2"
+        val viewModel2: SubFilterViewModel = mock()
+
+        val fragment = SubFilterViewModelProviderFakeFragment(
+            viewModelKeyMap = mapOf(key1 to viewModel1, key2 to viewModel2)
+        )
+
+        // When
+        val result1 = SubFilterViewModelProvider.getSubFilterViewModelForKey(fragment, key1)
+        val result2 = SubFilterViewModelProvider.getSubFilterViewModelForKey(fragment, key2)
+
+        // Then
+        assertThat(result1).isEqualTo(viewModel1)
+        assertThat(result2).isEqualTo(viewModel2)
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt
index 3e55571fc74a..aa13618744ce 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt
@@ -5,18 +5,21 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doSuspendableAnswer
+import org.mockito.kotlin.verifyNoInteractions
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.models.ReaderPost
 import org.wordpress.android.models.ReaderPostList
 import org.wordpress.android.models.ReaderTag
-import org.wordpress.android.models.ReaderTagType
+import org.wordpress.android.ui.reader.ReaderTestUtils
 import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException
 import org.wordpress.android.ui.reader.repository.ReaderPostRepository
 import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper
@@ -34,22 +37,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
 
     private val collectedUiStates: MutableList<ReaderTagsFeedViewModel.UiState> = mutableListOf()
 
-    val tag = ReaderTag(
-        "tag",
-        "tag",
-        "tag",
-        "endpoint",
-        ReaderTagType.FOLLOWED,
-    )
-
-    private val postListLoadingItem = ReaderTagsFeedViewModel.TagFeedItem(
-        tagChip = ReaderTagsFeedViewModel.TagChip(
-            tag = tag,
-            onTagClick = {},
-        ),
-        postList = ReaderTagsFeedViewModel.PostList.Loading,
-    )
-
     @Before
     fun setUp() {
         viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository, readerTagsFeedUiStateMapper)
@@ -58,10 +45,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
     @Test
     fun `given valid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates {
         // Given
-        val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem(
-            ReaderTagsFeedViewModel.TagChip(tag, {}),
-            ReaderTagsFeedViewModel.PostList.Loaded(listOf())
-        )
+        val tag = ReaderTestUtils.createTag("tag")
         val posts = ReaderPostList().apply {
             add(ReaderPost())
         }
@@ -69,14 +53,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
             delay(100)
             posts
         }
-        whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any()))
-            .thenReturn(
-                ReaderTagsFeedViewModel.UiState.Loaded(
-                    listOf(postListLoadingItem, postListLoadingItem)
-                )
-            )
-        whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any()))
-            .thenReturn(tagFeedItem)
+        mockMapLoadingTagFeedItems()
+        mockMapLoadedTagFeedItems()
 
         // When
         viewModel.start(listOf(tag))
@@ -85,7 +63,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
         // Then
         assertThat(collectedUiStates).contains(
             ReaderTagsFeedViewModel.UiState.Loaded(
-                data = listOf(tagFeedItem)
+                data = listOf(getLoadedTagFeedItem(tag))
             )
         )
     }
@@ -93,57 +71,33 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
     @Test
     fun `given invalid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates {
         // Given
+        val tag = ReaderTestUtils.createTag("tag")
         val error = ReaderPostFetchException("error")
-        val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem(
-            ReaderTagsFeedViewModel.TagChip(tag, {}),
-            ReaderTagsFeedViewModel.PostList.Error(
-                ReaderTagsFeedViewModel.ErrorType.Default, {}
-            ),
-        )
         whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer {
             delay(100)
             throw error
         }
-        whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any()))
-            .thenReturn(
-                ReaderTagsFeedViewModel.UiState.Loaded(
-                    listOf(postListLoadingItem, postListLoadingItem)
-                )
-            )
-        whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any()))
-            .thenReturn(tagFeedItem)
+        mockMapLoadingTagFeedItems()
+        mockMapErrorTagFeedItems()
 
         // When
         viewModel.start(listOf(tag))
-//        viewModel.fetchTag(tag)
         advanceUntilIdle()
 
         // Then
         assertThat(collectedUiStates).contains(
             ReaderTagsFeedViewModel.UiState.Loaded(
-                data = listOf(tagFeedItem)
+                data = listOf(getErrorTagFeedItem(tag))
             )
         )
     }
 
     @Suppress("LongMethod")
     @Test
-    fun `given valid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates {
+    fun `given valid tags, when start, then UI state should update properly`() = testCollectingUiStates {
         // Given
-        val tag1 = ReaderTag(
-            "tag1",
-            "tag1",
-            "tag1",
-            "endpoint1",
-            ReaderTagType.FOLLOWED,
-        )
-        val tag2 = ReaderTag(
-            "tag2",
-            "tag2",
-            "tag2",
-            "endpoint2",
-            ReaderTagType.FOLLOWED,
-        )
+        val tag1 = ReaderTestUtils.createTag("tag1")
+        val tag2 = ReaderTestUtils.createTag("tag2")
         val posts1 = ReaderPostList().apply {
             add(ReaderPost())
         }
@@ -158,33 +112,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
             delay(200)
             posts2
         }
-        whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any()))
-            .thenReturn(
-                ReaderTagsFeedViewModel.UiState.Loaded(
-                    listOf(
-                        ReaderTagsFeedViewModel.TagFeedItem(
-                            tagChip = ReaderTagsFeedViewModel.TagChip(
-                                tag = tag1,
-                                onTagClick = {},
-                            ),
-                            postList = ReaderTagsFeedViewModel.PostList.Loading,
-                        ),
-                        ReaderTagsFeedViewModel.TagFeedItem(
-                            tagChip = ReaderTagsFeedViewModel.TagChip(
-                                tag = tag2,
-                                onTagClick = {},
-                            ),
-                            postList = ReaderTagsFeedViewModel.PostList.Loading,
-                        ),
-                    )
-                )
-            )
-        val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem(
-            ReaderTagsFeedViewModel.TagChip(tag1, {}),
-            ReaderTagsFeedViewModel.PostList.Loaded(listOf()),
-        )
-        whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any()))
-            .thenReturn(tagFeedItem)
+        mockMapLoadingTagFeedItems()
+        mockMapLoadedTagFeedItems()
 
         // When
         viewModel.start(listOf(tag1, tag2))
@@ -194,9 +123,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
         assertThat(collectedUiStates).contains(
             ReaderTagsFeedViewModel.UiState.Loaded(
                 data = listOf(
-                    tagFeedItem,
-                    tagFeedItem,
-                )
+                    getLoadedTagFeedItem(tag1),
+                    getLoadedTagFeedItem(tag2)
+                ),
             )
         )
     }
@@ -205,20 +134,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
     @Test
     fun `given valid and invalid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates {
         // Given
-        val tag1 = ReaderTag(
-            "tag1",
-            "tag1",
-            "tag1",
-            "endpoint1",
-            ReaderTagType.FOLLOWED,
-        )
-        val tag2 = ReaderTag(
-            "tag2",
-            "tag2",
-            "tag2",
-            "endpoint2",
-            ReaderTagType.FOLLOWED,
-        )
+        val tag1 = ReaderTestUtils.createTag("tag1")
+        val tag2 = ReaderTestUtils.createTag("tag2")
         val posts1 = ReaderPostList().apply {
             add(ReaderPost())
         }
@@ -231,41 +148,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
             delay(200)
             throw error2
         }
-        whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any()))
-            .thenReturn(
-                ReaderTagsFeedViewModel.UiState.Loaded(
-                    listOf(
-                        ReaderTagsFeedViewModel.TagFeedItem(
-                            tagChip = ReaderTagsFeedViewModel.TagChip(
-                                tag = tag1,
-                                onTagClick = {},
-                            ),
-                            postList = ReaderTagsFeedViewModel.PostList.Loading,
-                        ),
-                        ReaderTagsFeedViewModel.TagFeedItem(
-                            tagChip = ReaderTagsFeedViewModel.TagChip(
-                                tag = tag2,
-                                onTagClick = {},
-                            ),
-                            postList = ReaderTagsFeedViewModel.PostList.Loading,
-                        )
-                    )
-                )
-            )
-        val tagFeedItemLoaded = ReaderTagsFeedViewModel.TagFeedItem(
-            ReaderTagsFeedViewModel.TagChip(tag1, {}),
-            ReaderTagsFeedViewModel.PostList.Loaded(listOf())
-        )
-        val tagFeedItemError = ReaderTagsFeedViewModel.TagFeedItem(
-            ReaderTagsFeedViewModel.TagChip(tag2, {}),
-            ReaderTagsFeedViewModel.PostList.Error(
-                ReaderTagsFeedViewModel.ErrorType.Default, {}
-            )
-        )
-        whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any()))
-            .thenReturn(tagFeedItemLoaded)
-        whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any()))
-            .thenReturn(tagFeedItemError)
+        mockMapLoadingTagFeedItems()
+        mockMapLoadedTagFeedItems()
+        mockMapErrorTagFeedItems()
 
         // When
         viewModel.start(listOf(tag1, tag2))
@@ -275,13 +160,108 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() {
         assertThat(collectedUiStates).contains(
             ReaderTagsFeedViewModel.UiState.Loaded(
                 data = listOf(
-                    tagFeedItemLoaded,
-                    tagFeedItemError,
+                    getLoadedTagFeedItem(tag1),
+                    getErrorTagFeedItem(tag2),
                 )
             )
         )
     }
 
+    @Suppress("LongMethod")
+    @Test
+    fun `given tags fetched, when start again, then nothing happens`() = testCollectingUiStates {
+        // Given
+        val tag1 = ReaderTestUtils.createTag("tag1")
+        val tag2 = ReaderTestUtils.createTag("tag2")
+        val posts1 = ReaderPostList().apply {
+            add(ReaderPost())
+        }
+        val posts2 = ReaderPostList().apply {
+            add(ReaderPost())
+        }
+        whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer {
+            delay(100)
+            posts1
+        }
+        whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer {
+            delay(200)
+            posts2
+        }
+        mockMapLoadingTagFeedItems()
+        mockMapLoadedTagFeedItems()
+
+        // When
+        viewModel.start(listOf(tag1, tag2))
+        advanceUntilIdle()
+        val firstCollectedStates = collectedUiStates.toList()
+        Mockito.clearInvocations(readerPostRepository)
+
+        // Then
+        viewModel.start(listOf(tag1, tag2))
+        advanceUntilIdle()
+
+        assertThat(collectedUiStates).isEqualTo(firstCollectedStates) // still same states, nothing new emitted
+        verifyNoInteractions(readerPostRepository)
+    }
+
+    @Suppress("LongMethod")
+    @Test
+    fun `given no tags requested, when start, then UI state should update properly`() = testCollectingUiStates {
+        // Given
+        val tags = emptyList<ReaderTag>()
+
+        // When
+        viewModel.start(tags)
+        advanceUntilIdle()
+
+        // Then
+        assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java)
+    }
+
+    private fun mockMapLoadingTagFeedItems() {
+        whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any()))
+            .thenAnswer {
+                val tags = it.getArgument<List<ReaderTag>>(0)
+                ReaderTagsFeedViewModel.UiState.Loaded(
+                    tags.map { tag ->
+                        ReaderTagsFeedViewModel.TagFeedItem(
+                            tagChip = ReaderTagsFeedViewModel.TagChip(
+                                tag = tag,
+                                onTagClick = {},
+                            ),
+                            postList = ReaderTagsFeedViewModel.PostList.Loading,
+                        )
+                    }
+                )
+            }
+    }
+
+    private fun mockMapLoadedTagFeedItems() {
+        whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any()))
+            .thenAnswer {
+                getLoadedTagFeedItem(it.getArgument(0))
+            }
+    }
+
+    private fun mockMapErrorTagFeedItems() {
+        whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any()))
+            .thenAnswer {
+                getErrorTagFeedItem(it.getArgument(0))
+            }
+    }
+
+    private fun getLoadedTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem(
+        ReaderTagsFeedViewModel.TagChip(tag, {}),
+        ReaderTagsFeedViewModel.PostList.Loaded(listOf())
+    )
+
+    private fun getErrorTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem(
+        ReaderTagsFeedViewModel.TagChip(tag, {}),
+        ReaderTagsFeedViewModel.PostList.Error(
+            ReaderTagsFeedViewModel.ErrorType.Default, {}
+        ),
+    )
+
     private fun testCollectingUiStates(block: suspend TestScope.() -> Unit) = test {
         val collectedUiStatesJob = launch {
             collectedUiStates.clear()
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt
index c88e8ac499b0..c5251634c31c 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt
@@ -51,7 +51,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() {
             "endpoint",
             ReaderTagType.FOLLOWED,
         )
-        val onTagClick = {}
+        val onTagClick = { _: ReaderTag -> }
         val onSiteClick = {}
         val onPostImageClick = {}
         val onPostLikeClick = {}
@@ -119,7 +119,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() {
             ReaderTagType.FOLLOWED,
         )
         val errorType = ReaderTagsFeedViewModel.ErrorType.Default
-        val onTagClick = {}
+        val onTagClick = { _: ReaderTag -> }
         val onRetryClick = {}
         // When
         val actual = classToTest.mapErrorTagFeedItem(
@@ -146,7 +146,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() {
     @Test
     fun `Should map loading posts UI state correctly`() {
         // Given
-        val onTagClick = {}
+        val onTagClick = { _: ReaderTag -> }
         val tag1 = ReaderTag(
             "tag",
             "tag",