I need a help.
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment, testFragment)
.addToBackStack(testFragment.tag)
.commit()
TestFragment.kt
→worked
TestAdapter.kt
→didn't work
→requireActivity() seems to be unavailable.
What is the difference? And what should I do to display TestFragment in TestAdapter.kt?
There is a button for click in TestAdapter.kt. That's why I need to do this.
An Adapter is not a Fragment, so it doesn't have the same functions available that Fragment does. If you need an Activity reference inside an Adapter (which you shouldn't because that is poor design--poor encapsulation), you need to add a constructor parameter or property to your class that allows you to pass an Activity reference to your Adapter. For example:
class ExpandableListAdapter(val activity: Activity): ListAdapter { //...
and then when you create it in your Fragment, you could pass requireActivity() as a constructor parameter.
But like I said, this would be poor encapsulation. Maybe you just need a Context reference for when you're creating view holders? In that case, you can get it from the parent parameter:
override fun createViewHolder(parent: ViewGroup, viewType: Int) {
val context = parent.context
// use context to create view holder's layout
}
Related
I am really noob into kotlin and I was trying to implement applandeo calendar library o my project in kotlin. Everything works well if you use activities but when changing into fragments I don't know how to give context because "this" is not working as a Context. In te function openDatePicker() the first parameter should be the context, but no idea about how to get it.
Also I don't know if its possible to pass from a fragment to an activity. My project is structured as a main activity with a bottom navigation bar where every elements redirects to the fragment. This code is inside one of those fragments. Any help or idea will be great ! :)
class CalendarFragment : Fragment(), OnDayClickListener, OnSelectDateListener{
private lateinit var binding: CalendarViewFragmentBinding
private val notes = mutableMapOf<EventDay, String>()
private lateinit var appContext: Context
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val context = this.context
// Inflate the layout for this fragment
val calendar_view = inflater.inflate(R.layout.calendar_view_fragment, container, false)
binding = CalendarViewFragmentBinding.inflate(layoutInflater)
binding.fabButton.setOnClickListener { openDatePicker() }
binding.calendarView.setOnDayClickListener(this)
return calendar_view
}
private fun openDatePicker() {
DatePickerBuilder(************, this)
.pickerType(CalendarView.ONE_DAY_PICKER)
.headerColor(R.color.md_theme_light_primary)
.todayLabelColor(R.color.md_theme_light_primary)
.selectionColor(R.color.md_theme_light_secondary)
.dialogButtonsColor(R.color.md_theme_light_secondary)
.build()
.show()
}
I tried functions such as requireContext(), requireActivity(), requireContext().applicationContext, this.context, but no one working as I expect.
Your Fragment is attached to its context when its onAttach() callback is invoked. This happens before it reaches the CREATED lifecycle state:
When your fragment reaches the CREATED state, it has been added to a FragmentManager and the onAttach() method has already been called.
What this means is that by the time onCreate is called (or any lifecycle callbacks after that, including onCreateView, onViewCreated, onStart etc.) your fragment will have a context, and you can access it using requireContext().
You could also use getContext(), which you can access as a property with context, but that returns null if the Fragment isn't associated with a context yet - meaning you have to null-check and handle that possibility. requireContext() will throw an exception if you don't have that context yet, but if you're making sure to only call it when the fragment is in the CREATED state (or later) then it will be safe, and you won't need to check the return value. This is the recommended way of doing things.
So as long as you're accessing the Context in a lifecycle callback like onCreateView, requireContext() will work - that's how you get the Fragment's context. Because you're calling openDatePicker from inside onCreateView, you can just use requireContext() in that function - it's safe at that point!
But you can't define it as a normal top-level variable like this:
class MyFragment : Fragment {
var appContext: Context = requireContext()
}
because that variable is assigned at construction time, which happens way before the fragment reaches the CREATED state. You don't have access to the context at construction time, so any top-level stuff that requires it will end up throwing an exception. This goes for Activities too! That's why you have to assign stuff later, like in onCreate (and this is where lateinit comes in useful, you can have a top-level variable without having to assign it before you're ready)
And no, you can't pass this as a Context when you're in a Fragment, because a Fragment isn't a Context. It works for an Activity because that is a Context - to be more accurate it's able to provide one, but that only works after it's in the CREATED state. That's why you can get in trouble using it in top-level declarations like this
class MyActivity : AppCompatActivity {
val thing = ThingThatRequiresContext(this)
}
because like we just talked about, at construction time the Activity doesn't have access to a context, and the thing that requires it (if it tries to access it immediately) will end up throwing an exception. You see that error a lot, where an Activity can't be started because something like this is happening during initialisation.
Just use it from Fragment. Here is the documentation
private fun openDatePicker() {
DatePickerBuilder(requireContext(), this)
.pickerType(CalendarView.ONE_DAY_PICKER)
.headerColor(R.color.md_theme_light_primary)
.todayLabelColor(R.color.md_theme_light_primary)
.selectionColor(R.color.md_theme_light_secondary)
.dialogButtonsColor(R.color.md_theme_light_secondary)
.build()
.show()
}
Hi fragment has the context. You just have to use context instead of this, and if you want a context that is never null, use requireContext()
I have a compound view that I want to create its viewmodel by ViewModelLazy, I need to send the ViewModelStoreOwner of the view to ViewModelLazy but trying to get the ViewModelStoreOwner using ViewTreeViewModelStoreOwner.get(this) always returns null. The compound view itself is a simple view, but I am using it in a recyclerview adapter that resides in a fragment. Right now, I am getting forced to use the parent fragment ViewModelStoreOwner, which is causing all the items in the adapter to have the same viewmodel instance. I searched for an example on how to use ViewTreeViewModelStoreOwner but I can't find one, am I missing something?
Note: I am injecting the viewmodel by dagger-hilt
This example shows how it can be implemented in custom view.
class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.summaryModel.observe(findViewTreeLifecycleOwner()!!, ::populateSummaryView)
}
private fun populateSummaryView(summaryModel: SummaryModel) {
// do stuff
}
}
Found this great example here
1, you need update
androidx.activity to 1.2.0
androidx.fragment to 1.3.0
2, activity or fragment set its ViewModelStore to rootViw via setTag, so any view in activity or fragment's rootView gets the same ViewModelStoreOwner and also same ViewModelStore.
// ComponentActivity line 409
ViewTreeViewModelStoreOwner.set(getWindow().getDecorView(), this)
// ViewTreeViewModelStoreOwner line 50
view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);
from reading many website im aware that the optimal way handling onclick on recycerview was to set clicklistener not in OnBindViewHolder, its either in onCreateViewHolder(explained in here) or pass the clickListener in "bind" method(explained in here).
but one thing that really bother me, was how clickListener handled in activity / fragment
val adapter : MyAdapter(){
//click goes here
}
it may be not much but, honestly its not so so readable
i prefer to handle click listener not in constuctor, but with on separate method, meyabe like this
adapter.onItemClickListner = {
val adapter = MyAdapter()
//click more readable, yay!
}
but im not sure, if its will affect performance or not, or do i really need to stick with click on constructor?
I myself have used this method before.. Instead of passing the listener in the constructor, I have set it explicitly from the method call as you described. When I used that approach I had to make sure in the adapter that i checked for initialization of the listener and its null safety. So, you'll have to make sure before calling that it's absolutely present and initialized before calling the listener methods in the adapter.
I gradually moved to constructor as I didn't want to have such conscious checks all over my adapter before any click. (When there were like multiple clickable views.)
create a interface which has some function to handle your onClick.
Now you need to implement this interface in your activity class like this :-
class MainActivity : InterfaceAdapterOnClick (Here InterfaceAdapterOnClick is an interface which will have some functions to handle your onClick)
Let's suppose we have one function named onClickItem in our interface.
So now when you create your adapter like this adapter = MyAdapter() you can pass your interface with that. So it should look like :-
var adapter = MyAdapter(this) (this because you have already implemented interface in your mainActivity class) and your adapter should look like :-
class MyAdapter(onClickHandler: InterfaceAdapterOnClick) {}
So with this at the end you will have a seperate interface method in your mainActivity and your mainAcitvity should look like :-
class MainActivity : InterfaceAdapterOnClick {
// This will be your seperate onClick handler for your recyclerView item
override func onClickItem(// You can take some parameter as input here like `posistion` or `adapterItem` here ) {
}
}
IMHO, If your adapter needs that itemClickListener to function properly then you should pass it through its constructor and passing it using a setter function is not good since you might forgot to pass it at some point you forget to pass the listener and waste your time to check why clicking on items does not work! (It happened to me :D). If you are concerned about readability then you can use an interface and pass the implementation to adapter or use some features of Kotlin like named parameter or pass the callable reference of a function to make it more readable. For example something like this
class MyAdapter(onItemClick: (MyData) -> Unit) {
// ...
}
And initialize it like this
val adapter = MyAdapter(
onItemClick = { data ->
// ...
}
)
or pass the function like this
class MyActivity: AppCompatActivity() {
override fun onCreateView(/**/) {
// ...
val adapter = MyAdapter(
onItemClick = ::onItemClick
)
// or
// val adapter = MyAdapter(::onItemClick)
}
private fun onItemClick(data: MyData) {
}
}
you can simply pass an interface between the activity and the adapter class but the best method is by passing a function to your adapter class, like this
adapter = BasketRVA { basketEntity: BasketEntity, minus: Boolean -> changeQuantity(basketEntity, minus) }
private fun changeQuantity(basketEntity: BasketEntity, minus: Boolean) {
///your code
}
and in the adapter type this
class BasketRVA(private val clickListener: (BasketEntity, Boolean)->Unit): RecyclerView.Adapter<BasketRVA.BasketViewHolder>()
and in your viewHolder class and bind function
inner class BasketViewHolder(binding: BasketItemBinding): RecyclerView.ViewHolder(binding.root) {
#SuppressLint("SetTextI18n")
fun bind(basketEntity: BasketEntity?, clickListener: (BasketEntity, Boolean) -> Unit){
binding.tvProductName.text = basketEntity?.productName
in your binding function create a click listener for your view and pass the function you want to proceed
binding.imMinus.setOnClickListener {
val minus = true
clickListener(basketEntity!!, minus)
}
in your bind viewHolder
override fun onBindViewHolder(holder: BasketViewHolder, position: Int) {
BasketViewHolder(binding).bind(basketList[position], clickListener)
Log.d(TAG, "onBindViewHolder: basket list position is ${basketList[position]}")
}
Ask me if you still have any question
I have an app structured in MVVM. I have different fragments within the same activity. Each fragment has its own ViewModel and all data are retrieved from a REST API.
In FragmentA, there is a RecyclerView that lists X class instances. I want to set OnClickListener on the RecyclerView and I want to pass related X object to FragmentB when an item clicked in the RecyclerView. How can I achieve this?
How I imagine it is the following.
The Fragment passes a listener object to the adapter, which in turn passes it to the ViewHolders
Here is a quick sketch of how it should look like
class Fragment {
val listener = object: CustomAdapter.CustomViewHolderListener() {
override fun onCustomItemClicked(x: Object) {}
}
fun onViewCreated() {
val adapter = CustomAdapter(listener)
}
}
---------------
class CustomAdapter(private val listener: CustomViewHolderListener) {
val listOfXObject = emptyList() // this is where you save your x objects
interface CustomViewHolderListener{
fun onCustomItemClicked(x : Object)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.itemView.setOnClickListener {
listener.onCustomItemClicked(listOfXObject[position])
}
}
}
Here are some articles that might help you get the general gist of the things.
They don't answer your question directly though
Hope it is helpful
link 1 link 2
if you're using data binding you need to pass your view(which is Fragment in your case) into the layout via adapter class and you need to import your view in layout file to be able to call view's method
android:onClick="#{() -> view.onXXXClick(item)}"
pass your current model class which is item into this new method and then create onXXXClick method in your view and do whatever you wish.
if you will be doing view related operations such as navigation from one fragment to another or starting a service you should create above function in your view, if you're doing network or db related operations it should be in your ViewModel
you can check out my GitHub repository to understand better.
Is it possible to use ViewHolder element outside onBindViewHolder?
This is my adapter class
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mValues[position]
with(holder.mView) {
tag = item
txvTitle.text = item.title
setOnClickListener(mOnClickListener)
}
}
inner class ViewHolder(val mView: View) :RecyclerView.ViewHolder(mView{})
If I have one function in adapter, how can I use txvTitle?
fun checkHashMapExists(hashMap: HashMap<Long, ABC>?, newValues: ArrayList<OT>){
for(i in newValues){
if(hashMap?.keys.toString().contains(i.id)){
txvTitle.setTextColor(Color.parseColor("#000000")) // txvTitle cannot resolved
}
}
}
FragmentA
override fun onResume() {
super.onResume()
mAdapter.checkHashMapExists(hashMap,otList)
}
Even though I do not recommend using it, you can call the following method to get the ViewHolder for data element at position i:
MyViewHolder holder = (MyViewHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
if(holder != null) {
// Do whatever you want
}
Keep in mind what the documentation says about that method:
Return the ViewHolder for the item in the given position of the data set. Unlike findViewHolderForLayoutPosition(int) this method takes into account any pending adapter changes that may not be reflected to the layout yet. On the other hand, if notifyDataSetChanged() has been called but the new layout has not been calculated yet, this method will return null since the new positions of views are unknown until the layout is calculated.
Instead, what I would do if I were you, is to have an attribute in your model class that indicates which color its ViewHolder should use to display the txtView.title text.
Then, whenever you want to update the color of an element (or elements) in your RecyclerView, you can change that attribute of your model class and then call notifyDataSetChanged();
Normally you should not use ViewHolder outside onBindViewHolder. All UI-related code should be called inside onBindViewHolder. Prefer to call notifyDataSetChanged or other notifyXXX method to redraw your RecyclerView content.
The adapter has no way to know what txvTitle means, as it is specific to a single item in the RecyclerView.
Within a ViewHolder however, this property (if existent) is related to the view that is being "prepared".
Thus, updates to the view should only be done in the ViewHolder class.
Take a look at this example.
In this example, a separate class is created which extends RecyclerView.Viewholder(view).
Within this class, a method called bind is defined, which is passed the object (or model) that is used to populate the view.
The create method inflates the layout of the view to be drawn in the RecyclerView
You can create a function in your adapter class which takes Higher order function as parameter and then expose your ViewHolder object in that function to be used outside of Adapter class.
See how it can be done :
Let's say you've Adapter class as below
class Adapter {
...
// Here we create object of our Higher order function
var holderCallback: ((RecyclerView.ViewHolder?) -> Unit)? = null
//Then We provide callback like below in onBindViewHolder method
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
... Some binding stuff
holderCallback?.let {
it(holder)
}
... even some more stuff
}
}
And now we can access from outside (In your case, from Fragment class)
class Fragment {
// Here we have adapter object
...
//So, we get our callback like below from any method in here
...Inside some method where we receive callback
adapter.holderCallback { viewHolder: RecyclerView.ViewHolder? -> //Here you've got ViewHolder object
// Now do some amazing stuff here !!
}
}