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

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.

Related

Horizontal Scrolling is not working in RowsSupportFragment Android Leanback?

I am trying to implement a rowsSupportFragment using Android Leanback for android tv.
But the vertical Scrolling is not working is Keeps glitching.
class DetailsRowListFragment: RowsSupportFragment() {
private var rootAdapter : ArrayObjectAdapter = ArrayObjectAdapter(
ListRowPresenter(
FocusHighlight.ZOOM_FACTOR_MEDIUM,true)
)
private var episodeItemSelectedListener: ((Episode)->Unit)?= null
private var relatedItemSelectedListener: ((Relation)->Unit)?= null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//setting up adapter from within the class
adapter = rootAdapter
//Setting up listener
onItemViewSelectedListener = ItemViewSelectedListener()
}
fun bindData(animeData : AnimeDetails ){
val episodeObjectAdapter = ArrayObjectAdapter(EpisodePresenter())
animeData.episodes.forEach { it->
episodeObjectAdapter.add(it)
}
val episodeHeaderItem= HeaderItem("Episodes")
val listRow = ListRow(episodeHeaderItem,episodeObjectAdapter)
rootAdapter.add(listRow)
}
fun setOnContentSelectedListener(listener: (Episode)->Unit){
this.episodeItemSelectedListener=listener
}
private fun setRelatedRow(itemList: List<Relation>){
val relatedObjectAdapter = ArrayObjectAdapter(RelatedPresenter())
itemList.forEach { it->
relatedObjectAdapter.add(it)
}
val headerItem = HeaderItem("Related Media")
val listRow = ListRow(headerItem,relatedObjectAdapter)
rootAdapter.add(listRow)
}
inner class ItemViewSelectedListener : OnItemViewSelectedListener {
override fun onItemSelected(
itemViewHolder: Presenter.ViewHolder?,
item: Any?,
rowViewHolder: RowPresenter.ViewHolder?,
row: Row?
) {
if( row !=null && row!!.headerItem!!.name=="Episodes" && item is Episode){
episodeItemSelectedListener?.invoke(item as Episode)
}else{
Log.d("Selection Listener","Listener Activated")
}
}
}
The Implemetation of My RowsSupportFragment Class is given above
enter image description here
Whenever I try to scroll side to side using the emulator's remote the row jsut glitches and comes back to its initial position.
But I am able to move the list by dragging with my mouse cursor.

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

Cannot fill a MutableLiveData of type ArrayList, outcome is always null

