Im creating a really basic recyclerview in fragment_home.xml which is linked to a <FrameLayout /> inserted in activity_main.xml
It works well...until I create the HomeFragment class in HomeFragment.kt, which inflates fragment_home.xml
Then I want to add up the arrayList (arrayHomeMenu) in the function onActivityCreated()
my problem is with this line
val homeMenuAdapter = HomeMenuAdapter(arrayHomeMenu, this) //context "this" appears with red underline
It retrieves error, so I cant continue...
HomeFragment.kt
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?{
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?){
super.onActivityCreated(savedInstanceState)
//create list of words
val arrayHomeMenu = ArrayList<HomeMenuModel>()
arrayHomeMenu.add(HomeMenuModel("Verbs","List of 461 words", R.drawable.ic_logo))
arrayHomeMenu.add(HomeMenuModel("Nouns","List of 52 words", R.drawable.ic_logo))
arrayHomeMenu.add(HomeMenuModel("Adjectives","List of 65 words", R.drawable.ic_logo))
arrayHomeMenu.add(HomeMenuModel("Adverbs","List of 345 words", R.drawable.ic_logo))
val homeMenuAdapter = HomeMenuAdapter(arrayHomeMenu, this) //context "this" appears with red underline
homeMenu_recyclerView.layoutManager = LinearLayoutManager(context)
homeMenu_recyclerView.adapter = homeMenuAdapter
}
This is a context problem....
I tried to replace this by activity, context, Context, applicationContext with not success at all...
HomeMenuAdapter.kt
class HomeMenuAdapter(val arrayList: ArrayList<HomeMenuModel>, val context: Context) :
RecyclerView.Adapter<HomeMenuAdapter.ViewHolder>() {
....//correct content
}
It works correctly when I create the recyclerview direct in activity_main.xml and work directly with MainActivity.kt
Once I move the recyclerview to a fragment...it throws the error described above.
HomeMenuModel.kt
class HomeMenuModel(val hm_title:String,val hm_description: String, val hm_image:Int)
I was checking this answer, but no success...
what am doing wrong?? thank you
The reason why you can use this as context inside an Activity, is because Activity extends Context.
Fragment however does not extend Context so you cannot use this.
The reason why activity, context also don't work is Kotlin's distinction of nullable types. While activity and context do return a Context, the return value is nullable. You can see this by paying close attention to the error message that appears when hovering over the red underline:
Type mismatch.
Required:
Context
Found:
Context? (or FragmentActivity?)
The question mark indicates that this is a nullable type, while a non-nullable Context is required. The reason why they're nullable is that the Fragment can only retrieve the Activity when it is attached to it, which is not always the case.
However, Fragment has a convenient method called requireContext() to work around this issue. It has a non-nullable return type but will instead throw an exception if it cannot retrieve the context, so it is on you to make sure to only call it when the Fragment is attached.
In short, you should be able to instantiate your adapter like this:
val homeMenuAdapter = HomeMenuAdapter(arrayHomeMenu, requireContext())
Since you are in a Fragment this can't be passed as a context:
val homeMenuAdapter = HomeMenuAdapter(arrayHomeMenu, this)
In the above code you are trying to pass fragment as a context, so instead what you can do is get the context of the fragment. So you can do:
val homeMenuAdapter = HomeMenuAdapter(arrayHomeMenu, requireContext())
Related
I'm attempting to configure a RecyclerView adapter and I have a line of code
val decorator = AppCompatResources.getDrawable(context, R.drawable.decorator)
This line of code will not work however as context is nullable and getDrawable requires context. I can think of 4 potential options for getting a non-nullable context in onViewCreated:
Wrap the code in context?.let {}
Wrap the code in activity?.applicationContext?.let {}
Use requireContext()
Get context from my application. My application class includes the following code:
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
lateinit var instance: MyApp
private set
val context: Context
get() = instance
}
My dev lead is not a fan of requireContext, and will request that I remove it if he sees it in a pull request. Is he wrong? Help me get Context smart, which of these options are the best and why?
When following tutorials, "this" is used for context however, "Context! was expected" is the error that pops up. I also tried getActivity(), which gave an unresolved reference. A third option I tried, was Activity which gave the error, " Classifier 'Activity' does not have a companion object, and thus must be initialized here"
current code
The goal is to get TextToSpeech to work within the recycler adapter. The main issue that I'm having is what to pass in for the context parameter within the TextToSpeech class.
" tts = TextToSpeech(this, this )" is what is usually given in tutorials, this does not work within a fragment/recyclerView.
Assuming you're inside onBindViewHolder(), you can leverage the fact that all instances of View have a context property:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val context = holder.itemView.context
// now you can use context anywhere below
}
Alternatively, you can pass context from MainActivity or Fragment in RecyclerViews onCreateViewHolder
//declare context var
private lateinit var context:Context
#NonNull
override fun onCreateViewHolder(#NonNull parent: ViewGroup, viewType: Int): MyViewHolder {
...
context = parent.context
...,
}
It's hard to understand what the problem is from the headline - I'll try my best explaining:
I'm using Koin for dependency injection. I'm injecting my HomeViewModelinto my HomeFragment (the viewModel has parameters, but that should be unrelated to the problem):
// fragment code
private var viewModelParameters: ParametersDefinition? = null
lateinit var viewModel: VM
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
binding?.lifecycleOwner = viewLifecycleOwner
viewModel = getViewModel(HomeViewModel::class, parameters = viewModelParameters)
return binding?.root ?: inflater.inflate(layout, container, false)
}
The fragment contains a RecyclerView. The recycler's ViewHolder declares an interface, that is injected via Koins by inject()`:
class MyRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), KoinComponent {
private val callback by inject<Callback>()
fun bind(item: MyItemType) {
itemView.setOnClickListener { callback.myCallbackFunction(item) }
}
interface Callback {
fun myCallbackFunction(item: MyItemType)
}
}
My HomeviewModel implements this interface, and I bind it to the viewModel in my KoinGraph module via Koin's bind DSL method:
private val baseModule = module {
single { androidApplication().resources }
single { PermissionHelper(androidApplication()) }
...
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
}
Now, when I click on my recycler item, the callback's myCallBackFunction is called, which should trigger the implementation in my HomeViewModel. Which it does, but: It is not the same instance, but a new HomeViewmodel.
My understanding is that Android's ViewModelclass, if used in the typical way (currently using, without Koin, by viewModels() - see here), should only exist once. But with Koin's viewModel{} call, I can create multiple instances, which I think I shouldn't be able to? Or should I (and if yes, why)?
Anyway, I'd like to bind my callback to the view model I already have (the one the fragment knows of) and not a new instance my fragment doesn't know about.
How can I achieve that using Koin and its injection patterns?
By the way, If I use
single { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
instead of
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
my code works as intended - since I'm forcing my view model to be a singleton that way - which is what I want. But what is the point of the viewModel{} command then? And are there any downsides to it? It doesn't feel like what I should be supposed to do but maybe it's totally fine?
I've been doing some with a fragment that has a view model with a dependency on the WorkManager. I used to get the WorkManager using the now deprecated method WorkManager.getInstance(), so I refactored the code and followed the same method of getting the WorkManager instance as that done in the Sunflower project (which has since changed). The Sunflower sample project now uses NavArgs() and no longer does this: InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
My question is, can IllegalStateException be thrown when assigning the viewModel variable by injection because of getting a WorkManager instance using requireActivity() like in my code below? Is it possible for an activity to not be attached/get destroyed at the time this variable is assigned? Should I refactor and use the application context instead of requireActivity()?
class DetailFragment : Fragment() {
private val viewModel by inject<ViewModel> { parametersOf(WorkManager.getInstance(requireActivity())) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = DetailFragmentBinding.inflate(inflater, container, false).apply {
vm = viewModel.apply {
event.observe(this#DetailFragment, Observer {
viewDataBinding.refreshLayout.isRefreshing = false
})
}
}
return viewDataBinding.root
}
}
You should be mostly safe. But it only depends on where you get viewModel for the first time. You could make sure viewModel gets initialized in onCreate or onStart before setting up any handler accessing it.
In your case it's called in onCreateView only, thus it should be attached to an Activity and for that reason safe.
I'm trying to pass a bundle of object instances down from my main activity to the first fragment in a chain of other fragments using the NavHostFragment. I've tried all sorts but the bundle always seems to be null once it reaches the first fragment.
Here's how I'm initiating the NavHostFragment (frameContainer is a Frame Container element in my layout xml)
NavHostFragment navHost = NavHostFragment.create(R.navigation.claim_nav_graph);
getSupportFragmentManager().beginTransaction()
.replace(R.id.frameContainer, navHost)
.setPrimaryNavigationFragment(navHost)
.commit();
The documentation says there are 2 different .create functions, one of them you can pass a second arguments to as a bundle, but Android Studio doesn't allow me to use this version.
Does anyone have any ideas?
Thanks in advance!
It does seem to be a flaw with the NavHostFragment, passing data down to the first fragment does not seem to be possible, as the Bundle you can set as a second argument on the create function is overwritten along the way.
In the end I resolved this by building the bundle in the first fragment of the activity instead. I was able to access the activities intent properties using the below.
// Kotlin
activity.intent?.extras?.getBundle(KEY_BUNDLE_ID)
// Java
getActivity().getIntent().getBundleExtra(KEY_BUNDLE_ID)
This was enough of a workaround for me in this situation, but it would be great if it was possible
If you're using viewModels, you can do this:
your viewmodel:
class NiceViewModel: ViewModel() {
var dataYouNeedToPass = "initialValue"
}
your activity:
class MainActivity : AppCompatActivity() {
val niceViewModel: NiceViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
niceViewModel.dataYouNeedToPass = "data You Need To Pass"
}
}
your fragment:
class YourFragment : Fragment() {
private lateinit var niceViewModel: NiceViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
niceViewModel = (activity as MainActivity).niceViewModel
niceViewModel.dataYouNeedToPass //do whatever you need to do with this
}
}