Android Navigation: shared element transitions not working between fragments - android-fragments

I followed the instructions here.
Here's my code:
val navDirections = PostsListFragmentDirections.actionPostsListFragmentToPostDetailFragment(post)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val animationExtras = FragmentNavigatorExtras(
title to title.transitionName,
body to body.transitionName
)
navigateWithAnimations(navDirections, animationExtras)
} else {
navigateTo(navDirections)
}
navigateTo and navigateWithAnimations are extensions functions:
fun Fragment.navigateTo(navDirections: NavDirections) {
NavHostFragment.findNavController(this).navigate(navDirections)
}
fun Fragment.navigateWithAnimations(navDirections: NavDirections, extras: Navigator.Extras) {
NavHostFragment.findNavController(this).navigate(navDirections, extras)
}
The code is being executed but the animations are not working.
Here's the nav_graph.xml
<fragment
android:id="#+id/postsListFragment"
android:name="PostsListFragment"
android:label="PostsListFragment">
<action
android:id="#+id/action_postsListFragment_to_postDetailFragment"
app:destination="#id/postDetailFragment">
<argument
android:name="post"
app:argType="Post"
app:nullable="true"
android:defaultValue="#null" />
</action>
</fragment>
<fragment
android:id="#+id/postDetailFragment"
android:name="PostDetailsFragment"
android:label="PostDetailFragment">
<argument
android:name="post"
app:argType="Post"
app:nullable="true"
android:defaultValue="#null" />
</fragment>
And here's how I'm setting the view into the second (destination fragment):
private val args: PostDetailsFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
args.post?.let { post ->
tv_title.text = post.title
tv_body.text = post.title
}
}
In both XML files I have defined:
android:transitionName="transition_title"
android:transitionName="transition_body"

Did you establish the sharedElementEnterTransition property in your second fragment? You would have to do something like this in the onCreate method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
main = requireActivity() as MainActivity
sharedElementEnterTransition = ChangeBounds().apply {
duration = 750
interpolator = AccelerateDecelerateInterpolator()
}
sharedElementReturnTransition = ChangeBounds().apply {
duration = 750
interpolator = AccelerateDecelerateInterpolator()
}
}
In other words, the receiving fragment needs to know how to deal with shared elements.

Related

The Firestore's subcollection return empty with no errors

