Search code examples
androidkotlinnavigation-drawer

How can I get the navigation drawer to work after navigating through several fragments in my Android project?


I'm using a navigation drawer for my app. It functions well when I initially open the app. I can navigate from the Home Fragment to the Borrow Fragment using the navigation drawer. However, after several navigations, it stops working.

I have two fragments within the navigation drawer: Home Fragment and Borrow Fragment. The Home Fragment leads to the Book Info Fragment, which, in turn, leads to the Maps Fragment. The Maps Fragment eventually leads to the Borrow Fragment. The navigation happens using the navigation directions.

When I'm in the Borrow Fragment through this route, I can't navigate back to the Home Fragment using the navigation drawer. It only pops up to the Home Fragment. Any insights on what might be causing this issue?

activity_main_drawer.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:id="@+id/nav_home"
            android:icon="@drawable/home_icon"
            android:title="@string/menu_home" />
        <item
            android:id="@+id/nav_borrow"
            android:icon="@drawable/book_icon"
            android:title="@string/menu_borrow" />
        <item
            android:id="@+id/nav_settings"
            android:icon="@drawable/setting"
            android:title="@string/menu_settings" />
    </group>
</menu>

mobile_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="com.example.bookradar.HomeFragment"
        android:label="@string/menu_home"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_nav_home_to_nav_book_info"
            app:destination="@id/nav_book_info"
            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/nav_borrow"
        android:name="com.example.bookradar.BorrowFragment"
        android:label="BorrowFragment"
        tools:layout="@layout/fragment_borrow">
        <argument
            android:name="item"
            android:defaultValue="@null"
            app:argType="com.example.bookradar.BookInfo"
            app:nullable="true" />
    </fragment>
    <fragment
        android:id="@+id/nav_maps"
        android:name="com.example.bookradar.MapsFragment"
        android:label="MapsFragment"
        tools:layout="@layout/fragment_maps">
        <action
            android:id="@+id/action_nav_maps_to_nav_borrow"
            app:destination="@id/nav_borrow" />
        <argument
            android:name="books"
            app:argType="com.example.bookradar.BookInfo[]"/>
    </fragment>
    <fragment
        android:id="@+id/nav_book_info"
        android:name="com.example.bookradar.BookInfoFragment"
        android:label="@string/nav_book_info"
        tools:layout="@layout/fragment_book_info">
        <argument
            android:name="item"
            android:defaultValue="@null"
            app:argType="com.example.bookradar.model.BookModel"
            app:nullable="true" />
        <action
            android:id="@+id/action_nav_book_info_to_nav_maps"
            app:destination="@id/nav_maps" />
    </fragment>


</navigation>

MainActivity.kt:

class MainActivity : AppCompatActivity() {
    private lateinit var appBarConfiguration: AppBarConfiguration
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setSupportActionBar(binding.appBarMain.toolbar)

        /*binding.appBarMain.fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()
        }*/

        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main) as NavHostFragment
        val navController = navHostFragment.navController

        setSupportActionBar(binding.appBarMain.toolbar)

        val drawerLayout: DrawerLayout = binding.drawerLayout
        val navView: NavigationView = binding.navView
        // 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_borrow, R.id.nav_maps
            ), 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)
        return true
    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment_content_main)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}

HomeFragment.kt:

class HomeFragment : Fragment() {

    private lateinit var adapter: MyBookRecyclerViewAdapter
    private lateinit var apiKey: String

    private lateinit var binding: FragmentHomeBinding
    private lateinit var recyclerView: RecyclerView
    private lateinit var bookList: MutableList<BookModel>
    var rootView: View? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        if (rootView != null) {
            return rootView as View
        }
        binding = FragmentHomeBinding.inflate(inflater, container, false)
        rootView = binding.root

        apiKey = "KakaoAK " + BuildConfig.KAKAO_API
        val searchBar = binding.searchBook
        recyclerView = binding.listBook
        bookList = mutableListOf<BookModel>()
        adapter = MyBookRecyclerViewAdapter(bookList,
            object : MyBookRecyclerViewAdapter.OnItemClickListener {
                override fun onItemClick(item: BookModel) {
                    val action = HomeFragmentDirections.actionNavHomeToNavBookInfo(item)
                    findNavController().navigate(action)
                }
            })
        recyclerView.layoutManager = LinearLayoutManager(context)
        recyclerView.adapter = adapter

        searchBar.clearFocus()
        var job: Job? = null
        searchBar.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
                    throwable.printStackTrace()
                }
                val prevListSize = bookList.size
                bookList.clear()
                adapter.notifyItemRangeRemoved(0, prevListSize)
                job?.cancel()
                job = lifecycleScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
                    try {
                        var response: BookListModel? = null
                        var prevSize = 0
                        var page = 1
                        while (response == null || response.meta.is_end.not()) {
                            response = RetrofitHelper.getInstance().getBooks(apiKey, query, page++)
                            bookList.addAll(response.documents)
                            withContext(Dispatchers.Main) {
                                adapter.notifyItemRangeChanged(prevSize, bookList.size)
                            }
                            prevSize = bookList.size
                        }
                    } catch (_: CancellationException) {
                        Log.d("Coroutine", "Job cancelled")
                    } catch (e: Exception) {
                        Log.e("Network Error", "Error fetching data", e)
                    }
                }
                return true
            }

            override fun onQueryTextChange(newText: String?): Boolean {
                // false if no custom behavior is defined.
                return false
            }

        })

        /* val textView: TextView = binding.textHome
         homeViewModel.text.observe(viewLifecycleOwner) {
             textView.text = it
         }*/
        return rootView as View
    }


}

BookInfoFragment.kt:

class BookInfoFragment : Fragment() {
    private lateinit var binding: FragmentBookInfoBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = FragmentBookInfoBinding.inflate(layoutInflater)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val item = arguments?.getParcelable<BookModel>("item")
        binding.textBookTitle.text = item?.title
        binding.textAuthor.text =
            item?.authors?.joinToString(", ") ?: getString(R.string.unknown_author)
        binding.textPublisher.text = item?.publisher
        binding.textIsbn.text = item?.isbn
        binding.textContent.text = item?.contents
        // implementation for fetching book info
        binding.floatingActionButton.setOnClickListener {
            val crawler = Crawler()
            val isbns = binding.textIsbn.text.split(' ')
            var bInfos: ArrayList<BookInfo>?
            lifecycleScope.launch {
                withContext(Dispatchers.IO) {
                    bInfos = crawler.getBookInfos(isbns)?.let { it1 -> ArrayList(it1) }
                    bInfos?.forEach {
                        it.book = item
                    }
                }
                if (bInfos != null) {
                    val action = BookInfoFragmentDirections.actionNavBookInfoToNavMaps(bInfos!!.toTypedArray())
                    findNavController().navigate(action)
                } else {
                    Toast.makeText(context, getString(R.string.no_book_found), Toast.LENGTH_SHORT).show()
                    Log.d("BookInfoFragment", "No book found")
                }
            }
        }
        Glide.with(this)
            .load(item?.thumbnail)
            .apply(bitmapTransform(BlurTransformation(25, 3)))
            .into(binding.imageBookCoverBlur)
        Glide.with(this)
            .load(item?.thumbnail)
            .into(binding.imageBookCover)

        // Inflate the layout for this fragment
        return binding.root
    }

    companion object {
        @JvmStatic
        fun newInstance(item: BookModel) = BookInfoFragment().apply {
            arguments = Bundle().apply {
                putParcelable("item", item)
            }
        }

    }
}

MapsFragment.kt:

class MapsFragment : Fragment(), GoogleMap.OnMarkerClickListener {

    private val callback = OnMapReadyCallback { googleMap ->
        /**
         * Manipulates the map once available.
         * This callback is triggered when the map is ready to be used.
         * This is where we can add markers or lines, add listeners or move the camera.
         * In this case, we just add a marker near Sydney, Australia.
         * If Google Play services is not installed on the device, the user will be prompted to
         * install it inside the SupportMapFragment. This method will only be triggered once the
         * user has installed Google Play services and returned to the app.
         */
        val school = LatLng(37.500062,126.868063)
        googleMap.setOnMarkerClickListener(this)
        googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(school, 20f))
        arguments?.getParcelableArray("books")?.forEach {
            val book = it as BookInfo
            val marker = googleMap.addMarker(
                MarkerOptions().position(book.library!!.location).title(book.library!!.getName(requireContext())).snippet(book.loc)
            )
            marker?.tag = book
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_maps, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
        Log.d("MapsFragment", arguments?.getParcelableArray("books")?.size.toString())
        mapFragment?.getMapAsync(callback)

    }