Im working on a quizgame and i want to store some ids in a MutableLiveData-arraylist. Therfore i made a function to loop all my documents in de database and add each ID to the arraylist. BUT the outcome is always null. I don't see where i go wrong?
I'm working with a MVVM-structure
GameViewModel:
class GameViewModel : ViewModel() {
// database instance
val db = FirebaseFirestore.getInstance()
// the current category
private val _category = MutableLiveData<String>()
val category: LiveData<String>
get() = _category
// the list of questionIds of the selected category
private val _questionIdsArray = MutableLiveData<ArrayList<Long>>()
val questionIdsArray: LiveData<ArrayList<Long>>
get() = _questionIdsArray
// the current question
private val _question = MutableLiveData<String>()
val question: LiveData<String>
get() = _question
/**
* Set Current Category
*/
fun SetCategory (categoryName: String){
_category.value = categoryName
}
/**
* Get the list of QuestionIds
*/
fun GetListQuestionIds() {
db.collection("questions")
.whereEqualTo("category", "$_category")
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
_questionIdsArray.value?.add(document.data["questionid"] as Long)
Log.d("GetSize","${_questionIdsArray.value?.size}")
}
Log.d("GetSize2","${_questionIdsArray.value?.size}")
}
.addOnFailureListener { exception ->
Log.w("errorforloop", "Error getting documents: ", exception)
}
}
/**
* Get a Question
*/
fun GetQuizQuestion() {
Log.d("retro","${_questionIdsArray.value?.size}")
db.collection("questions")
.whereEqualTo("category", "$_category")
.whereEqualTo("questionid", "${_questionIdsArray.value?.get(0)}")
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
_question.value = document.data["question"].toString()
}
}
.addOnFailureListener { exception ->
Log.w("err", "Error getting documents: ", exception)
}
}
GAMEFRAGMENT:
class GameFragment : Fragment() {
private lateinit var viewModel: GameViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentGameBinding.inflate(inflater)
// Get the viewModel
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
binding.lifecycleOwner = this
// Set the viewModel for DataBinding - this allows the bound layout access to all of the data in the VieWModel
binding.gameviewModel = viewModel
//arguments passed
val selectedCategory = arguments?.getString("selectedCategory")!!
//set current category so that the viewModel can use it
viewModel.SetCategory(selectedCategory)
viewModel.GetListQuestionIds()
viewModel.GetQuizQuestion()
return binding.root
}
If someone can enlighten me ...
Your Problem
You're not initializing the array. This is your code:
// the list of questionIds of the selected category
private val _questionIdsArray = MutableLiveData<ArrayList<Long>>()
val questionIdsArray: LiveData<ArrayList<Long>>
get() = _questionIdsArray
This declares a MutableLiveData of type ArrayList<Long>, but does not initialize it so its value defaults to null.
In your for loop you conditionally add items:
_questionIdsArray.value?.add(document.data["questionid"] as Long)
But of course value was never initialized so it's null so add is no-op (does nothing).
The Solution
Just ensure you initialize the live data object at some point.
You could do this inline in the declaration:
// the list of questionIds of the selected category
private val _questionIdsArray = MutableLiveData<ArrayList<Long>>(arrayListOf())
val questionIdsArray: LiveData<ArrayList<Long>>
get() = _questionIdsArray
Or during your attempt to populate the list:
.addOnSuccessListener { documents ->
val idsArray = arrayListOf<Long>() // Non-null list to add to
for (document in documents) {
idsArray.add(document.data["questionid"] as Long)
Log.d("GetSize","${idsArray.size}")
}
_questionIdsArray.value = idsArray // Now set live data with a valid list
Log.d("GetSize2","${_questionIdsArray.value?.size}")
}

FragmentPagerAdapter throwing IllegalArgumentException - Fragment Already Added

I have a ViewPager and a TabLayout containing 3 Fragments. At times when I swipe through(with the help of ViewPager) the 3 Fragments, an IllegalArgumentException is thrown with a message that Fragment is already added: ...(Fragment at index 0 to be specific). What could be the problem as the error log is only showing something to do with FragmentManager class and none of my Fragment classes? Below is the adapter(s) I am using
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.fragment.app.FragmentStatePagerAdapter
data class TitledFragment(val fragment: Fragment, val title: CharSequence?)
/**
* Should be used when displaying more than 2 [Fragment]s in a view-pager otherwise use
* [FragmentViewPagerAdapter]
*/
#Suppress("KDocUnresolvedReference")
class FragmentStateViewPagerAdapter(
private val titledFragmentList: List<TitledFragment>,
fragmentManager: FragmentManager
) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val tabCount = titledFragmentList.size
/**
* Selects the middle or first [Fragment] as the default selected [Fragment] from a list
* ([titledFragmentList]) of odd-numbered [Fragment]s and even-numbered [Fragment]s respectively.
* For example, a view-pager supposed to display 3 fragments ([titledFragmentList] = 3) will have
* it's default selected fragment position equal to 1(2nd [Fragment]) from the list whereas a
* view-pager supposed to display 2 fragments ([titledFragmentList] = 2) will have it's default
* selected fragment position equal to 0(1st [Fragment])
*/
val middleFragmentPosition: Int
get() = if (tabCount <= 0 || tabCount == 1) {
0
} else {
val fl = tabCount.toFloat() / 2
when {
tabCount % 2 == 0 -> fl.toInt() - 1
else -> fl.toInt()
}
}
override fun getPageTitle(position: Int): CharSequence? =
titledFragmentList[position].title ?: super.getPageTitle(position)
override fun getItem(position: Int): Fragment = titledFragmentList[position].fragment
override fun getCount(): Int = tabCount
}
/**
* Should be used when displaying less than 3 [Fragment]s in a view-pager otherwise use
* [FragmentStateViewPagerAdapter]
*/
#Suppress("KDocUnresolvedReference")
class FragmentViewPagerAdapter(
private val titledFragmentList: List<TitledFragment>,
fragmentManager: FragmentManager
) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val tabCount = titledFragmentList.size
/**
* Selects the middle or first [Fragment] as the default selected [Fragment] from a list
* ([titledFragmentList]) of odd-numbered [Fragment]s and even-numbered [Fragment]s respectively.
* For example, a view-pager supposed to display 3 fragments ([titledFragmentList] = 3) will have
* it's default selected fragment position equal to 1(2nd [Fragment]) from the list whereas a
* view-pager supposed to display 2 fragments ([titledFragmentList] = 2) will have it's default
* selected fragment position equal to 0(1st [Fragment])
*/
val middleFragmentPosition: Int
get() = if (tabCount <= 0 || tabCount == 1) {
0
} else {
val fl = tabCount.toFloat() / 2
when {
tabCount % 2 == 0 -> fl.toInt() - 1
else -> fl.toInt()
}
}
override fun getPageTitle(position: Int): CharSequence? =
titledFragmentList[position].title ?: super.getPageTitle(position)
override fun getItem(position: Int): Fragment = titledFragmentList[position].fragment
override fun getCount(): Int = tabCount
}
I think, you shouldn't hold references to fragments outside of adapter. They should be created inside the adapter. Probably the adapter can be written this way:
class FragmentStateViewPagerAdapter(
private val tabTitles: List<String>,
fragmentManager: FragmentManager
) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val tabs: List<Fragment> = (tabTitles.indices).map { YourFragment.newInstance(it) }
override fun getCount(): Int = tabTitles.size
override fun getItem(position: Int): Fragment = tabs[position]
override fun getPageTitle(position: Int): CharSequence {
// Generate a title depending on the position.
return tabTitles[position]
}
}
But your example also works. Maybe more code is required. Also strangely BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT is not required in my case (it shows error, but must not).
class MainActivity : AppCompatActivity() {
private lateinit var adapter: FragmentViewPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titledFragments = (1..3).map { TitledFragment(PlusOneFragment(), it.toString()) }
adapter = FragmentViewPagerAdapter(titledFragments, supportFragmentManager)
with(view_pager) {
adapter = this#MainActivity.adapter
}
}
}