I'm trying to make an application where I could retrieve the specific user's subcollection data.
Specifically my goal is to retrieve all of the plants contains in the plants collection.
Like that:
Here is the code on how I tried to retried the subcollection.
#Singleton
class PlantRepositoryImpl #Inject constructor(
private val userRef: CollectionReference
): PlantRepository {
override fun getPlantsFromFirestore() = callbackFlow {
val user = Firebase.auth.currentUser!!.uid
// Log.d("Data:","${user}")
val snapshotListener = userRef.document(user).collection("plants").orderBy("name").addSnapshotListener{ snapshot, e ->
val response = if (snapshot != null) {
val plant = snapshot.toObjects(Plant::class.java)
Success(plant)
} else {
Error(e?.message ?: e.toString())
}
trySend(response).isSuccess
}
awaitClose {
snapshotListener.remove()
}
}
Perhaps there could be the issue with the AppModule file itself, since I'm not experienced with the Hilt library.
Here is the code of the AppModule:
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
fun provideFirebaseFirestore()= Firebase.firestore
#Provides
fun provideUserRef(
db: FirebaseFirestore
)=db.collection("users")
#Provides
fun provideUserRepository(
userRef: CollectionReference
): PlantRepository = PlantRepositoryImpl(userRef)
#Provides
fun provideUseCases(
repo: PlantRepository
) = UseCases(
getPlants = GetPlants(repo),
addPlant = AddPlant(repo),
deletePlant = DeletePlant(repo),
editPlant= EditPlant(repo)
)
}
Here are the content of the Plant class.
data class Plant(
val id:String?=null,
val plant_category_id:String?=null,
val image:String?=null,
val name:String?=null,
val category:String?=null,
val room_location:String?=null,
)
Main activity class:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
IndoorPlantCareTheme {
val navController = rememberNavController()
SetupNavGraph(navController = navController)
}
}
}
}
And here is the the IndoorPlantCare.kt:
#HiltAndroidApp
class IndoorPlantCare: Application()
Here is the Manifest file:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.indoorplantcare">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".IndoorPlantCare"
android:allowBackup="true"
android:dataExtractionRules="#xml/data_extraction_rules"
android:fullBackupContent="#xml/backup_rules"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.IndoorPlantCare"
tools:targetApi="31"
tools:ignore="MissingClass">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="#string/app_name"
android:theme="#style/Theme.IndoorPlantCare">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
UI:
when (plantResponse) {
is Loading -> ProgressBar()
is Success ->LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
items(items = plantResponse.data) {
CardList(model = it) { model ->
Log.d("TAG", "Clicked on $model")
// navController.navigate(route = NavScreens.DetailScreen.name + "/$model")
}
}
}
is Error -> printError(plantResponse.message)
}
}
}
#Composable
fun CardList(model: Plant, onItemClick:(String) -> Unit = {} ){
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp,
start = 6.dp,
end = 6.dp,
)
.clickable {
onItemClick(model.id.toString())
}
.fillMaxWidth(),
elevation = 8.dp
) {
Column {
Image(
painter = rememberAsyncImagePainter(model = model.image) ,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(125.dp),
contentScale = ContentScale.Crop
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = 12.dp,
bottom = 12.dp,
start = 8.dp,
end = 8.dp
)
) {
Text(
text = model.name.toString(),
modifier = Modifier
.fillMaxWidth(0.85f)
.wrapContentWidth(Alignment.Start),
style = MaterialTheme.typography.h5
)
}
Text(
text = "Author : ${model.room_location}",
modifier = Modifier
.fillMaxWidth(0.85f)
.wrapContentWidth(Alignment.Start)
.padding(
top = 12.dp,
bottom = 12.dp,
start = 8.dp,
end = 8.dp
),
style = MaterialTheme.typography.h6
)
}
}
}
I have tried tinkering it, but unfortunately I started getting the Hilt related errors that I'm unsure on how to fix them.
Other that, I receive no error or even do not receive any debug messages when I tried to debug it in the Log.d("Data:","${user}") line.
In that case, it could be the issue with AppModule itself. I would appreciate if I could receive the guidance.

How can via kotlin mapOf map buttons key to value?

