While studying Kotlin, I got trouble with fragment sessions.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d("life_cycle", "F onViewCreated")
super.onViewCreated(view, savedInstanceState)
val pass: Button = findViewById(R.id.pass)
pass.setOnClickListener {
dataPassListener.onDataPass("Good bye")
}
}
I want to write val pass: Button = findViewById(R.id.pass), but I can't use findViewById().
I think the problem is that androidx.appcompat.app.AppCompatActivity is not imported.
Guessing from what you've written, this snippet is from one of your classes that extends some variant of the Fragment class.
If this is the case, what you want to do is:
val pass: Button = view.findViewById(R.id.pass)
The difference here is that findViewById() is invoked against the view instance passed in the lifecycle callback.
(This assumes that the R.id.pass resource is defined in the view inflated by this Fragment).
you have to use view.findViewById
val pass: Button = view.findViewById(R.id.pass)
Related
I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)
As per the android documentation, To get the data binding within a fragment, I use a non-nullable getter, but sometimes' When I try to access it again, after I'm wait for the user to do something, I receive a NullPointerException.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = ResultProfileBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setupViews() {
// On click listener initialized right after view created, all is well so far.
binding.btnCheckResult.setOnClickListener {
// There is the crash when trying to get the nonnull binding.
binding.isLoading = true
}
}
Does anyone know what the cause of the NullPointerException crash is? I'm trying to avoid not working according to the android documentation, and do not return to use nullable binding property (e.g _binding?.isLoading). Is there another way?
I can't explain why you're having any issue in the code above since a View's click listener can only be called while it is on screen, which must logically be before onDestroyView() gets called. However, you also asked if there's any other way. Personally, I find that I never need to put the binding in a property in the first place, which would completely avoid the whole issue.
You can instead inflate the view normally, or using the constructor shortcut that I'm using in the example below that lets you skip overriding the onCreateView function. Then you can attach your binding to the existing view using bind() instead of inflate(), and then use it exclusively inside the onViewCreated() function. Granted, I have never used data binding, so I am just assuming there is a bind function like view binding has.
class MyFragment: Fragment(R.layout.result_profile) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = ResultProfileBinding.bind(view)
// Set up your UI here. Just avoid passing binding to callbacks that might
// hold the reference until after the Fragment view is destroyed, such
// as a network request callback, since that would leak the views.
// But it would be fine if passing it to a coroutine launched using
// viewLifecycleOwner.lifecycleScope.launch if it cooperates with
// cancellation.
}
}
I have a custom dialog with X button, that dialog is called in a Fragment and here i have to manage all the clicks from the Alert.
Which is the best way to do so?
I actually was going to set the click listeners in the DialogFragment but i have to change some layout stuff and set variables from my Fragment so it will be better if i manage it from the fragment directly.
Here is my code now:
class ElencoDialog(private val testata: Testata, private val elimina: Boolean): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
}
}
}
And here is my fragment where i show the dialog:
class ElencoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false).show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true).show(parentFragmentManager, "ElencoDialog")
})
}
}
So instead of managing the click from the DialogFragment how can i manage the clicks directly from my Fragment?
First of all it is bad practice to have anything apart from the default constructor in a DialogFragment (or any Fragment). Although this might work initially, the system might need to recreate the fragment for various reasons, rotation, low memory etc. and it will attempt to use an empty constructor to do so. You should instead be using fragment arguments to pass simple data (covered in another question), a ViewModel for more complex data (I prefer this method anyway) or the new fragment results API, which I've outlined below.
But in answer to your specific question, to interact between your dialog fragment and main fragment, you have a few options:
Target fragment
You can set your original fragment as a target of your dialog but using setTargetFragment(Fragment). The fragment can then be retrieved safely from your dialog using getTargetFragment. It would probably be best practice to have your fragment implement an interface which you can cast to has the relevant callback methods.
Fragment results API
This is a relatively new API that attempts to replace the above, you can read more about it here: Communicating between fragments.
ViewModel
You can use a shared ViewModel scoped to the activity or parent fragment and keep your state in there. This would also solve the problem of having to pass your initial state through your fragment constructor. I won't explain how they work here as that's another question, but I would take a look here: ViewModel overview.
Pass callBack from fragment to your Dialog Fragment
class ElencoFragment() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
})
}
}
Your Dialog Fragment
class ElencoDialog(private val testata: Testata, private val elimina: Boolean, block : () -> Unit): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
block()
}
}
}
I have listed 3 fragments in my CustomActivity and set a button in the third fragment. I want to finish my CustomActivity by setting the setOnClickListener in CustomActivity using the button set in the third fragment. But I get the error message of: kotlin.KotlinNullPointerException. Please help me to remove this error.
The button of third fragment is android:id="#+id/returnButton"
This is CustomActivity code.
class CustomActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom)
val fragmentList = arrayListOf<Fragment>(
TutorialFragment(),
TutorialFragment2(),
TutorialFragment3()
)
val adapter = MyAdapter(supportFragmentManager, fragmentList)
viewPager.adapter = adapter
val button : Button = findViewById(R.id.returnButton)!!
button.setOnClickListener {
val intent = Intent(this,CustomActivity::class.java)
this.finish()
}
The thing you are doing wrong is accessing the view of the fragment i.e button and setting the listener in the Activity, which is wrong.
Here if you want to finish the activity from button click from fragment you can directly set the listener inside onViewCreated() method of the TutorialFragment.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
returnButton.setOnClickListener {
activity?.finish()
}
}
Also, please avoid using !! in your code.
The not-null assertion operator (!!) converts any value to a non-null type and throws an exception if the value is null.
Thus, if you want an NPE, you can have it, but you have to ask for it explicitly, and it does not appear out of the blue.
In the Fragment
activity?.let{
it.finish()
}
I'm starting bottomSheetDialogFragment from a fragment A.
I want to select the date from that bottomSheetDialogFragment then set it in the fragment A.
The select date is already done, I just want to get it in the fragment A to set it in some fields.
How can I get the value?
Any suggestions how to do it?
Create an interface class like this
public interface CustomInterface {
public void callbackMethod(String date);
}
Implement this interface in your Activity or Fragment. and make an object of this Interface.
private CustomInterface callback;
Initialize it in onCreate or onCreateView
callback=this;
Now pass this callback in your BottomSheetDialogFragment constructor when you call it.
yourBottomSheetObject = new YourBottomSheet(callback);
yourBottomSheetObject.show(getSupportFragmentManager()," string");
Now in your BottomSheetFragment's constructor
private CustomInterface callback;
public SelectStartTimeSheet(CustomInterface callback){
this.callback=callback;
}
And at last use this callback object to set your date
callback.callbackMethod("your date");
and yout will recieve this date in your Fragment or Your Activity in callbackMethod function.
override the constructor of a fragment is a bad practice as the document said:
Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state.
if you using another constructor that passing a callback as the param, when the fragment is resotored by the framework, your app crash
the recommend way is using viewModel and livedata.
Android navigation architecture component
eg:
Suppose you open Fragment B from Fragment A using navController.
and you want some data from fragment B to Fragment A.
class B :BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.your_layout, container, false)
root.sampleButton.setOnClickListener {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("your_key", "your_value")
dismiss()
}
}
and in your Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("your_key")
?.observe(viewLifecycleOwner) {
if (it == "your_value") {
//your code
}
}
you can use do as below:
Select Account Fragment code
class SelectAccountFragment(val clickListener: OnOptionCLickListener) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_fragment_accounts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = DataProcessorApp(context).allUsers
val rvAccounts = view.findViewById<RecyclerView>(R.id.rvAccounts)
rvAccounts.layoutManager = LinearLayoutManager(context)
rvAccounts.adapter = AccountsAdapter(context, list)
Log.e(tag,"Accounts "+list.size);
tvAccountAdd.setOnClickListener {
val intent = Intent(context,LoginActivity::class.java)
startActivity(intent)
}
tvManageAccounts.setOnClickListener {
Log.e(tag,"Manage Click")
clickListener.onManageClick()
}
}
interface OnOptionCLickListener{
fun onManageClick()
}
}
Now show and get call back into another fragment /activity as below
SelectAccountFragment accountFragment = new SelectAccountFragment(() -> {
//get fragment by tag and dismiss it
BottomSheetDialogFragment fragment = (BottomSheetDialogFragment) getChildFragmentManager().findFragmentByTag(SelectAccountFragment.class.getSimpleName();
if (fragment!=null){
fragment.dismiss();
}
});
accountFragment.show(getChildFragmentManager(),SelectAccountFragment.class.getSimpleName());
If you are using BottomSheetDialogFragment , since it's a fragment, you should create your interface and bind to it at onAttach lifecycle method of the fragment , doing the appropriate cast of activity reference to your listener/callback type.
Implement this interface in your activity and dispatch change when someone click in a item of fragment's inner recyclerview, for instance
It's a well known pattern and are explained better at here
One big advice is rethink your app architecture, since the best approach is to always pass primitive/simple/tiny data between Android components through Bundle, and your components are able to retrieve the required state with their dependencies later on.
For example, you should never pass along large Objects like Bitmaps, Data Classes , DTO's or View References.
first there is some serialization process going on regarding Parcel which impacts in app responsiveness
second it can lead you to TransactionTooLarge type of error.
Hope that helps!
You can also use LocalBroadcastManager. And as hglf said, it is better to keep the empty constructor for your fragment and use newInstance(Type value) instead to instantiate your fragment if you still want to use the interface callBack way.
You can use the benefit of Navigation library:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController();
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")) {
val result = navBackStackEntry.savedStateHandle.get<String>("key");
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
For more info, read the document.
The accepted answer is wrong.
What you can do is just user Fragment A's childFragmentManager when calling show().
like this:
val childFragmentManager = fragmentA.childFragmentManager
bottomSheetDialogFragment.show(childFragmentManager, "dialog")