Kotlin 2-way binding custom view

I have 1 custom view that extends ConstraintLayout and contains 1 EditText and 2 TextViews
On my custom view i define this attr (and others) :
<attr name="Text" format="string" />
and i use it like :
app:Text="#={login.email}"
Inside my custom view i define :
companion object {
#JvmStatic #BindingAdapter("Text")
fun setText(nMe : View, nText: String) {
nMe.nInput.setText(nText)
}
#InverseBindingAdapter(attribute = "Text")
fun getText(nMe : View) : String {
return nMe.nInput.text.toString()
}
witch works fine in one-way binding
app:Text="#{login.email}"
But when i try to use it in 2-way binding i get erros pointing to ActivityLoginBinding.java java.lang.String callbackArg_0 = mBindingComponent.null.getText(mEmail);
What to do to get 2-way binding?
L.E : After some research i end up with this :
#InverseBindingMethods(InverseBindingMethod(type =
CustomInput::class,attribute = "bind:Text",event =
"bind:textAttrChanged",method = "bind:getText"))
class CustomEditTextBinder {
companion object {
#JvmStatic
#BindingAdapter(value = ["textAttrChanged"])
fun setListener(editText: CustomInput, listener: InverseBindingListener?) {
if (listener != null) {
editText.nInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
listener.onChange()
}
})
}
}
#JvmStatic
#InverseBindingAdapter(attribute = "Text")
fun getText(nMe: CustomInput): String {
return nMe.nInput.text.toString()
}
#JvmStatic
#BindingAdapter("Text")
fun setText(editText: CustomInput, text: String?) {
text?.let {
if (it != editText.nInput.text.toString()) {
editText.nInput.setText(it)
}
}
}
}
}
But right now i get :
Could not find event TextAttrChanged
I think all you need is event = "android:textAttrChanged".
This works for me (set text to empty String if it is 0):
object DataBindingUtil {
#BindingAdapter("emptyIfZeroText")
#JvmStatic
fun setText(editText: EditText, text: String?) {
if (text == "0" || text == "0.0") editText.setText("") else editText.setText(text)
}
#InverseBindingAdapter(attribute = "emptyIfZeroText", event = "android:textAttrChanged")
#JvmStatic
fun getText(editText: EditText) = editText.text.toString()
}

Resources