Skip to content

Instantly share code, notes, and snippets.

@yasukotelin
Created November 26, 2020 16:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yasukotelin/f4293dde2876b73b23a61d69ef028bf1 to your computer and use it in GitHub Desktop.
Save yasukotelin/f4293dde2876b73b23a61d69ef028bf1 to your computer and use it in GitHub Desktop.
android / architecture-components-samplesのNavigationExtension.ktをFragment呼び出しに対応したバージョン
class MainBottomNavigationFragment : DaggerFragment() {
private var _binding: FragmentMainBottomNavigationBinding? = null
private val binding get() = _binding!!
// activityViewModels()しないように注意
// activityスコープなどでViewModelを作りたい場合は別のViewModelを作ること(MainBottomNavigationViewModelなど
private val navigateExtensionsViewModel: NavigateExtensionsViewModel by viewModels()
private val navGraphIds = listOf(
R.navigation.navigation_home,
R.navigation.navigation_ranking,
R.navigation.navigation_search,
R.navigation.navigation_my_page,
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainBottomNavigationBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
this.lifecycleOwner = viewLifecycleOwner
}
if (savedInstanceState == null) {
setupBottomNavigation()
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
/*
通常のフローではこの条件には入らない。アクティビティが破棄されたときなどに、
ViewModelの値はクリアされているがBottomNavigationのViewは復元されるという場合がある。
その場合onViewCreatedではなくonViewStateRestoredでsetupする必要がある。
*/
if (savedInstanceState != null) {
setupBottomNavigation()
}
// BottomNavigationのnavigationに指定したDeep linkをハンドリングする
// 例えばnavigation_my_pageのdeep linkの場合は、マイページのBottomItemが選択された上でDeep link処理してくれる
handleDeepLink()
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
private fun setupBottomNavigation() {
binding.bottomNavigation.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = childFragmentManager,
containerId = R.id.nav_host_bottom_navigation_container,
viewModel = navigateExtensionsViewModel,
intent = requireActivity().intent,
)
}
private fun handleDeepLink() {
// BottomNavigationにセットしたNavigation graphへのDeep linkをハンドリング
binding.bottomNavigation.handleDeepLink(
navGraphIds = navGraphIds,
fragmentManager = childFragmentManager,
containerId = R.id.nav_host_bottom_navigation_container,
viewModel = navigateExtensionsViewModel,
intent = requireActivity().intent,
)
}
}
/*
* Copyright 2019, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Intent
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.core.util.set
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.bottomnavigation.BottomNavigationView
object NavigationExtensions {
/**
* [ViewModel.selectedItemId]で初期値としてセットされる必要がある定数
*/
const val INITIAL_SELECTED_ITEM_ID = -1
/**
* BottomNavigationを保持するFragment用のViewModelとして実装する必要があるViewModelインタフェース
*
* Fragmentのスコープで生成し、Activityのような広いスコープで保持しないようにすること
* (BottomNavigationの保持するselectedItemIdとViewModelのselectedItemIdが一致しなくなるため)
*/
interface ViewModel {
/**
* [BottomNavigationView.getSelectedItemId]はonViewCreatedではまだ復元されていないため、
* onViewCreatedで[setupWithNavController]を呼び出した際にもともと選択されていたItemが取得できないために、
* 意図しない挙動を取ってしまいます。
* そのため、selectされているItemIdをViewModelで保持し、ViewModelを[setupWithNavController]に渡すことで、
* 適切に復元処理を行います。
*
* また、この[selectedItemId]は選択されているItemIdと常に一致しているため、ロジックで使用することが可能です。
*
* 実装時の初期値は必ず[INITIAL_SELECTED_ITEM_ID]を代入する必要があることに注意してください。
*
* また、この[selectedItemId]に値をセットしてもBottomNavigationのItemは切り替わりません。
* 切り替えたい場合は[BottomNavigationView.setSelectedItemId]を使用してください。
* (セットされた値は自動で[selectedItemId]に反映されます。)
*/
var selectedItemId: Int
/**
* 各Itemで最初のFragmentが表示されている状態でItemを再タップされたときのListener
*/
fun onItemReselectedStartDestinationListener(itemId: Int)
}
}
/**
* Manages the various graphs needed for a [BottomNavigationView].
*
* Android Architecture Components samplesのExtensions関数をFragment対応した拡張版
*/
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
viewModel: NavigationExtensions.ViewModel,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
if (viewModel.selectedItemId == INITIAL_SELECTED_ITEM_ID) {
viewModel.selectedItemId = this.selectedItemId
}
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (viewModel.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[viewModel.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
viewModel.selectedItemId = item.itemId
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager, viewModel)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
fun BottomNavigationView.handleDeepLink(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
viewModel: NavigationExtensions.ViewModel,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent) &&
viewModel.selectedItemId != navHostFragment.navController.graph.id
) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager,
viewModel: NavigationExtensions.ViewModel,
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
val wasPopped = navController.popBackStack(
navController.graph.startDestination, false
)
if (!wasPopped) {
// ポップされてない == start destination
viewModel.onItemReselectedStartDestinationListener(item.itemId)
}
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
class NavigateExtensionsViewModel : ViewModel(), NavigationExtensions.ViewModel {
override var selectedItemId: Int = NavigationExtensions.INITIAL_SELECTED_ITEM_ID
override fun onItemReselectedStartDestinationListener(itemId: Int) {
}
}
@yasukotelin
Copy link
Author

https://github.com/android/architecture-components-samples/blob/main/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt
AndroidサンプルのExtensionはActivityから呼ばれる前提で書かれており、Fragmentから呼ぶとBottomNavigationViewの復元周りで動作がおかしくなるため、Fragment呼び出しに対応した版

  • Fragmentから呼び出されることを想定してBottomNavigationViewのselecteditemIdはViewModelで管理するようにしている
  • アクティビティを保持しない/画面回転などしても正常に動作するようにした
  • Deep linkのハンドリングをどのnavigationに指定してもハンドリングできるように呼び出しタイミングを変更した

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment