Jetpack Compose In Fragment doesn't recompose on popbackstack - android

I'm using the jetpack navigation component here, not the Compose Navigation Component since there are some areas in my app I cannot convert yet.
The issue I'm running into is:
Fragment A starts and contains a ComposeView in it's xml. Fragment A opens Fragment B. But, when you return to Fragment A, the ComposeView is empty and doesn't show anything.
Is this a bug? Is this known? Am I just doing something wrong? I looked into setting the view composition strategy and tried each option and none of them worked.

I found the answer. I had a BaseFragment:
abstract class BaseFragment : Fragment() {
private var hasInitializedRootView = false
private var rootView: View? = null
protected abstract val layoutId: Int
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return getPersistentView(inflater, container, savedInstanceState, layoutId)
}
private fun getPersistentView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?, layout: Int): View? {
if (rootView == null) {
// Inflate the layout for this fragment
rootView = inflater?.inflate(layout, container, false)
} else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove rootView from the existing parent view group
// (it will be added back).
(rootView?.parent as? ViewGroup)?.removeView(rootView)
}
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!hasInitializedRootView) {
hasInitializedRootView = true
viewCreated(view, savedInstanceState)
}
}
abstract fun viewCreated(view: View, savedInstanceState: Bundle?)
}
What would happen is that onCreateView wouldn't be called as wanted and the composition wouldn't restart. Changing to using a normal Fragment fixed this perfectly.

Related

setOnClickListener not working in fragment in kotlin

I am try to call my imageid in fragment class but its show error and also show error in setOnClickListener.
I am try every think please if you have any answer then please share it.
//This is a code
class Home : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view: View = inflater!!.inflate(R.layout.fragment_home, container, false)
view.Notification.setOnClickListener { view -> //Notification is a id of imageView
Log.d("btnSetup", "Selected")
}
return view
}
}
To get a view from an id you need to call findViewById. In your case:
view.findViewById<View>(R.id.Notification).setOnClickListener { view ->
Log.d("btnSetup", "Selected")
}
The best way is to do in onViewCreated as follow:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<ImageView>(R.id.Notification).setOnClickListener { view ->
Log.d("btnSetup", "Selected")
}
}

BottomSheetDialog dims background being used in ViewPager2

I have a fragment that I want to display as an embedded fragment in a ViewPager and as a Bottom Sheet. I followed this https://developer.android.com/reference/android/app/DialogFragment#DialogOrEmbed and created a DialogFragment
private val mViewModel: CardPricesViewModel by viewModels()
private val binding by viewBinding(FragmentCardDetailPricesBinding::inflate)
companion object {
// This is the same value as the navArg name so that the SavedStateHandle can acess from either
const val ARG_SKU_IDS = "skuIds"
fun newInstance(skus: List<Long>?) =
CardDetailPricesFragment().apply {
arguments = Bundle().apply {
putLongArray(ARG_SKU_IDS, skus?.toLongArray())
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
However, when it gets inflated in a ViewPager the background dims as though it is a BottomSheetDialogFragment
However, when I manually do it with
supportFragmentManager
.beginTransaction()
.replace(binding.cardPricesFragmentContainer.id, cardDetailPricesFragment)
.commit()
It works fine. I see that the FragmentStateAdapter uses FragmentViewHolders instead of the using transactions directly (?), so I am not sure how to resolve this issue. I see that onCreateDialog() is being called, so if I call dismiss() after onViewCreated(), it works properly, but I am not sure if this a workaround
After some digging, I found the DialogFragment.setShowsDialog(boolean) method that you can use to disable the dialog being created.

How to use ViewBinding when referencing view IDs from "other" layout XML i.e, not the corresponding Fragment XML?

I was previously using Kotlin Synthetics.
Here are the relevant files:
view_error.xml (other layout XML)
RecipeDetailFragment.kt
fragment_recipe_detail.xml (corresponding Fragment XML)
Previous Code in short (Using Kotlin Synthetics)
import kotlinx.android.synthetic.main.view_error.*
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_recipe_detail, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// btnRetry is from view_error.xml. Accessed using Kotlin Synthetics
btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
}
Current Code Attempt in short: (Using ViewBinding)
So, here I successfully used ViewBinding for corresponding Fragment layout.
But I am not sure how to use ViewBinding for view_error.xml here to access btnRetry of view_error.xml?
What code is required to be added below?
import com.packagename.databinding.FragmentRecipeDetailBinding
private var _binding: FragmentRecipeDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentRecipeDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// NOW THE btnRetry gives error as I removed the kotlin synthetics imports.
// How to access btnRetry through ViewBinding?
btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
}
You must be using <include> element to use the external layout within fragment_recipe_detail. something like this
in fragment_recipe_detail.xml
<include
android:id="#+id/retryLayoutId"
layout="#layout/retryLayout"
/>
So now in the case of view binding you can access the viewbinding variable and access the external layout id and then access its children. Something like given below.
binding.retryLayoutId.btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
where layoutId is the included layout's id.

Different ways to use Fragments

I am developing an app with Firebase. But whenever I use the onViewCreated method, the button does not respond to any clicks. But when I use the onCreateView, it works.
Here is my LoginFragment (Button does not respond to clicks):
class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentLoginBinding.inflate(layoutInflater)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS NOT WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
}
}
In this code, my button responds to clicks:
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInFlater,
container: ViewGroup?,
savedInstanceState: Bundle?
) {
val view = inflater.inflate(R.layout.fragment_login, container, false)
val binding = FragmentLoginBinding.bind(view)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
return view
}
}
Can someone explain to me why the first approach did not work?
The problem is in the fact that in onViewCreated you are creating a binding object with FragmentLoginBinding.inflate(layoutInflater) but you are not connecting that binding to the view, so whatever you do with that object will not have effect on the view.
FragmentLoginBinding.inflate(layoutInflater) creates a new binding object and also inflate a new view to which it is connected. But you are not using that view in your fragment, so using that method is not the correct choice.
So you can do something like:
val binding = FragmentLoginBinding.bind(getView())
inside onViewCreated if you really want, and that will create a binding with the view you have in your fragment.
Said that, creating the binding already in onCreateView is actually recommended by the Android documentation.