    override fun onMarkerClick(p0: Marker): Boolean {
        val book = p0.tag as BookInfo
        ModalBottomSheet().apply {
            this.book = book
        }.show(requireActivity().supportFragmentManager, ModalBottomSheet.TAG)
        return true
    }
}
class ModalBottomSheet : BottomSheetDialogFragment() {
    lateinit var book: BookInfo
    private lateinit var binding: BottomSheetBinding
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = BottomSheetBinding.inflate(inflater, container, false)
        binding.textLibraryName.text = book.library!!.getName(requireContext())
        binding.textLibraryLocation.text = book.loc
        binding.buttonBorrow.setOnClickListener {
            MapsFragmentDirections.actionNavMapsToNavBorrow(book).let {
                (requireActivity() as AppCompatActivity).supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main)!!
                    .findNavController().navigate(it)
                dismiss()
            }
        }
        return binding.root
    }


    companion object {
        const val TAG = "ModalBottomSheet"
    }
}

BorrowFragment.kt:

class BorrowFragment : Fragment() {
    lateinit var binding: FragmentBorrowBinding
    private var adapter: MemoAdapter? = null
    private var itemID: Int = 0
    private var dbHelper: DBHelper? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentBorrowBinding.inflate(inflater, container, false)

        val bInfo = arguments?.getParcelable<BookInfo>("item")
        if (bInfo != null) {
            val memo = Memo(
                bInfo.book!!.title!!,
                bInfo.library!!.getName(requireContext()),
                LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE).toString(),
                LocalDate.now().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE).toString(),
                bInfo.book!!.thumbnail!!
            )
            showManualEntry({ m ->
                addMemo(m)
            }, memo)
        }


        val dataList = mutableListOf<MutableMap<String, String>>()

        dbHelper = DBHelper(activity)
        val db = dbHelper?.readableDatabase
        val cursor = db?.rawQuery(DBContract.SQL_LOAD, null, null)
        while (cursor?.moveToNext()!!) {
            val item = mutableMapOf<String, String>()
            item["id"] = cursor.getInt(0).toString()
            item["title"] = cursor.getString(1)!!
            item["library"] = cursor.getString(2)!!
            item["borrow"] = cursor.getString(3)!!
            item["due"] = cursor.getString(4)!!
            item["image"] = cursor.getString(5)!!
            dataList.add(item)
            itemID = cursor.getInt(0)
        }
        cursor.close()

        adapter = MemoAdapter(this, dataList)
        binding.borrowingBookList.adapter = adapter

        binding.fab.setOnClickListener {
            showManualEntry({ memo ->
                addMemo(memo)
            })
        }

        return binding.root
    }

    /**
     * Calculate duration between two dates in days
     * @param fromTime start date
     * @param toTime end date
     * @return duration between two dates in days
     */
    fun calculateDuration(fromTime: LocalDate, toTime: LocalDate): Long {
        fromTime.until(toTime, java.time.temporal.ChronoUnit.DAYS)
        return fromTime.until(toTime, java.time.temporal.ChronoUnit.DAYS)
    }

    private fun addMemo(memo: Memo) {
        val item = mutableMapOf<String, String>()
        itemID++
        item["id"] = itemID.toString()
        item["title"] = memo.title
        item["library"] = memo.library
        item["borrow"] = memo.borrow
        item["due"] = memo.due
        item["image"] = memo.image
        adapter!!.datas.add(item)
        adapter!!.notifyItemChanged(itemID)

        DBContract.insertMemo(requireContext(), itemID, memo)
    }

    private fun calendarClickListener(
        eBinding: EditLayoutBinding,
        targetView: TextView
    ): View.OnClickListener {
        return View.OnClickListener {
            val today = LocalDate.now()
            DatePickerDialog(
                requireContext(),
                { _, year, month, dayOfMonth ->
                    val setDate = Calendar.getInstance()
                    setDate.set(year, month, dayOfMonth)

                    targetView.text = LocalDate.of(
                        setDate.get(Calendar.YEAR),
                        setDate.get(Calendar.MONTH) + 1,
                        setDate.get(Calendar.DAY_OF_MONTH)
                    ).format(DateTimeFormatter.ISO_LOCAL_DATE)

                    // calculate duration and update number picker
                    if (eBinding.editTextDueDate.text.isNotEmpty() && eBinding.editTextBorrowDate.text.isNotEmpty()) {
                        val borrowDate = LocalDate.parse(
                            eBinding.editTextBorrowDate.text,
                            DateTimeFormatter.ISO_LOCAL_DATE
                        )
                        val dueDate =
                            LocalDate.parse(
                                eBinding.editTextDueDate.text,
                                DateTimeFormatter.ISO_LOCAL_DATE
                            )
                        val duration = calculateDuration(borrowDate, dueDate)
                        eBinding.numberPickerDuration.value = duration.toInt()
                    }
                }, today.year, today.monthValue, today.dayOfMonth
            ).show()

        }
    }

    /**
     * Show alert dialog to manually enter the borrowed book information
     * @param callback callback function to handle the result
     * @param memo default values to show
     */
    fun showManualEntry(callback: (Memo) -> Unit, memo: Memo? = null) {
        val eBinding = EditLayoutBinding.inflate(layoutInflater).apply {
            // configure duration number picker
            numberPickerDuration.run {
                // min and max value of duration
                minValue = 1
                maxValue = 100

                setOnValueChangedListener { _, _, newVal ->
                    val borrowDate = LocalDate.parse(
                        editTextBorrowDate.text,
                        DateTimeFormatter.ISO_LOCAL_DATE
                    )
                    val dueDate = borrowDate.plusDays(newVal.toLong())
                    editTextDueDate.setText(dueDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
                }
            }
            imageButtonCalendarBorrowDate.setOnClickListener(
                calendarClickListener(
                    this,
                    editTextBorrowDate
                )
            )
            imageButtonCalendarDueDate.setOnClickListener(
                calendarClickListener(
                    this,
                    editTextDueDate
                )
            )

            // use provided values if exists
            if (memo != null) {
                editTextBookTitle.setText(memo.title)
                editTextLibrary.setText(memo.library)
                editTextBorrowDate.setText(memo.borrow)
                editTextDueDate.setText(memo.due)
                numberPickerDuration.value = calculateDuration(
                    LocalDate.parse(memo.borrow, DateTimeFormatter.ISO_LOCAL_DATE),
                    LocalDate.parse(memo.due, DateTimeFormatter.ISO_LOCAL_DATE)
                ).toInt()
            } else {
                // set the default date of due date to today
                editTextBorrowDate.setText(
                    LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE).toString()
                )
                editTextDueDate.setText(
                    LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE).toString()
                )
            }
        }

        // show alert dialog
        AlertDialog.Builder(activity).run {
            setTitle(getString(R.string.manual_entry))
            setView(eBinding.root)
            setPositiveButton("Ok") { _, _ ->
                val title = eBinding.editTextBookTitle.text.toString()
                val library = eBinding.editTextLibrary.text.toString()
                val borrow = eBinding.editTextBorrowDate.text.toString()
                val due = eBinding.editTextDueDate.text.toString()
                callback(Memo(title, library, borrow, due, memo?.image ?: ""))
            }
            setNegativeButton("Cancel", null)
            show()
        }

    }

    fun delMemo(id: Int) {
        val db = dbHelper?.writableDatabase
        db?.delete(DBContract.TABLE_NAME, "id=$id", null)
    }

    fun editMemo(pos: Int, memo: Memo) {
        adapter!!.datas[pos]["title"] = memo.title
        adapter!!.datas[pos]["library"] = memo.library
        adapter!!.datas[pos]["borrow"] = memo.borrow
        adapter!!.datas[pos]["due"] = memo.due
        adapter!!.notifyItemChanged(pos)

        DBContract.updateMemo(requireContext(), pos, memo)
    }
}

If my question is not clear, or I'm not giving enough context, I'm sorry. I'm a novice here :(

I clicked on the Home button to go from the Borrow Fragment to the Home Fragment, but it doesn't do anything. I can only pop up to the Home Fragment.


Solution

  • I think you are creating new nav graph instance. Just use Fragment.findNavController()

    Use:

    findNavController().navigate(
                    MapsFragmentDirections.actionNavMapsToNavBorrow(book)
                )
    

    instead of

    MapsFragmentDirections.actionNavMapsToNavBorrow(book).let {
                    (requireActivity() as AppCompatActivity).supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_main)!!
                        .findNavController().navigate(it)
                    dismiss()
                }
    

    Also, you can add android:menuCategory="secondary" in menu items. When using secondary category menu item fragment back stack is resets.