I have problem with backstack and toolbar. I have a singchat fragment and a user overlay. When I switch from the singchat fragment to the user overlay, I disable the mToolbar that is passed to all fragments from MainActivity and use AppBarLayout with scrollable behavior instead it. When I press "Negative Up" I go back to singlechat from the toolbar, but when I want to go back to the previous ContactFragment fragment, the toolbar switches to the mainToolbar and the next time I go to the fragments, the tools on the toolbar (such as Negative Up or Options Menu) disappear. (I think this is due to incorrect backstack configuration) Is there a way to fix this without changing the architecture of the application (without removing the toolbar from activity_main.xml and creating a toolbar for each fragment)? I will attach the code fragments below.
class SingleChatFragment(private val contact: CommonModel) :
BaseFragment(R.layout.fragment_single_chat) {
private lateinit var binding: FragmentSingleChatBinding
private lateinit var mListenerInfoToolbar: AppValueEventListener
private lateinit var mReceivingUser: User
private lateinit var mToolbarInfo: View
private lateinit var mRefUser: DatabaseReference
private lateinit var mRefMessages: DatabaseReference
private lateinit var mAdapter: SingleChatAdapter
private lateinit var mRecyclerView: RecyclerView
private lateinit var mMessagesListener: AppChildEventListener
private lateinit var mLayoutManager: LinearLayoutManager
private var mCountMessages = 20
private var mIsScrolling = false
private var mSmoothScrollToPosition = true
private val imageMultiMedia = registerForActivityResult(ActivityResultContracts
.PickMultipleVisualMedia(10)) { uris ->
if(uris.isNotEmpty()) {
Log.d("Photo Picker", "Items count ${uris.size}")
CoroutineScope(Dispatchers.IO).launch {
uploadImagesToDatabase(uris)
}
} else {
Log.d("Photo Picker", "No media")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSingleChatBinding.inflate(inflater, container, false)
return binding.root
}
override fun onResume() {
super.onResume()
initFields()
initToolbar()
initRecyclerView()
mToolbarInfo.setOnClickListener { replaceFragment(UserOverlay(mReceivingUser)) }
}
private fun initFields() {
mLayoutManager = LinearLayoutManager(this.context)
binding.chatInputMessage.addTextChangedListener(AppTextWatcher{
val string = binding.chatInputMessage.text.toString()
if(string.isEmpty()) {
binding.chatBtnSendMessage.visibility = View.GONE
binding.chatBtnAttach.visibility = View.VISIBLE
} else {
binding.chatBtnSendMessage.visibility = View.VISIBLE
binding.chatBtnAttach.visibility = View.GONE
}
})
binding.chatBtnAttach.setOnClickListener { openImagePicker() }
}
private fun openImagePicker() {
val mediaSelectionMax = 10
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaSelectionMax)
imageMultiMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
}
private fun uploadImagesToDatabase(uris: List<Uri>) {
for(uri in uris) {
val messageKey = REF_DATABASE_ROOT.child(NODE_MESSAGES).child(contact.id).push().key.toString()
val path = REF_STORAGE_ROOT.child(FOLDER_MESSAGES_IMAGES).child(messageKey)
val imageFileName = UUID.randomUUID().toString()
val imageRef = path.child(imageFileName)
imageRef.putFile(uri)
.addOnSuccessListener {
imageRef.downloadUrl.addOnSuccessListener {
sendMessageAsImage(contact.id, it.toString(), messageKey)
}
}
}
}
private fun initRecyclerView() {
mRecyclerView = binding.chatRecycleView
mAdapter = SingleChatAdapter(requireContext())
mRefMessages = REF_DATABASE_ROOT.child(NODE_MESSAGES).child(UID).child(contact.id)
mRecyclerView.adapter = mAdapter
mRecyclerView.layoutManager = mLayoutManager
mRecyclerView.setHasFixedSize(true)
mRecyclerView.isNestedScrollingEnabled = false
mMessagesListener = AppChildEventListener {
val message = it.getCommonModel()
if(mSmoothScrollToPosition) {
mAdapter.addItemToBottom(message) {
mRecyclerView.smoothScrollToPosition(mAdapter.itemCount)
}
} else {
mAdapter.addItemToTop(message)
}
}
mRefMessages.limitToLast(mCountMessages).addChildEventListener(mMessagesListener)
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
mIsScrolling = true
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (mIsScrolling && dy < 0 && mLayoutManager.findFirstVisibleItemPosition() <= 3) {
updateData()
}
}
})
}
private fun updateData() {
mSmoothScrollToPosition = false
mIsScrolling = false
mCountMessages += 10
mRefMessages.removeEventListener(mMessagesListener)
mRefMessages.limitToLast(mCountMessages).addChildEventListener(mMessagesListener)
}
private fun initToolbar() {
mToolbarInfo = APP_ACTIVITY.mToolbar.findViewById<ConstraintLayout>(R.id.toolbar_info)
mToolbarInfo.visibility = View.VISIBLE
mListenerInfoToolbar = AppValueEventListener {
mReceivingUser = it.getUserModel()
initInfoToolbar()
}
mRefUser = REF_DATABASE_ROOT.child(NODE_USERS).child(contact.id)
mRefUser.addValueEventListener(mListenerInfoToolbar)
binding.chatBtnSendMessage.setOnClickListener {
mSmoothScrollToPosition = true
val message = binding.chatInputMessage.text.toString()
if (message.isEmpty()) {
showToast("Input message")
} else sendMessage(message, contact.id, TYPE_TEXT) {
binding.chatInputMessage.setText("")
}
}
}
private fun initInfoToolbar() {
if (mReceivingUser.fullname.isEmpty()) {
mToolbarInfo.findViewById<TextView>(R.id.toolbar_chat_fullname).text = contact.fullname
} else mToolbarInfo.findViewById<TextView>(R.id.toolbar_chat_fullname).text =
mReceivingUser.fullname
mToolbarInfo.findViewById<ImageView>(R.id.toolbar_chat_image)
.downloadAndSetImage(mReceivingUser.photoUrl)
mToolbarInfo.findViewById<TextView>(R.id.toolbar_chat_status).text = mReceivingUser.state
}
override fun onPause() {
super.onPause()
mToolbarInfo.visibility = View.GONE
mRefUser.removeEventListener(mListenerInfoToolbar)
mRefMessages.removeEventListener(mMessagesListener)
hideKeyboard()
}
}
class UserOverlay(private val contact: User): Fragment(R.layout.fragment_user_account) {
private lateinit var binding: FragmentUserAccountBinding
private lateinit var mRefUserListener: AppValueEventListener
private lateinit var mRefUserInfoListener: AppValueEventListener
private val mRefUser: DatabaseReference = REF_DATABASE_ROOT.child(NODE_USERS).child(contact.id)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentUserAccountBinding.inflate(inflater, container, false)
val toolbar = binding.toolbar
val appBarLayout = binding.appbar
APP_ACTIVITY.mToolbar.visibility = View.GONE
(APP_ACTIVITY as AppCompatActivity).setSupportActionBar(toolbar)
(APP_ACTIVITY as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
}
appBarLayout.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener{ appBarLayout, verticalOffset ->
val maxScroll = appBarLayout.totalScrollRange
val percentage = abs(verticalOffset).toFloat() / maxScroll.toFloat()
if (percentage >= 0.96) {
binding.usernameImage.visibility = View.INVISIBLE
binding.statusImage.visibility = View.INVISIBLE
binding.profileImage.visibility = View.INVISIBLE
toolbar.visibility = View.VISIBLE
binding.profileImageSmallCollapsed.visibility = View.VISIBLE
binding.userNameCollapsed.visibility = View.VISIBLE
binding.statusCollapsed.visibility = View.VISIBLE
} else {
binding.usernameImage.visibility = View.VISIBLE
binding.statusImage.visibility = View.VISIBLE
binding.profileImage.visibility = View.VISIBLE
toolbar.visibility = View.GONE
binding.profileImageSmallCollapsed.visibility = View.INVISIBLE
binding.userNameCollapsed.visibility = View.INVISIBLE
binding.statusCollapsed.visibility = View.INVISIBLE
}
})
return binding.root
}
override fun onResume() {
super.onResume()
initToolbar()
initUserInfo()
}
private fun initUserInfo() {
mRefUserListener = AppValueEventListener {
val user = it.getUserModel()
binding.usernameImage.text = user.fullname
binding.statusImage.text = user.state
binding.profileImage.downloadAndSetImage(user.photoUrl)
binding.profileImageSmallCollapsed.downloadAndSetImage(user.photoUrl)
binding.userNameCollapsed.text = user.fullname
binding.statusCollapsed.text = user.state
}
mRefUserInfoListener = AppValueEventListener {
val userInfo = it.getCommonModel()
binding.userAccountBio.text = userInfo.bio
binding.userAccountPhone.text = userInfo.phone
binding.userAccountUsername.text = userInfo.username
//remove button if no data
if(binding.userAccountBio.text.isEmpty()) {
binding.container2.visibility = View.GONE
}
if(binding.userAccountUsername.text.isEmpty()) {
binding.container3.visibility = View.GONE
}
}
mRefUser.addValueEventListener(mRefUserListener)
mRefUser.addValueEventListener(mRefUserInfoListener)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewPager: ViewPager2 = binding.viewPager
val tabLayout: TabLayout = binding.tabLayout
val tabsPagerAdapter = TabsPagerAdapter(requireActivity(), contact)
viewPager.adapter = tabsPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when (position) {
0 -> { tab.text = "Media"
val customView = LayoutInflater.from(tabLayout.context)
.inflate(R.layout.tab_layout, tabLayout, false)
val tabText: TextView = customView.findViewById(R.id.tabText)
tabText.text = "Media"
tab.customView = customView}
1 -> { tab.text = "Voice"
val customView = LayoutInflater.from(tabLayout.context)
.inflate(R.layout.tab_layout, tabLayout, false)
val tabText: TextView = customView.findViewById(R.id.tabText)
tabText.text = "Voice"
tab.customView = customView
}
}
}.attach()
}
private fun initToolbar() {
binding.profileImageSmallCollapsed.downloadAndSetImage(contact.photoUrl)
binding.userNameCollapsed.text = contact.fullname
binding.statusCollapsed.text = contact.state
}
override fun onPause() {
super.onPause()
APP_ACTIVITY.mToolbar.visibility = View.VISIBLE
mRefUser.removeEventListener(mRefUserListener)
mRefUser.removeEventListener(mRefUserInfoListener)
}
}
Replace fragment function (replace only dataContainer)
fun replaceFragment(fragment: Fragment, addStack: Boolean = true){
if (addStack){
APP_ACTIVITY.supportFragmentManager.beginTransaction()
.addToBackStack(null)
.replace(R.id.dataContainer, fragment).commit()
} else {
APP_ACTIVITY.supportFragmentManager.beginTransaction()
.replace(R.id.dataContainer, fragment).commit()
}
}
And activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fullscreen_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/mainToolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
style="@style/mainToolbar">
<include
android:id="@+id/toolbar_info"
android:visibility="gone"
layout="@layout/toolbar_info"/>
</androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:id="@+id/dataContainer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/mainToolbar"
android:layout_height="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
toolbar_info works correct when i press negative up, but it's not works with appBar Here screens when i return back from single chat fragment: SingleChat Fragment ContactsFragment (With it's own toolbar)
And when i return back from UserOverlay Fragment: UserOverlay Fragment with appbar instead toolbar Contacts Fragment (with mainToolbar instead it's own)
I try to include Appbar Layout to mainToolbar in activity_main.xml, but it's impossible because it need toolbar, not appBar Layout.