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.
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.