Instantly share code, notes, and snippets.
Created
November 26, 2020 16:26
-
Save yasukotelin/f4293dde2876b73b23a61d69ef028bf1 to your computer and use it in GitHub Desktop.
android / architecture-components-samplesのNavigationExtension.ktをFragment呼び出しに対応したバージョン
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class NavigateExtensionsViewModel : ViewModel(), NavigationExtensions.ViewModel { | |
override var selectedItemId: Int = NavigationExtensions.INITIAL_SELECTED_ITEM_ID | |
override fun onItemReselectedStartDestinationListener(itemId: Int) { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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呼び出しに対応した版