How can via Kotlin mapOf map buttons key to value?
I have such code.
reference to start_button is correct, by clickin on it I can open corect button in xml, but in debug mode buttonsMap value (start_button) is null.
class StartDialog : DialogFragment(){
private val buttonsMap: Map<String, Button> by lazy(LazyThreadSafetyMode.NONE) {
mapOf(
"startButton" to start_button
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.setContentView(R.layout.start_dialog)
displayButtons(Wrapper(requireContext())
return dialog
}
private fun displayButtons(wrapper: Wrapper) {
wrapper.queryButtons(object : Wrapper.OnQueryButtonListener {
override fun onSuccess(buttons: List<ButtonDetails>) {
buttons.forEach { button ->
buttonsMap[button.key]?.apply {
text = "${button.description}"
setOnClickListener {
wrapper.startProcess(requireActivity(), button)
}
}
}
}
})
}
}
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/start_button"
android:layout_width="300dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:layout_marginTop="15dp"
android:gravity="center"
android:textColor="#color/ef_white"
android:text="Start"
android:textSize="16sp">

Problems updating recycler views with sqlite database in kotlin

I am writing a code that deletes the item from the database when the button of the popup menu is pressed , and deletes it according to the recycler viewer.
selected item is deleted from the database, but not from the recycler view.
error code: No error code
PrintActivity.kt
class PrintActivity : AppCompatActivity() {
val helper = SqliteHelper(this, "myDB.sql", 1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vBinding = ActivityPrintBinding.inflate(layoutInflater)
setContentView(vBinding.root)
var recyclerViewAdapter = CustomAdapter()
recyclerViewAdapter.listData = helper.select()
vBinding.myRecyclerView.adapter = recyclerViewAdapter
vBinding.myRecyclerView.layoutManager = LinearLayoutManager(this)
vBinding.myRecyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
}
inner class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>(){
var listData = ArrayList<questionType>()
inner class Holder(val vBinding: QuestionLayoutRecyclerBinding) :
RecyclerView.ViewHolder(vBinding.root){
fun setData(id:Int?, question: String, answer: String, exp: String) {
vBinding.printId.text=id.toString()
vBinding.myLinear.setOnClickListener {
var myPopupMenu = PopupMenu(this#PrintActivity, it)
menuInflater?.inflate(R.menu.menu, myPopupMenu.menu)
var listener = PopupMenuListener()
myPopupMenu.setOnMenuItemClickListener(listener)
myPopupMenu.show()
}
}
inner class PopupMenuListener:PopupMenu.OnMenuItemClickListener{
override fun onMenuItemClick(p0: MenuItem?): Boolean {
listData[adapterPosition].id?.let { helper.delete(it) }
//here
this#CustomAdapter.notifyDataSetChanged()
return false
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.Holder {
val vBinding = QuestionLayoutRecyclerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val question = listData[position]
holder.setData(question.id, question.question, question.answer, question.exp)
}
override fun getItemCount(): Int {
return listData.size
}
}
}
You can try this approach using higher order function as callback mechanism.
One recommendation I would give is you can try to avoid these nested inner classes and make into separate files. Hope it works
inner class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>(){
var listData = ArrayList<questionType>()
fun deleteItem(position: Int) { //Function to remove recyclerview item
listData.remove(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, getItemCount())
}
inner class Holder(val vBinding: QuestionLayoutRecyclerBinding) :
RecyclerView.ViewHolder(vBinding.root){
fun setData(id:Int?, question: String, answer: String, exp: String,itemRemovedCallback:(a:Int)->Unit) { //higher order callback function
vBinding.printId.text=id.toString()
vBinding.myLinear.setOnClickListener {
var myPopupMenu = PopupMenu(this#PrintActivity, it)
menuInflater?.inflate(R.menu.menu, myPopupMenu.menu)
var listener = PopupMenuListener(itemRemovedCallback) //Passed to popupmenu listener
myPopupMenu.setOnMenuItemClickListener(listener)
myPopupMenu.show()
}
}
inner class PopupMenuListener(val itemRemovedCallback:(a:Int)->Unit):PopupMenu.OnMenuItemClickListener{
override fun onMenuItemClick(p0: MenuItem?): Boolean {
listData[adapterPosition].id?.let { helper.delete(it) }
itemRemovedCallback.invoke(adapterPosition) //Calling function. It will pass position to function in adapter
return false
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.Holder {
val vBinding = QuestionLayoutRecyclerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val question = listData[position]
holder.setData(question.id, question.question, question.answer, question.exp,::deleteItem) //Passing function as higher order function to viewholder
}
override fun getItemCount(): Int {
return listData.size
}
}

Android Data Binding + Mediator Live Data - Handling Lifecycle Events

In my Android app I have a fragment where the user can simultaneously view and edit some object's attributes.
I'm using an MVVM architecture with data binding and a mediator live data that holds the Relation object being edited. Here's how it works:
Fragment inflates and binds the view (the layout xml).
During this process the Fragment has a ViewModel made.
The ViewModel will fetch the Relation object (and its attributes) from the database, and put it in a MediatorLiveData.
Thanks to data binding and binding adapters, the editText fields get set to the object's attributes automatically.
The user can then edit these editText fields and save.
Upon saving, the ViewModel will get the texts from the editTexts and use them to update the Relation object in the local database
Here's the problem: upon rotating the screen, the fragment gets destroyed and recreated. But I have no way to restore the editText contents. The binding will just reset the editText contents (because we didn't actually update the Relation object attributes yet, we only do that when the user presses 'save').
I can't use a Bundle / savedInstanceState because the binding will just overwrite that. Using a MediatorLiveData to hold the edited contents won't work either because the ViewModel gets destroyed upon rotation, so we lose that data.
Portion of the fragment layout. Note the data variable (viewmodel) and the data binding in the relationNameEditText:
<?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"
tools:context=".presentation.relationdetail.RelationDetailFragment">
<data>
<variable
name="relationDetailViewModel"
type="be.pjvandamme.farfiled.presentation.relationdetail.RelationDetailViewModel" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="#dimen/relationDetailLayoutMargin">
<TextView
android:id="#+id/nameLabelTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="8dp"
android:text="#string/nameLabel"
app:layout_constraintBottom_toTopOf="#+id/relationNameEditText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/relationNameEditText"
android:layout_width="#dimen/relationNameEditWidth"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintBottom_toTopOf="#+id/editTextChips"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/nameLabelTextView"
app:relationName="#{relationDetailViewModel.relation}" />
The Fragment itself:
class RelationDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentRelationDetailBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_relation_detail,
container,
false
)
val application = requireNotNull(this.activity).application
val arguments = RelationDetailFragmentArgs.fromBundle(arguments!!)
val relationDataSource = FarFiledDatabase.getInstance(application).relationDao
val relationLifeAreaDataSource = FarFiledDatabase.getInstance(application).relationLifeAreaDao
val viewModelFactory =
RelationDetailViewModelFactory(
arguments.relationId,
relationDataSource,
relationLifeAreaDataSource,
application
)
val relationDetailViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(RelationDetailViewModel::class.java)
binding.relationDetailViewModel = relationDetailViewModel
binding.setLifecycleOwner(this)
// stuff about chips
val textWatcher = object: TextWatcher{ /* */ }
binding.saveButton.isEnabled = false
binding.relationNameEditText.addTextChangedListener(textWatcher)
binding.relationSynopsisEditText.addTextChangedListener(textWatcher)
binding.lifeAreaNowEditText.addTextChangedListener(textWatcher)
// etc.
relationDetailViewModel.enableSaveButton.observe(this, Observer{ /* */})
relationDetailViewModel.showNameEmptySnackbar.observe(this, Observer{ /* */})
relationDetailViewModel.navigateToRelationsList.observe(this, Observer{ /* */})
return binding.root
}
}
The viewmodel:
class RelationDetailViewModel (
private val relationKey: Long?,
val relationDatabase: RelationDao,
val relationLifeAreaDatabase: RelationLifeAreaDao,
application: Application
): AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val relation = MediatorLiveData<Relation?>()
fun getRelation() = relation
private val relationLifeAreas = MediatorLiveData<List<RelationLifeArea?>>()
fun getRelationLifeAreas() = relationLifeAreas
// other LiveData's with backing properties, to trigger UI events
init {
initializeRelation()
}
private fun initializeRelation(){
if(relationKey == null || relationKey == -1L) {
initializeNewRelation()
getAdorableAvatarFacialFeatures()
}
else {
retrieveAvatarUrl()
relation.addSource(
relationDatabase.getRelationWithId(relationKey),
relation::setValue)
relationLifeAreas.addSource(
relationLifeAreaDatabase.getAllRelationLifeAreasForRelation(relationKey),
relationLifeAreas::setValue)
}
}
private fun initializeNewRelation(){
uiScope.launch{
var relationId = insert(Relation(0L,"","",null,false))
initializeLifeAreasForRelation(relationId)
relation.addSource(
relationDatabase.getRelationWithId(
relationId!!),
relation::setValue)
relationLifeAreas.addSource(
relationLifeAreaDatabase.getAllRelationLifeAreasForRelation(
relationId!!),
relationLifeAreas::setValue)
}
}
private fun initializeLifeAreasForRelation(relationId: Long?){
if(relationId != null){
enumValues<LifeArea>().forEach {
uiScope.launch{
var relationLifeArea = RelationLifeArea(0L,relationId,it,"")
insert(relationLifeArea)
}
}
}
}
private fun retrieveAvatarUrl(){
uiScope.launch{
var rel = get(relationKey!!)
var avatarUrl = rel?.avatarUrl
if (avatarUrl.isNullOrEmpty()){
getAdorableAvatarFacialFeatures()
_enableSaveButton.value = true
}
else
_adorableAvatarString.value = rel?.avatarUrl
}
}
private fun getAdorableAvatarFacialFeatures(){
uiScope.launch{
var getFeaturesDeferred = AdorableAvatarApi.retrofitService.getFacialFeatures()
try{
var result = getFeaturesDeferred.await()
_adorableAvatarString.value = "https://api.adorable.io/avatars/face/" +
result.features.eyes.shuffled().take(1)[0] + "/" +
result.features.nose.shuffled().take(1)[0] + "/" +
result.features.mouth.shuffled().take(1)[0] + "/" +
result.features.COLOR_PALETTE.shuffled().take(1)[0]
relation.value?.avatarUrl = _adorableAvatarString.value
} catch(t:Throwable){
// ToDo: what if this fails?? -> Try again later!!
_adorableAvatarString.value = "Failure: " + t.message
}
}
}
fun onEditRelation(
relationNameText: String,
relationSynopsisText: String,
lifeAreaNowText: String,
// etc.
){
_enableSaveButton.value = !compareRelationAttributes(
relationNameText,
relationSynopsisText,
lifeAreaNow.Text,
// etc
)
}
private fun compareRelationAttributes(
relationNameText: String,
relationSynopsisText: String,
lifeAreaNowText: String,
// etc.
): Boolean {
// checks if any of the attributes of the Relation object were changed
// i.e. at least 1 of the editText fields has a text content that does
// does not equal the corresponding attribute of the Relation object
}
fun onSave(
name: String,
synopsis: String,
nowLA: String,
// etc.
){
if(!name.isNullOrEmpty()) {
uiScope.launch {
// update the DB
}
// TODO: this one should go away, need some sort of up button instead
_navigateToRelationsList.value = true
}
else
_showNameEmptySnackbar.value = true
}
// database suspend funs omitted
// ui event handling functions
override fun onCleared(){ /* cancel the viewModelJob */ }
}
The binding adapters:
#BindingAdapter("relationName")
fun TextView.setRelationName(item: Relation?){
item?.let{
text = item.name
}
}
#BindingAdapter("relationSynopsis")
fun TextView.setRelationSynopsis(item: Relation?){
item?.let{
text = item.synopsis
}
}
#BindingAdapter("relationLifeAreaNow")
fun TextView.setLifeAreaNowText(item: List<RelationLifeArea?>?){
item?.let{
item.forEach{
if(it?.lifeArea == LifeArea.EPHEMERA){
text = it.content
}
}
}
}
<!-- etc. -->
So my question is: how do I handle this?
I'm thinking the only solution would be to 1) hold a Relation object with the EDITED attributes, updated whenever the user edits the editText, 2) store this in the database.
But I don't think this would be architecturally sound. Nor am I sure if it would work.
In this case, you can try Two Way Data Binding

Fragment already added issue, when using navigation with bottom navigation bar android

I have a bottom navigation view which I have implemented in androidx navigation, but the problem is it is showing me the error of
java.lang.IllegalStateException: Fragment already added: BaseFragment{27d5b00 (2156a830-7756-4fc9-bc63-7c6f3d6705f0) id=0x7f08008c android:switcher:2131230860:0}
I have a base fragment which provides views to different fragments
class MainActivity : AppCompatActivity(),
ViewPager.OnPageChangeListener,
BottomNavigationView.OnNavigationItemReselectedListener,
BottomNavigationView.OnNavigationItemSelectedListener {
// overall back stack of containers
private val backStack = Stack<Int>()
// list of base destination containers
private val fragments = listOf(
BaseFragment.newInstance(R.layout.content_home_base, R.id.toolbar_home, R.id.nav_host_home),
BaseFragment.newInstance(R.layout.content_library_base, R.id.toolbar_library, R.id.nav_host_library),
BaseFragment.newInstance(R.layout.content_settings_base, R.id.toolbar_settings, R.id.nav_host_settings))
// map of navigation_id to container index
private val indexToPage = mapOf(0 to R.id.home, 1 to R.id.library, 2 to R.id.settings)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// setup main view pager
main_pager.addOnPageChangeListener(this)
main_pager.adapter = ViewPagerAdapter()
main_pager.post(this::checkDeepLink)
main_pager.offscreenPageLimit = fragments.size
// set bottom nav
bottom_nav.setOnNavigationItemSelectedListener(this)
bottom_nav.setOnNavigationItemReselectedListener(this)
// initialize backStack with elements
if (backStack.empty()) backStack.push(0)
}
/// BottomNavigationView ItemSelected Implementation
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val position = indexToPage.values.indexOf(item.itemId)
if (main_pager.currentItem != position) setItem(position)
return true
}
override fun onNavigationItemReselected(item: MenuItem) {
val position = indexToPage.values.indexOf(item.itemId)
val fragment = fragments[position]
fragment.popToRoot()
}
override fun onBackPressed() {
val fragment = fragments[main_pager.currentItem]
val hadNestedFragments = fragment.onBackPressed()
// if no fragments were popped
if (!hadNestedFragments) {
if (backStack.size > 1) {
// remove current position from stack
backStack.pop()
// set the next item in stack as current
main_pager.currentItem = backStack.peek()
} else super.onBackPressed()
}
}
/// OnPageSelected Listener Implementation
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(p0: Int, p1: Float, p2: Int) {}
override fun onPageSelected(page: Int) {
val itemId = indexToPage[page] ?: R.id.home
if (bottom_nav.selectedItemId != itemId) bottom_nav.selectedItemId = itemId
}
private fun setItem(position: Int) {
main_pager.currentItem = position
backStack.push(position)
}
private fun checkDeepLink() {
fragments.forEachIndexed { index, fragment ->
val hasDeepLink = fragment.handleDeepLink(intent)
if (hasDeepLink) setItem(index)
}
}
inner class ViewPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount(): Int = fragments.size
}
}
Base Fragment Class :
class BaseFragment: Fragment() {
private val defaultInt = -1
private var layoutRes: Int = -1
private var toolbarId: Int = -1
private var navHostId: Int = -1
private val appBarConfig = AppBarConfiguration(rootDestinations)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
layoutRes = it.getInt(KEY_LAYOUT)
toolbarId = it.getInt(KEY_TOOLBAR)
navHostId = it.getInt(KEY_NAV_HOST)
} ?: return
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return if (layoutRes == defaultInt) null
else inflater.inflate(layoutRes, container, false)
}
override fun onStart() {
super.onStart()
// return early if no arguments were parsed
if (toolbarId == defaultInt || navHostId == defaultInt) return
// setup navigation with toolbar
val toolbar = requireActivity().findViewById<Toolbar>(toolbarId)
val navController = requireActivity().findNavController(navHostId)
NavigationUI.setupWithNavController(toolbar, navController, appBarConfig)
// NavigationUI.setupWithNavController(toolbar,navController)
}
fun onBackPressed(): Boolean {
return requireActivity()
.findNavController(navHostId)
.navigateUp(appBarConfig)
}
fun popToRoot() {
val navController = requireActivity().findNavController(navHostId)
navController.popBackStack(navController.graph.startDestination, false)
}
fun handleDeepLink(intent: Intent) = requireActivity().findNavController(navHostId).handleDeepLink(intent)
companion object {
private const val KEY_LAYOUT = "layout_key"
private const val KEY_TOOLBAR = "toolbar_key"
private const val KEY_NAV_HOST = "nav_host_key"
fun newInstance(layoutRes: Int, toolbarId: Int, navHostId: Int) = BaseFragment().apply {
arguments = Bundle().apply {
putInt(KEY_LAYOUT, layoutRes)
putInt(KEY_TOOLBAR, toolbarId)
putInt(KEY_NAV_HOST, navHostId)
}
}
}
}
I tried empty the stack first but that did not work, i have three navgraphs for my three viewpager elements/ fragments.
We reverted back back to 2.1.0 (nav library) and it worked again
Error is not quite connected with Navigation library. It's most probably fragment's error itself. Try testing that fragment in empty activity without using Navigation.
Navigation library is somehow treating error wrongly.
p.s. in my case it was just and view's id missing in layout file.
Thanks for the support, I found the solution I created different navigation graphs for each page.

Resources