Skip to content

Commit

Permalink
Migrate to Navigation Component
Browse files Browse the repository at this point in the history
  • Loading branch information
SIKV committed Jun 9, 2024
1 parent 197e16d commit 71f7cfb
Show file tree
Hide file tree
Showing 42 changed files with 250 additions and 493 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Browse, search, download, and share amazing free photos provided by talented pho
- [Jetpack Compose](https://developer.android.com/jetpack/compose)
- [Android Architecture Components](https://developer.android.com/topic/libraries/architecture)
- [Hilt](https://developer.android.com/training/dependency-injection/hilt-android)
- [Navigation Component](https://developer.android.com/guide/navigation)
- [Paging 3](https://developer.android.com/topic/libraries/architecture/paging/v3-overview)
- [Room](https://developer.android.com/topic/libraries/architecture/room)
- [Retrofit](https://square.github.io/retrofit)
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.viewpager2)

implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.navigation.fragment.ktx)

implementation(libs.hilt.android)
kapt(libs.hilt.compiler)

Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/github/sikv/photos/di/RouteModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package com.github.sikv.photos.di

import com.github.sikv.photos.navigation.route.FeedbackRoute
import com.github.sikv.photos.navigation.route.PhotoDetailsRoute
import com.github.sikv.photos.navigation.route.SearchRoute
import com.github.sikv.photos.navigation.route.SetWallpaperRoute
import com.github.sikv.photos.route.impl.FeedbackRouteImpl
import com.github.sikv.photos.route.impl.PhotoDetailsRouteImpl
import com.github.sikv.photos.route.impl.SearchRouteImpl
import com.github.sikv.photos.route.impl.SetWallpaperRouteImpl
import dagger.Binds
import dagger.Module
Expand All @@ -18,6 +20,9 @@ abstract class RouteModule {
@Binds
abstract fun bindPhotoDetailsRoute(route: PhotoDetailsRouteImpl): PhotoDetailsRoute

@Binds
abstract fun bindSearchRoute(route: SearchRouteImpl): SearchRoute

@Binds
abstract fun bindSetWallpaperRoute(route: SetWallpaperRouteImpl): SetWallpaperRoute

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.github.sikv.photos.route.impl

import com.github.sikv.photos.feedback.FeedbackFragment
import com.github.sikv.photos.navigation.Navigation
import androidx.navigation.NavController
import com.github.sikv.photos.R
import com.github.sikv.photos.navigation.route.FeedbackRoute
import javax.inject.Inject

class FeedbackRouteImpl @Inject constructor() : FeedbackRoute {

override fun present(navigation: Navigation?) {
navigation?.addFragment(FeedbackFragment())
override fun present(navController: NavController) {
navController.navigate(R.id.navigateToFeedback)
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
package com.github.sikv.photos.route.impl

import com.github.sikv.photos.navigation.Navigation
import com.github.sikv.photos.navigation.NavigationAnimation
import androidx.navigation.NavController
import com.github.sikv.photos.R
import com.github.sikv.photos.navigation.args.PhotoDetailsFragmentArguments
import com.github.sikv.photos.navigation.args.withArguments
import com.github.sikv.photos.navigation.navigate
import com.github.sikv.photos.navigation.route.PhotoDetailsRoute
import com.github.sikv.photos.photo.details.PhotoDetailsFragment
import javax.inject.Inject

class PhotoDetailsRouteImpl @Inject constructor() : PhotoDetailsRoute {

override fun present(navigation: Navigation?, args: PhotoDetailsFragmentArguments) {
val photoDetailsFragment = PhotoDetailsFragment()
.withArguments(args)

navigation?.addFragment(photoDetailsFragment,
animation = NavigationAnimation.SLIDE_V
)
override fun present(navController: NavController, args: PhotoDetailsFragmentArguments) {
navController.navigate(R.id.navigateToPhotoDetails, args)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.sikv.photos.route.impl

import androidx.navigation.NavController
import com.github.sikv.photos.R
import com.github.sikv.photos.navigation.args.SearchFragmentArguments
import com.github.sikv.photos.navigation.navigate
import com.github.sikv.photos.navigation.route.SearchRoute
import javax.inject.Inject

class SearchRouteImpl @Inject constructor() : SearchRoute {

override fun present(navController: NavController, args: SearchFragmentArguments) {
navController.navigate(R.id.navigateToSearch, args)
}
}
184 changes: 36 additions & 148 deletions app/src/main/java/com/github/sikv/photos/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,55 +1,36 @@
package com.github.sikv.photos.ui

import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.setupWithNavController
import com.github.sikv.photos.FeatureFlagFetcher
import com.github.sikv.photos.R
import com.github.sikv.photos.common.ui.BaseFragment
import com.github.sikv.photos.databinding.ActivityMainBinding
import com.github.sikv.photos.navigation.OnDestinationChangedListener
import com.github.sikv.photos.navigation.args.SearchFragmentArguments
import com.github.sikv.photos.navigation.args.withArguments
import com.github.sikv.photos.photo.details.PhotoDetailsFragment
import com.github.sikv.photos.search.SearchFragment
import com.github.sikv.photos.ui.fragment.root.FavoritesRootFragment
import com.github.sikv.photos.ui.fragment.root.HomeRootFragment
import com.github.sikv.photos.ui.fragment.root.MoreRootFragment
import com.github.sikv.photos.ui.fragment.root.RootFragment
import com.github.sikv.photos.ui.fragment.root.SearchRootFragment
import com.github.sikv.photos.util.changeFragment
import com.github.sikv.photos.util.getActiveRootFragment
import com.github.sikv.photos.navigation.route.SearchRoute
import com.github.sikv.photos.util.reportShortcutUsed
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.reflect.KClass

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var featureFlagFetcher: FeatureFlagFetcher

private val fragmentsTag = mapOf(
R.id.home to HomeRootFragment::class.customTag(),
R.id.search to SearchRootFragment::class.customTag(),
R.id.favorites to FavoritesRootFragment::class.customTag(),
R.id.more to MoreRootFragment::class.customTag()
)

private var initialFragmentId = R.id.home
private var initialDelayedFragment: Fragment? = null
@Inject
lateinit var searchRoute: SearchRoute

private lateinit var binding: ActivityMainBinding

private val destinationChangedListener = object : OnDestinationChangedListener {
override fun onDestinationChanged(fragment: Fragment?) {
handleBottomNavigationVisibility(fragment)
}
private val destinationChangedListener = NavController.OnDestinationChangedListener { _, destination, _ ->
handleBottomNavigationVisibility(destination)
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -60,141 +41,48 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackPress()
}
})

featureFlagFetcher.fetch(this) {
when (intent.action) {
getString(R.string._shortcut_action_search) -> {
getString(R.string._shortcut_search).reportShortcutUsed()

initialFragmentId = R.id.search
initialDelayedFragment = SearchFragment().withArguments(SearchFragmentArguments())
}
}

if (savedInstanceState == null) {
setupNavigation()
findNavController().addOnDestinationChangedListener(destinationChangedListener)
binding.bottomNavigationView.setupWithNavController(findNavController())

// This solves the follow issue:
// https://stackoverflow.com/questions/71089052/android-navigation-component-bottomnavigationviews-selected-tab-icon-is-not-u
// Always show selected Bottom Navigation item as selected (return true).
binding.bottomNavigationView.setOnItemSelectedListener { item ->
// In order to get the expected behavior, you have to call default Navigation method manually.
NavigationUI.onNavDestinationSelected(item, findNavController())
return@setOnItemSelectedListener true
}

setNavigationListener()
setOnDestinationChangedListener(destinationChangedListener)

handleBottomNavigationVisibility()
}
}

// FYI: Marked as public because of Lint 'Synthetic Accessor' error.
fun handleBackPress() {
if ((supportFragmentManager.getActiveRootFragment() as? RootFragment)?.provideNavigation()
?.backPressed() == false
) {
if (isInitialFragmentSelected()) {
selectInitialFragment()
} else {
finish()
}
handleShortcuts()
}
}

override fun onDestroy() {
setOnDestinationChangedListener(null)
findNavController().removeOnDestinationChangedListener(destinationChangedListener)
super.onDestroy()
}

// TODO Hide [Search] tab if ConfigProvider.getSearchSources() returns 0.
private fun setupNavigation() {
binding.bottomNavigationView.selectedItemId = initialFragmentId

listOf(
HomeRootFragment(),
SearchRootFragment(),
FavoritesRootFragment(),
MoreRootFragment()
).forEach { fragment ->
val tag = fragment.customTag()

val transaction = supportFragmentManager
.beginTransaction()
.add(R.id.navigationContainer, fragment, tag)

if (tag != fragmentsTag[initialFragmentId]) {
transaction.hide(fragment)
}

transaction.commitNow()
}

val delayedFragment = initialDelayedFragment

if (delayedFragment != null) {
val tag = fragmentsTag[initialFragmentId]
val fragment = supportFragmentManager.findFragmentByTag(tag) as? RootFragment

fragment?.addDelayedFragment(delayedFragment)
}
}

fun handleBottomNavigationVisibility(fragment: Fragment?) {
// Hide bottom navigation if [Photo Details] is opened.
val bottomNavigationVisible = fragment !is PhotoDetailsFragment
binding.bottomNavigationView.isVisible = bottomNavigationVisible
}

private fun handleBottomNavigationVisibility() {
supportFragmentManager.fragments.iterator().forEach { rootFragment ->
if (!rootFragment.isHidden) {
// Check the current fragment.
handleBottomNavigationVisibility(rootFragment.childFragmentManager.fragments.lastOrNull())
}
}
}

private fun setNavigationListener() {
binding.bottomNavigationView.setOnItemSelectedListener { menuItem ->
supportFragmentManager.changeFragment(
hideFragmentTag = supportFragmentManager.getActiveRootFragment()?.customTag(),
showFragmentTag = fragmentsTag[menuItem.itemId]
)
true
}
private fun handleShortcuts() {
when (intent.action) {
getString(R.string._shortcut_action_search) -> {
val searchDashboardItem = binding.bottomNavigationView.menu.findItem(R.id.searchDashboard)
NavigationUI.onNavDestinationSelected(searchDashboardItem, findNavController())

binding.bottomNavigationView.setOnItemReselectedListener { menuItem ->
val tag = fragmentsTag[menuItem.itemId]
val fragment = supportFragmentManager.findFragmentByTag(tag) as? RootFragment
searchRoute.present(findNavController(), SearchFragmentArguments())

if (fragment?.isAdded == true) {
(fragment.provideNavigation().backToRoot() as? BaseFragment)?.onScrollToTop()
getString(R.string._shortcut_search).reportShortcutUsed(this)
}
}
}

private fun setOnDestinationChangedListener(destinationChangedListener: OnDestinationChangedListener?) {
supportFragmentManager.fragments.iterator().forEach { fragment ->
val navigation = (fragment as? RootFragment)?.provideNavigation()
navigation?.setOnDestinationChangedListener(destinationChangedListener)
}
}

private fun isInitialFragmentSelected(): Boolean {
return binding.bottomNavigationView.selectedItemId != initialFragmentId
private fun findNavController(): NavController {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.navHost) as NavHostFragment
return navHostFragment.navController
}

private fun selectInitialFragment() {
binding.bottomNavigationView.selectedItemId = initialFragmentId
}

private fun KClass<out Fragment>.customTag(): String = java.simpleName

private fun Fragment.customTag(): String = this::class.customTag()

private fun String.reportShortcutUsed() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val shortcutManager = getSystemService(ShortcutManager::class.java) as ShortcutManager
shortcutManager.reportShortcutUsed(this)
}
private fun handleBottomNavigationVisibility(destination: NavDestination) {
// Hide bottom navigation if Photo Details is opened.
binding.bottomNavigationView.isVisible = destination.id != R.id.photoDetails
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,31 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.github.sikv.photos.R
import com.github.sikv.photos.common.VoiceInputManager
import com.github.sikv.photos.common.ui.BaseFragment
import com.github.sikv.photos.common.ui.applyStatusBarsInsets
import com.github.sikv.photos.config.FeatureFlag
import com.github.sikv.photos.config.FeatureFlagProvider
import com.github.sikv.photos.databinding.FragmentSearchDashboardBinding
import com.github.sikv.photos.navigation.args.SearchFragmentArguments
import com.github.sikv.photos.navigation.args.withArguments
import com.github.sikv.photos.navigation.route.SearchRoute
import com.github.sikv.photos.recommendations.RecommendationsFragment
import com.github.sikv.photos.search.SearchFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

// TODO: Move to a separate module.

@AndroidEntryPoint
class SearchDashboardFragment : BaseFragment() {
class SearchDashboardFragment : Fragment() {

@Inject
lateinit var featureFlagProvider: FeatureFlagProvider

@Inject
lateinit var searchRoute: SearchRoute

private lateinit var voiceInputManager: VoiceInputManager

private var _binding: FragmentSearchDashboardBinding? = null
Expand Down Expand Up @@ -69,17 +73,11 @@ class SearchDashboardFragment : BaseFragment() {

override fun onDestroyView() {
super.onDestroyView()

_binding = null
}

override fun onScrollToTop() {}

private fun showSearchFragment(searchText: String? = null) {
val fragment = SearchFragment().withArguments(
SearchFragmentArguments(searchText)
)
navigation?.addFragment(fragment)
searchRoute.present(findNavController(), SearchFragmentArguments(searchText))
}

private fun setListeners() {
Expand Down
Loading

0 comments on commit 71f7cfb

Please sign in to comment.