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)
Related
When trying to write setHasOptionsMenu(true) in onCreate and override fun onCreateOptionsMenu as usual, Android Studio crosses out these functions saying that they are deprecated.
I looked at what they suggest
https://developer.android.com/jetpack/androidx/releases/activity?authuser=5#1.4.0-alpha01
and it turns out that they are asking to insert some new functions in Activity (MainActivity.kt) and some in Fragment (DogListFragment.kt). But in my app, all menu customization was done only in Fragment, so Activity can't do that. Activity simply doesn't have access to the RecyclerView, which is in the layout (fragment_god_list.xml) that belongs to Fragment. Activity only has androidx.fragment.app.FragmentContainerView in its activity_main.xml
Does anyone know how this can be done in Fragment without having to do anything with the menus in Activity?
GitHub project: https://github.com/theMagusDev/DogglersApp
MainActivity.kt:
package com.example.dogglers
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.dogglers.databinding.ActivityMainBinding
private lateinit var navController: NavController
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup view binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Setup navController
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
DogListFragment.kt:
package com.example.dogglers
import android.os.Bundle
import android.view.*
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.dogglers.adapter.DogCardAdapter
import com.example.dogglers.const.Layout
import com.example.dogglers.databinding.FragmentDogListBinding
class DogListFragment : Fragment() {
private var _binding: FragmentDogListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var recyclerView: RecyclerView
private var layoutType = Layout.VERTICAL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDogListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.verticalRecyclerView
setUpAdapter()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.layout_manu, menu)
val layoutButton = menu.findItem(R.id.action_switch_layout)
// Calls code to set the icon
setIcon(layoutButton)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_switch_layout -> {
layoutType = when (layoutType) {
Layout.VERTICAL -> Layout.HORIZONTAL
Layout.HORIZONTAL -> Layout.GRID
else -> Layout.VERTICAL
}
setUpAdapter()
return true
}
// Otherwise, do nothing and use the core event handling
// when clauses require that all possible paths be accounted for explicitly,
// for instance both the true and false cases if the value is a Boolean,
// or an else to catch all unhandled cases.
else -> return super.onOptionsItemSelected(item)
}
}
fun setUpAdapter() {
recyclerView.adapter = when(layoutType){
Layout.VERTICAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.VERTICAL
)
}
Layout.HORIZONTAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
DogCardAdapter(
context,
Layout.HORIZONTAL
)
}
else -> {
recyclerView.layoutManager = GridLayoutManager(context, 2, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.GRID
)
}
}
}
private fun setIcon(menuItem: MenuItem?) {
if (menuItem == null)
return
menuItem.icon = when(layoutType) {
Layout.VERTICAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_vertical_layout)
Layout.HORIZONTAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_horizontal_layout)
else -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
}
}
}
ActivityMain.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"/>
</FrameLayout>
FragmentDogList:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/vertical_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/vertical_list_item"/>
</FrameLayout>
When you call setUpActionBarWithNavController() method , you are setting up toolbar inside the activity. Your Fragment is inside this activity. Your fragment has this actionBar too. To use Menu provider inside fragment, you need to call below method inside of onViewCreated() method of fragment.
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
Also, You need to make your fragment implement MenuProvider Interface
class DogListFragment : Fragment(),MenuProvider {...
IDE will ask you to implement its provider method i.e onCreateMenu and onMenuItemSelected
Inside OnCreateMenu, use menu Inflator to inflate menu layout
example:-
menuInflater.inflate(R.menu.search_menu,menu)
I am working on an android app using Kotlin that lets a user record details about a vehicle then a list of saved vehicles is shown in a RecyclerView on the home screen. When there are no saved vehicles an empty view with a message is shown.
I have looked at other solutions like the ones on this page How to show an empty view with a RecyclerView? but my problem appears to be with the fragment lifecycle.
The problem I'm having is that when a vehicle is saved and the app returns to the home screen, the vehicle isn't shown unless you leave the app and come back, or try register another vehicle then cancel and go back to the home screen. In addition, when a saved vehicle is deleted, the app returns to the home screen but the empty view isn't shown unless again you leave that page and come back.
I'm using one activity with multiple fragments, a ViewModel to get the data, a ListAdapter and an EmptyDataObserver to check if there is data or not.
Here is a gif of the app
Here is the MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up view binding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Get the navigation host fragment from this Activity
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
// Instantiate the navController using the NavHostFragment
navController = navHostFragment.navController
// Make sure actions in the ActionBar get propagated to the NavController
setupActionBarWithNavController(this, navController)
}
/**
* Enables back button support. Simply navigates one element up on the stack.
*/
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
The HomeFragment layout
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
tools:context=".ui.fragments.HomeFragment">
<include
android:id="#+id/empty_data_parent"
layout="#layout/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/vehicles_list_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/vehicles_list_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/newVehicleFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:contentDescription="#string/new_vehicle_fab"
android:src="#drawable/ic_baseline_add_24"
android:tintMode="#color/white"
app:borderWidth="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
The HomeFragment
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val viewModel: VehiclesViewModel by activityViewModels {
VehiclesViewModelFactory((activity?.application as CarMaintenanceApplication).database.vehicleDao())
}
private lateinit var vehicleListAdapter: VehicleListAdapter
private lateinit var emptyDataView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// Retrieve and inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emptyDataView = view.findViewById(R.id.empty_data_parent)
setupRecyclerView(view)
binding.newVehicleFab.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToVehicleRegistrationFragment(
getString(R.string.register_vehicle)
)
this.findNavController().navigate(action)
}
}
private fun setupRecyclerView(view: View) {
vehicleListAdapter = VehicleListAdapter {
val action = HomeFragmentDirections.actionHomeFragmentToVehicleDetailsFragment(it.id)
view.findNavController().navigate(action)
}
binding.vehiclesListRecyclerView.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = vehicleListAdapter
}
viewModel.allVehicles.observe(this.viewLifecycleOwner) { vehicles ->
vehicleListAdapter.submitList(vehicles)
val emptyDataObserver = EmptyDataObserver(vehicles.isEmpty(), binding.vehiclesListRecyclerView,
emptyDataView)
vehicleListAdapter.registerAdapterDataObserver(emptyDataObserver)
}
}
/**
* Frees the binding object when the Fragment is destroyed.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
The Adapter class
class VehicleListAdapter(private val onItemClicked: (Vehicle) -> Unit) :
ListAdapter<Vehicle, VehicleListAdapter.VehicleViewHolder>(DiffCallback) {
private lateinit var context: Context
/**
* Provides a reference for the views needed to display items in the list.
*/
class VehicleViewHolder(private var binding: VehiclesListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(vehicle: Vehicle, context: Context) {
binding.apply {
vehicleName.text = context.getString(R.string.vehicle_name, vehicle.manufacturer, vehicle.model)
vehicleLicense.text = vehicle.licensePlate
vehicleOdometer.text = vehicle.mileage.toString()
}
}
}
/**
* Creates new views with R.layout.vehicles_list_item as its template.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VehicleViewHolder {
context = parent.context
val layoutInflater = VehiclesListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return VehicleViewHolder(layoutInflater)
}
/**
* Replaces the content of an existing view with new data.
*/
override fun onBindViewHolder(holder: VehicleViewHolder, position: Int) {
val currentVehicle = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(currentVehicle)
}
holder.bind(currentVehicle, context)
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Vehicle>() {
override fun areItemsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
return oldItem == newItem
}
}
}
}
The EmptyDataObserver
class EmptyDataObserver(
private val isEmpty: Boolean, private val recyclerView: RecyclerView?, private val emptyView: View?) :
RecyclerView.AdapterDataObserver() {
init {
checkIfEmpty()
}
private fun checkIfEmpty() {
emptyView?.visibility = if (isEmpty) View.VISIBLE else View.GONE
recyclerView?.visibility = if (isEmpty) View.GONE else View.VISIBLE
}
override fun onChanged() {
super.onChanged()
checkIfEmpty()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
checkIfEmpty()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
checkIfEmpty()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
checkIfEmpty()
}
}
The Navigation graph code
<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_graph.xml"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.pkndegwa.mycarmaintenance.ui.fragments.HomeFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_home">
<action
android:id="#+id/action_homeFragment_to_vehicleRegistrationFragment"
app:destination="#id/vehicleRegistrationFragment" />
<action
android:id="#+id/action_homeFragment_to_vehicleDetailsFragment"
app:destination="#id/vehicleDetailsFragment" />
</fragment>
<fragment
android:id="#+id/vehicleRegistrationFragment"
android:name="com.pkndegwa.mycarmaintenance.ui.fragments.VehicleRegistrationFragment"
android:label="{title}"
tools:layout="#layout/fragment_vehicle_registration">
<action
android:id="#+id/action_vehicleRegistrationFragment_to_homeFragment"
app:destination="#id/homeFragment"
app:popUpTo="#id/homeFragment"
app:popUpToInclusive="true" />
<argument
android:name="title"
app:argType="string" />
<argument
android:name="vehicle_id"
app:argType="integer"
android:defaultValue="-1" />
</fragment>
<fragment
android:id="#+id/vehicleDetailsFragment"
android:label="#string/vehicle_details"
tools:layout="#layout/fragment_vehicle_details">
<argument
android:name="vehicle_id"
app:argType="integer" />
<action
android:id="#+id/action_vehicleDetailsFragment_to_vehicleRegistrationFragment"
app:destination="#id/vehicleRegistrationFragment" />
<action
android:id="#+id/action_vehicleDetailsFragment_to_homeFragment"
app:destination="#id/homeFragment"
app:popUpTo="#id/homeFragment"
app:popUpToInclusive="true"/>
</fragment>
</navigation>
an image of the navGraph
What mistake I'm I making?
Edit - I got a solution. In the DAO file, the return type for getting a list of vehicles was a Flow object. When I changed it to LiveData it seems to work fine. I don't understand why yet because in the Android developer codelabs it's recommended to use Flow as the return type from database queries.
So, the previous query was
#Query("SELECT * FROM vehicles ORDER BY manufacturer ASC")
fun getAllVehicles(): Flow<List<Vehicle>>
And I changed it to
#Query("SELECT * FROM vehicles ORDER BY manufacturer ASC")
fun getAllVehicles(): LiveData<List<Vehicle>>
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'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?
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.