Why does btnOpen return null when I invoke it on onCreateView with Kotlin? [duplicate]

How to use Kotlin Android Extensions with Fragments?
If I use them inside onCreateView(), I get this NullPointerException exception:
Caused by: java.lang.NullPointerException: Attempt to invoke virtual
method 'android.view.View android.view.View.findViewById(int)' on a
null object reference
Here is the fragment code:
package com.obaied.testrun.Fragment
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.obaied.acaan.R
import kotlinx.android.synthetic.main.fragment_card_selector.*
public class CardSelectorFragment : Fragment() {
val TAG = javaClass.canonicalName
companion object {
fun newInstance(): CardSelectorFragment {
return CardSelectorFragment()
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var rootView = inflater?.inflate(R.layout.fragment_card_selector, container, false)
btn_K.setOnClickListener { Log.d(TAG, "onViewCreated(): hello world"); }
return rootView
}
}
`
Kotlin synthetic properties are not magic and work in a very simple way. When you access btn_K, it calls for getView().findViewById(R.id.btn_K).
The problem is that you are accessing it too soon. getView() returns null in onCreateView. Try doing it in the onViewCreated method:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_K.setOnClickListener { Log.d(TAG, "onViewCreated(): hello world"); }
}
You are calling this btn_K too soon as at that time it returns a null and is giving you Null Pointer Exception.
You can use these views by this synthetic plugin in onActivityCreated() method which is called just after onCreateView() of Fragment lifecycle.
onActivityCreated()
{
super.onActivityCreated(savedInstanceState)
btn_K.setOnClickListener{}
}
Synthetic properties generated by Kotlin Android Extensions plugin needs a view for Fragment/Activity to be set before hand.
In your case, for Fragment, you need to use view.btn_K in onViewCreated
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.fragment_card_selector, container, false)
view.btn_K.setOnClickListener{} // access with `view`
return view
}
Or better, you should only access synthetic properties in onViewCreated
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_card_selector, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_K.setOnClickListener{} // access without `view`
}
Please notice that savedInstanceState parameter should be nullable Bundle?, and also check Importing synthetic properties
It is convenient to import all widget properties for a specific layout
in one go:
import kotlinx.android.synthetic.main.<layout>.*
Thus if the layout filename is activity_main.xml, we'd import
kotlinx.android.synthetic.main.activity_main.*.
If we want to call the synthetic properties on View, we should also
import kotlinx.android.synthetic.main.activity_main.view.*.
the only thing you need to do is:
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var rootView = inflater?.inflate(R.layout.fragment_card_selector, container, false)
rootView.btn_K.setOnClickListener { Log.d(TAG, "onViewCreated(): hello world"); }
return rootView
}
In Fragments please write your code in onActivityCreated:-
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.login_activity, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
callbackManager = CallbackManager.Factory.create()
initialization()
onClickLogin()
onClickForgot()
onClickSocailLogIn()
}
In my case nothing worked until I followed the advice from Otziii in the comments. Clean, rebuild (no restart needed), re-run the app. I also didn't need to go with onActivityCreated and just onCreateView did the trick.
One time I also made the error of inflating wrong layout, thus not getting the expected controls obviously.
no need to define companion object just call every id by a view like
lateinit var mView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mView=inflater.inflate(R.layout.product_list,container,false)
mView.addProduct.setOnClickListener {
val intent=Intent(activity,ProductAddActivity::class.java)
startActivity(intent)
} return mView
}
Adding it to #Egor Neliuba's answer, Yes whenever you call a view without reference, kotlinex looks for a rootView, and since you are inside a fragment and fragment doesn't have getView() method. Therefore it might throw NullPointerException
There are two ways to overcome this,
Either you override onViewCreated() as mentioned
Or If you want to bind views in some other class(say anonymous), you can simply create an extension function like this,
fun View.bindViews(){...}
The second approach is helpful, when you have a single fragment with multiple behaviour.
class CardSelectorFragment : Fragment() {
val TAG = javaClass.canonicalName
companion object {
fun newInstance(): CardSelectorFragment {
return CardSelectorFragment()
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var rootView = inflater?.inflate(R.layout.fragment_card_selector, container, false)
rootView?.findViewById<TextView>(R.id.mTextView)?.setOnClickListener{
Log.d(TAG, "onViewCreated(): hello world");
}
//btn_K.setOnClickListener { Log.d(TAG, "onViewCreated(): hello world"); }
return rootView
}
}
**Here you are using btn_K.setOnClickListener before finding
-You have to find the element form xml to your java/kotlin code by using findViewById then and then only you can perform operation on that view or element.
-So that's why null pointer execption you got
**

Categories

Resources