I'm trying to implement the android jetpack navigation component.
The case is almost the same as in the official doc: https://developer.android.com/guide/navigation/navigation-conditional
But my case is, if the user hasn't logged in yet where the login access will be recorded in the database (Room),
then the system will force the user to login.
I use https://developer.android.com/guide/navigation/navigation-global-action to declare
destination to the login fragment
Then in order, the definition is as follows
Home Fragment is startDestination,
If no user data has been found in the database, display the login page
If the login is successful, bring the user back to home and save data login to database.
I use MVVM Architecture and Coroutine which runs requests to REST
using Retrofit.
Here are some code that I used:
Navigation (xml)
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.depo.trask.ui.home.HomeFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_home">
<argument android:name="authentication"
android:defaultValue="none" />
<action
android:id="#+id/action_homeFragment_to_settingsFragment"
app:destination="#id/settingsFragment" />
</fragment>
<fragment
android:id="#+id/settingsFragment"
android:name="com.depo.trask.ui.settings.SettingsFragment"
android:label="#string/settings" />
<fragment
android:id="#+id/loginFragment"
android:name="com.depo.trask.ui.login.LoginFragment"
android:label="fragment_login"
tools:layout="#layout/fragment_login" />
<action
android:id="#+id/action_global_loginFragment"
app:destination="#id/loginFragment"
app:launchSingleTop="false"
app:popUpTo="#+id/nav_host_fragment"
app:popUpToInclusive="true" />
</navigation>
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var bottomNavigationView: BottomNavigationView
private lateinit var toolbar: Toolbar
private val navigationInMainScreen = setOf(
R.id.loginFragment,
R.id.homeFragment,
R.id.containerShippingFragment,
R.id.locationFragment,
R.id.historyFragment,
R.id.aboutFragment
)
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.Theme_MyTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavController()
}
override fun onSupportNavigateUp(): Boolean {
navController.navigateUp()
return super.onSupportNavigateUp()
}
// Nav Controller
private fun setupNavController() {
navController = findNavController(R.id.nav_host_fragment)
setupActionBar()
setupBottomNavigationBar()
navController.addOnDestinationChangedListener { _, destination, _ ->
// Listening toolbar
when(destination.id){
R.id.loginFragment, R.id.aboutFragment -> toolbar.visibility = View.GONE
else -> toolbar.visibility = View.VISIBLE
}
// Listening bottom navigation bar
when (destination.id) {
R.id.loginFragment, R.id.settingsFragment -> bottomNavigationView.visibility = View.GONE
else -> bottomNavigationView.visibility = View.VISIBLE
}
}
}
// Top Bar OR App Bar
private fun setupActionBar() {
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
val appBarConfiguration = AppBarConfiguration(
navigationInMainScreen
)
setupActionBarWithNavController(navController, appBarConfiguration)
}
// Bottom Navigation Bar
private fun setupBottomNavigationBar() {
bottomNavigationView = findViewById(R.id.bottom_navigation_view)
bottomNavigationView.setupWithNavController(navController)
}
}
Home Fragment
class HomeFragment : Fragment() {
?? Should I usede LoginViewModel ?
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeAuthenticationState()
}
private fun observeAuthenticationState() {
val navController = findNavController()
// REDIRECT USER TO LOGIN PAGE ?
// How To check it
navController.navigate(R.id.action_global_loginFragment)
}
}
Login Fragment
class LoginFragment : Fragment() {
private lateinit var viewModel: LoginViewModel // How can I shared this to home fragment
private lateinit var binding: FragmentLoginBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// For Retrofit
val networkConnectionInterceptor = NetworkConnectionInterceptor(requireContext())
val api = MyApi(networkConnectionInterceptor)
// For Room
val db = AppDatabase(context = requireActivity().applicationContext)
// For ViewModel
val repository = UserRepository(api, db)
val factory = LoginViewModelFactory(repository)
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_login,
container,
false
)
viewModel = ViewModelProvider(this, factory).get(LoginViewModel::class.java)
binding.buttonLogin.setOnClickListener { loginUser() }
return binding.root
}
private fun loginUser() {
val username = binding.editTextUsername.text.toString().trim()
val password = binding.editTextPassword.text.toString().trim()
binding.progressBar.show()
lifecycleScope.launch {
try {
val loginResponse = viewModel.userLogin(username!!, password!!)
if(loginResponse.user != null){
viewModel.saveLoggedInUser(loginResponse.user)
}
binding.progressBar.hide()
binding.root.context.toast(loginResponse.message!!)
} catch (e: ApiException) {
binding.progressBar.hide()
binding.root.context.toast( e.toString())
} catch (e: NoInternetException) {
binding.progressBar.hide()
binding.root.context.toast( e.toString())
}
}
}
}
My question ?
How to force users to log in fragments correctly on home fragments?
How to use LoginViewModel on HomeFragment?
Related
In my project that makes open new fragment with NavigationComponenet when click button. I want to test if fragment open when click button, But it don't work properly. Only it click button and does not open another fragment. So, I can't test if it works. Why it does not navigate?
#RunWith(AndroidJUnit4::class)
class WelcomeFragmentTestDoctor {
val phoneHelper = PhoneHelper
private lateinit var scenario: FragmentScenario<WelcomeFragment>
#Before
fun setup() {
scenario = launchFragmentInContainer(themeResId = R.style.AppTheme)
scenario.moveToState(Lifecycle.State.STARTED)
Intents.init()
}
#After
fun tearDown(){
Intents.release()
}
#Test
fun clickApplyAsADoctor(){
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
scenario.onFragment { fragment ->
navController.setGraph(R.navigation.auth_navigation)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(withId(R.id.buttonDoctor)).perform(click())
Assert.assertEquals(navController.currentDestination?.id, R.id.action_welcomeFragment_to_doctorRegistrationFragment)
}
}
fragment_doctor_registration.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/_14sdp"
android:paddingBottom="#dimen/_14sdp"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DoctorRegistrationFragment.kt
class DoctorRegistrationFragment : Fragment() {
private lateinit var mBinding: FragmentDoctorRegistrationBinding
private val mViewModel: DoctorRegistrationViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mBinding = FragmentDoctorRegistrationBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
DoctorRegistrationComponent.inject()
with(mBinding) {
backButton.setOnCrashOnClickListener {
findNavController().popBackStack()
}
btnSend.setOnCrashOnClickListener {
mViewModel.onEvent(
DoctorRegistrationInteractions.RegisterStart(fdr_name.text, fdr_surname.text,
fdr_title.text, fdr_diploma.text, fdr_branch.text,
inputLogin.lifEdittext.text.toString(), fdr_email.text, fdr_address.text,
fdr_company.text, fdr_tax.text
))
}
}
with(mViewModel) {
actions.map { it.getContentIfNotHandled() }.onEach(::handleActions).launchIn(viewLifecycleOwner.lifecycleScope)
}
}
private fun handleActions(action: DoctorRegistrationActions) {
when (action) {
is DoctorRegistrationActions.ErrorMessage -> PopupMessage.error(requireActivity(),message = action.message)
DoctorRegistrationActions.Init -> { }
is DoctorRegistrationActions.SuccessMessage -> {
PopupMessage.success(requireActivity(), message = action.message)
findNavController().popBackStack()
}
}
}
}
You need to check the fragment id not the Action id
Assert.assertEquals(navController.currentDestination?.id, R.id.doctorRegistrationFragment)
Assuming that doctorRegistrationFragment is the id of your fragment tag in your nav graph
<fragment
android:id="#+id/doctorRegistrationFragment"
/* rest of attrs */ >
The title itself is my problem, whenever I open MainActivity then navigate to another fragment available in the hamburger/drawer menu then press/swipe back to return in main screen (first fragment) it recreates. Is there away for Nav Component to make it not recreate the first fragment? I am using the Jetpack Navigation template generated by Android Studio and it seems that is the default behavior.
This is the MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private var _binding: ActivityMainBinding? = null
// This property is only valid between onCreate and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var drawerLayout: DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appBarMain.toolbar)
drawerLayout = binding.drawerLayout
val navView: NavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_content_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(setOf(
R.id.nav_home, R.id.nav_marketcap, R.id.nav_about), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
menu.findItem(R.id.action_settings).isChecked = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
return true
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START))
drawerLayout.closeDrawer(GravityCompat.START)
else
super.onBackPressed()
}
}
This is the Home Fragment (The first fragment in MainActivity) that holds child fragment AssetFragment
class HomeFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var viewPager : ViewPager2
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
viewPager = binding.viewPagerContainer
val bottomNav = binding.bottomNav
// val tabLayout = binding.tabLayout
val fragmentList : MutableList<Pair<String, Fragment>> = mutableListOf()
fragmentList.add(Pair(getString(R.string.assets), AssetFragment.newInstance()))
fragmentList.add(Pair(getString(R.string.news), NewsFragment.newInstance()))
fragmentList.add(Pair(getString(R.string.videos), VideosFragment.newInstance()))
val adapter = AppFragmentAdapter(fragmentList, this)
viewPager.adapter = adapter
viewPager.offscreenPageLimit = 2
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
bottomNav.menu.getItem(position).isChecked = true
homeViewModel.setTitle(adapter.getFragmentTabName(position))
}
})
val bottomNavListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when(item.itemId) {
R.id.page_1 -> {
// Respond to navigation item 1 click
viewPager.setCurrentItem(0, true)
true
}
R.id.page_2 -> {
// Respond to navigation item 2 click
viewPager.setCurrentItem(1, true)
true
}
R.id.page_3 -> {
// Respond to navigation item 3 click
viewPager.setCurrentItem(2, true)
true
}
else -> false
}
}
bottomNav.setOnNavigationItemSelectedListener(bottomNavListener)
// val layoutInflater : LayoutInflater = LayoutInflater.from(context)
//Connect TabLayout with ViewPager2
// TabLayoutMediator(tabLayout, viewPager){ tab, position ->
// tab.customView = prepareTabView(layoutInflater, tabLayout, adapter.getFragmentTabName(position), tabIcons[position])
// }.attach()
return root
}
// private fun prepareTabView(
// layoutInflater: LayoutInflater,
// tabLayout: TabLayout,
// fragmentName: String,
// drawableId: Int
// ): View {
//
// val rootView : View = layoutInflater.inflate(R.layout.main_custom_tab_text, tabLayout, false)
//
// val tabName : AppCompatTextView = rootView.findViewById(R.id.tabName)
//
// tabName.text = fragmentName
// tabName.setCompoundDrawablesWithIntrinsicBounds(null, AppCompatResources.getDrawable(requireContext(), drawableId), null, null)
//
// return tabName
//
// }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onResume() {
super.onResume()
requireView().isFocusableInTouchMode = true
requireView().requestFocus()
requireView().setOnKeyListener(object : View.OnKeyListener {
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
if (event!!.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
onBackPress()
return true
}
return false
}
})
}
fun onBackPress() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
requireActivity().onBackPressed()
}
}
This is one of the child fragment displayed in ViewPager hosted by parent fragment HomeFragment
class AssetFragment : Fragment() {
companion object {
fun newInstance() = AssetFragment()
}
private lateinit var viewModel: AssetViewModel
private var _binding: FragmentAssetsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var logTxt: AppCompatTextView
private lateinit var recyclerView: RecyclerView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentAssetsBinding.inflate(inflater, container, false)
val root: View = binding.root
recyclerView = binding.recyclerView
swipeRefreshLayout = binding.refreshLayout
logTxt = binding.errorLog
recyclerView.layoutManager = LinearLayoutManager(context)
adapter = AssetAdapter(requireContext(), this)
recyclerView.adapter = adapter
swipeRefreshLayout.isRefreshing = true
fetchAssets("30")
swipeRefreshLayout.setOnRefreshListener {
swipeRefreshLayout.isRefreshing = true
fetchAssets("30")
}
return root
}
private fun fetchAssets(limit: String) {
//Network stuff
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(AssetViewModel::class.java)
// TODO: Use the ViewModel
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Navigation xml
This are the fragments that will be shown in the drawer menu
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/nav_home">
<fragment
android:id="#+id/nav_home"
android:name="com.myapp.ui.home.HomeFragment"
android:label="#string/home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/nav_marketcap"
android:name="com.myapp.ui.marketcap.MarketCapFragment"
android:label="#string/marketCap"
tools:layout="#layout/fragment_marketcap" />
<fragment
android:id="#+id/nav_about"
android:name="com.myapp.ui.about.AboutFragment"
android:label="#string/about"
tools:layout="#layout/fragment_about" />
</navigation>
The menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item android:title="#string/menu">
<menu>
<item
android:id="#+id/nav_home"
android:icon="#drawable/ic_assets"
android:title="#string/home" />
<item
android:id="#+id/nav_marketcap"
android:icon="#drawable/ic_marketcap"
android:title="#string/marketCap" />
<item
android:id="#+id/nav_about"
android:icon="#drawable/ic_about"
android:title="#string/about" />
</menu>
</item>
</group>
<item android:title="#string/connect">
<menu>
<item
android:id="#+id/email_connect"
android:icon="#drawable/ic_email"
android:title="#string/fui_email_hint" />
</menu>
</item>
</menu>
Flow:
Open the app
Launching the MainActivity
Show HomeFragment (AssetFragment)
Open drawer menu
Select item e.g. About (AboutFragment)
Press/Swipe back
Problem here The HomeFragment onCreateView is being triggered once again
Expected behavior HomeFragment will no longer need to inflate view since we just literally make the user back to the very first destination. Unless a user itself press Home item in our drawer menu, that is the time HomeFragment will be recreated.
As per the Saving state with fragments guide, it is expected that your Fragment's view (but not the fragment itself) is destroyed and recreated when it is on the back stack.
As per that guide, one of the types of state is non config state:
NonConfig: data pulled from an external source, such as a server or local repository, or user-created data that is sent to a server once committed.
NonConfig data should be placed outside of your fragment, such as in a ViewModel. The ViewModel class inherently allows data to survive configuration changes, such as screen rotations, and remains in memory when the fragment is placed on the back stack.
So your fragment should never be calling fetchAssets("30") in onCreateView(). Instead, this logic should happen inside a ViewModel such that it is instantly available when the fragment returns from the back stack. As per the ViewModel guide, your fetchAssets should be done inside the ViewModel and your Fragment would observe that data.
I tried to use default setup of Jetpack Navigation Drawer generated by AS IDE but I have this problem where it always recreate fragments when switching/navigating instead of just adding a new fragment on the top? They say that it was intended but is there any solution to not recreate fragments even without handling ViewModel stuff?
This is the activity
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private var _binding: ActivityMainBinding? = null
// This property is only valid between onCreate and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var drawerLayout: DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appBarMain.toolbar)
drawerLayout = binding.drawerLayout
val navView: NavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_content_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(setOf(
R.id.nav_home, R.id.nav_marketcap, R.id.nav_about), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
menu.findItem(R.id.action_settings).isChecked = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
return true
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START))
drawerLayout.closeDrawer(GravityCompat.START)
else
super.onBackPressed()
}
}
Fragment
class AssetFragment : Fragment() {
companion object {
fun newInstance() = AssetFragment()
}
private lateinit var viewModel: AssetViewModel
private var _binding: FragmentAssetsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var logTxt: AppCompatTextView
private lateinit var recyclerView: RecyclerView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentAssetsBinding.inflate(inflater, container, false)
val root: View = binding.root
recyclerView = binding.recyclerView
swipeRefreshLayout = binding.refreshLayout
logTxt = binding.errorLog
recyclerView.layoutManager = LinearLayoutManager(context)
adapter = AssetAdapter(requireContext(), this)
recyclerView.adapter = adapter
swipeRefreshLayout.isRefreshing = true
fetchAssets("30")
swipeRefreshLayout.setOnRefreshListener {
swipeRefreshLayout.isRefreshing = true
fetchAssets("30")
}
return root
}
private fun fetchAssets(limit: String) {
//Network stuff
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(AssetViewModel::class.java)
// TODO: Use the ViewModel
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Navigation xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/nav_home">
<fragment
android:id="#+id/nav_home"
android:name=".ui.home.HomeFragment"
android:label="#string/home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/nav_marketcap"
android:name=".ui.marketcap.MarketCapFragment"
android:label="#string/marketCap"
tools:layout="#layout/fragment_marketcap" />
<fragment
android:id="#+id/nav_about"
android:name=".ui.about.AboutFragment"
android:label="#string/about"
tools:layout="#layout/fragment_about" />
</navigation>
Menu xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item android:title="#string/menu">
<menu>
<item
android:id="#+id/nav_home"
android:icon="#drawable/ic_crypto"
android:title="#string/home" />
<item
android:id="#+id/nav_marketcap"
android:icon="#drawable/ic_marketcap"
android:title="#string/marketCap" />
<item
android:id="#+id/nav_about"
android:icon="#drawable/ic_about"
android:title="#string/about" />
</menu>
</item>
</group>
<item android:title="#string/connect">
<menu>
<item
android:id="#+id/email_connect"
android:icon="#drawable/ic_email"
android:title="#string/fui_email_hint" />
</menu>
</item>
</menu>
onCreateView is getting called every time a fragment switch even pressing back button. I needed to avoid recreating Home fragment, just like when you regularly use fragment manager to add and pop fragment at the top and do not recreate the home fragment.
https://developer.android.com/guide/navigation/navigation-programmatic#share_ui-related_data_between_destinations_with_viewmodel
Any ViewModel objects created in this way live until the associated
NavHost and its ViewModelStore are cleared or until the navigation
graph is popped from the back stack.
In the fragment,
instead of
private lateinit var viewModel: AssetViewModel
put there
val viewModel: AssetViewModel
by navGraphViewModels(R.id.nav_host_fragment_content_main)
And remove
viewModel = ViewModelProvider(this).get(AssetViewModel::class.java)
I'm having a problem where when executing
findNavController(R.id.main_nav_host).navigateUp()
or
findNavController(R.id.main_nav_host).popBackStack()
Instead of going back to the last fragment in the backstack, it reopens/navigates to the same/current fragment.
Can somebody point me in the right direction why this is happening?
Navigation graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main_navigation_root"
app:startDestination="#+id/dest_main">
<fragment
android:id="#+id/dest_main"
android:name="com.example.popularmovies.ui.main.views.MainMoviesFragment"
android:label="#string/home"
tools:layout="#layout/fragment_main_movies">
<action
android:id="#+id/action_dest_main_to_dest_movie_details"
app:destination="#+id/dest_movie_details"
app:enterAnim="#anim/slide_in_right"
app:exitAnim="#anim/slide_out_left"
app:popEnterAnim="#anim/slide_in_left"
app:popExitAnim="#anim/slide_out_right" />
</fragment>
<fragment
android:id="#+id/dest_movie_details"
android:name="com.example.popularmovies.ui.details.movie.view.MovieDetailsFragment"
android:label="#string/movie_details"
tools:layout="#layout/fragment_movie_details"/>
</navigation>
MainActivity layout:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<fragment
android:id="#+id/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/main_navigation"/>
</FrameLayout>
MainActivity:
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
initNavUi()
}
override fun onBackPressed() {
findNavController(R.id.main_nav_host).popBackStack()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.main_nav_host).navigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
private fun initNavUi() {
val navController = Navigation.findNavController(this, R.id.main_nav_host)
appBarConfiguration = AppBarConfiguration(
setOf(R.id.dest_main)
)
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
}
}
Destination home fragment:
class MainMoviesFragment : Fragment(), Injectable, MovieViewHolder.MovieClickListener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var fragmentViewModel: MainMoviesFragmentViewModel
private lateinit var moviesRv: RecyclerView
private lateinit var moviesAdapter: MainMoviesAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews(view)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
fragmentViewModel = ViewModelProviders.of(this,viewModelFactory).get(MainMoviesFragmentViewModel::class.java)
fragmentViewModel.start()
observe()
}
override fun onMovieClicked(position: Int) {
fragmentViewModel.onMovieClicked(position)
}
private fun initViews(view: View) {
moviesRv = view.findViewById<RecyclerView>(R.id.fragment_main_movies_rv).apply{
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
moviesAdapter = MainMoviesAdapter(this#MainMoviesFragment)
adapter = moviesAdapter
}
}
private fun observe() {
fragmentViewModel.moviesLiveData.observe(this, Observer { moviesAdapter.submitList(it) })
fragmentViewModel.onMovieClickedLiveEvent.observe(this, Observer { handleMovieClickedEvent(it) })
}
private fun handleMovieClickedEvent(movieModel: MovieModel?){
val action = MainMoviesFragmentDirections.actionDestMainToDestMovieDetails()
findNavController().navigate(action)
}
}
Destination target fragment:
class MovieDetailsFragment : Fragment() {
private lateinit var viewModel: MovieDetailsFragmentViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_movie_details, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MovieDetailsFragmentViewModel::class.java)
}
}
The project code on GitHub can be found here
Your onMovieClickedLiveEvent, used in your MainMoviesFragmentViewModel, is firing every time you go back to your MainMoviesFragment since MutableLiveData saves the current value. This means that popBackStack() works just fine, but then you instantly get navigated back to the detail page (note: you'll still want to remove your code in onBackPressed() since right now you can't exit the app by hitting the back button).
It seems like, particularly with the name of the variable, that you should be using the SingleLiveEvent class, instead of MutableLiveData directly, as per this blog post.
Of course, there's no particular reason to use a LiveData or go through the ViewModel at all in this case. Your MovieViewHolder could pass the MovieModel directly to onMovieClicked, which could call handleMovieClickedEvent directly. That would avoid the use of LiveData (which is designed to store state, not events) and better model what you actually want to achieve: an event listener.
I am trying to implement Nagivation from Android Architecture Components. I am able to navigate successfully across my fragments. I am trying to attach it to my bottom navigation but I am unable to use it properly. I can navigate between fragments successfully with Navigation.findNavController(View).navigate(R.id.Fragment) but when I do that by using any UI component or back button, a highlight from my bottom navigation is not changing as shown in the following gif
My code is as follows for MainActivity.kt,
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
navController.navigate(R.id.homeFragment)
return#OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
navController.navigate(R.id.addEmotionFragment)
return#OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = Navigation.findNavController(this, R.id.nav_host)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
}
For HomeFragment.kt
class HomeFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.addEmotionFragment)
}
}
}
Try to use navigation-ui.
implementation 'androidx.navigation:navigation-ui:' + navigationVersion //currently 1.0.0-alpha05
in activity
navController = Navigation.findNavController(this, R.id.nav_host)
NavigationUI.setupWithNavController(bottomNavigation, navController)
And make sure your fragment id match menu id.
<item
android:id="#+id/homeFragment"
android:title="Home"/>
You need to manually set a checked property inside your button click like:
button.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.addEmotionFragment)
.... (Over Here)
}
The Java Code would be:
MenuItem menuItem = YourBottomViewObject.getMenu().getItem(1);
menuItemsetChecked(true);
MenuItem menuItem = YourBottomViewObject.getMenu().getItem(0);
menuItemsetChecked(false);