How to give context to fragment in Kotlin? - android

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()

Related

How to automatically pass an argument to a function when it is called implicitly kotlin

maybe I phrased my question a little strange, but something became interesting to me.
Let's imagine that I have some Extension function:
fun Int.foo() {
TODO()
}
Suppose that I need to pass the context of the Fragment from which I call it to this function, in which case I would do it like this:
fun Int.foo(context: Context) {
TODO()
}
Here we are explicitly passing the Context of our Fragment to the function. However, I'm interested in the question - is it possible to somehow change this function so (or can it be called in some other way) so that I do not have to explicitly pass the Context?
I understand that I could do like this:
fun Fragment.foo() {
var context = this.context
}
...however, I need an Extension function just above Int, so this method is not suitable.
Are there any ways how this can be done?
I guess you're looking for context-dependent declarations that let you combine multiple receiver scopes:
context(Fragment)
fun Int.foo() {
check(context != null) // context is actually Fragments context
}
Keep in mind however this feature is still in experimental state so it requires opt in by adding -Xcontext-receivers to your compiler options.
The Int class is just a class for op with numbers
It doesn't make sense to contain a Context object
It is not possible to get context without passing it to the function
There are other ways, which is to create a static object for the application class
for example
class App : Application() {
companion object {
var app: App? = null
}
init {
app = this;
}
}
and then
fun Int.foo(){
val context=App.app
...
}

Android requireActivity() on Adapter

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
}

ViewTreeViewModelStoreOwner.get(view) always returns null

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);

Android Kotlin Coroutine on screen rotation

I am launching a coroutine that after a specified delay display a counter value on the screen.
job = launch(UI) {
var count= 0
while (true) {
textView.text = "${count++}"
delay(200L)
}
}
Now on screen rotation I want UI keeps getting updated with correct counter value. Does someone has any idea how to resume the job on configuration(e.g. screen rotation) change.
Does someone has any idea how to resume the job on configuration(e.g. screen rotation) change.
Your job never stopped running, but you keep holding on to and updating a TextView which is no longer showing on the screen. After the configuration changed, your activity and its entire view hierarchy got scraped.
While technically you can configure your app not to recreate the activity on rotation, Google strongly discourages you from doing that. The app will seem to work for the case of rotation, but then will break on another kind of config change like timezone, location etc. You just have to bite the bullet and make your app work across activity recreation events.
I made my coroutines work across activity recreation by relying an a Fragment in which I set
retainInstance = true
This means that your fragment instance survives the death of its parent activity and, when the new activity replaces it, Android injects your fragment into it instead of creating a new one. It does not prevent the destruction of the view hierarchy, you must write code that updates the fragment's state to reflect these changes. It helps because it allows you to keep the fragment's state instead of bothering with parcelization.
On configuration change, your fragment will go through these lifecycle events:
onDestroyView
onCreateView
It doesn't go through onPause/onResume, this only happens when you switch activities or exit the app. You can start your coroutine in onResume and cancel it in onPause.
As of the recently released version 0.23 of kotlinx.coroutines, launch became an extension function: you must call it in the context of some CoroutineScope which controls the resulting job's lifecycle. You should bind its lifecycle to the fragment, so let your fragment implement CoroutineScope. Another change is that the UI coroutine context is now deprecated in favor of Dispatchers.Main.
Here's a brief example that demonstrates all the points I mentioned:
class MyFragment : Fragment, CoroutineScope {
private var textView: TextView? = null
private var rootJob = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + rootJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val rootView = inflater.inflate(R.layout.frag_id, container, false)
this.textView = rootView.findViewById(R.id.textview)
return rootView
}
override fun onDestroyView() {
this.textView = null
}
override fun onResume() {
this.launch {
var count = 0
while (true) {
textView?.text = "$count"
count++
delay(200L)
}
}
}
override fun onPause() {
rootJob.cancel()
rootJob = Job()
}
}
Now, as the view hierarchy gets rebuilt, your coroutine will automatically fetch the current instance of textView. If a timer tick happens to occur at an inconvenient moment while the UI is being rebuilt, the coroutine will just silently skip updating the view and try again at the next tick.
By default rotation kills the activity and restarts it. That means your textview will no longer be the one on screen, it will be the one belonging to the old activity.
Your options are:
1)Add a configSettings to your manifest to turn off this behavior.
2)Use something that can persist around activity restarts like a View Model, a loader, an injected event bus, etc.
Personally unless you have a different layout for portrait and landscape I'd just go with number 1, its easier.
You can do it in ViewModel instead of Activity.

Fragment instance is retained but child fragment is not re-attached

Update: accepted answer points to explanation (bug) with a work-around, but also see my Kotlin based work-around attached as an answer below.
This code is in Kotlin, but I think it is a basic android fragment life-cycle issue.
I have a Fragment that holds a reference to an other "subfragment"
Here is basically what I am doing:
I have a main fragment that has retainInstance set to true
I have a field in the main fragment that will hold a reference to the subfragment, initially this field is null
In the main fragment's onCreateView, I check to see if the subfragment field is null, if so, I create an instance of the subFragment and assign it to the field
Finally I add the subfragment to a container in the layout of the main fragment.
If the field is not null, ie we are in onCreateView due to a configuration change, I don't re-create the subfragment, I just try to added it to the containter.
When the device is rotated, I do observe the onPaused() and onDestroyView() methods of the subfragment being called, but I don't see any lifecyle methods being called on the subfragment during the process of adding the retained reference to the subfragment, to the child_container when the main fragments view is re-created.
The net affect is that I don't see the subfragment view in the main fragment. If I comment out the if (subfragment == null) and just create a new subfragment everytime, i do see the subfragment in the view.
Update
The answer below does point out a bug, in which the childFragmentManager is not retained on configuration changes. This will ultimately break my intended usage, which was to preserve the backstack after rotation, however I think what I am seeing is something different.
I added code to the activities onWindowFocusChanged method and I see something like this when the app is first launched:
activity is in view
fm = FragmentManager{b13b9b18 in Tab1Fragment{b13b2b98}}
tab 1 fragments = [DefaultSubfragment{b13bb610 #0 id=0x7f0c0078}]
and then after rotation:
activity is in view
fm = FragmentManager{b13f9c30 in Tab1Fragment{b13b2b98}}
tab 1 fragments = null
here fm is the childFragmentManager, and as you can see, we still have the same instance of Tab1Fragment, but it has a new childFragmentManager, which I think is unwanted and due to the bug reported in the answer below.
The thing is that I did add the subfragment to this new childFragmentManger.
So it seems like the transaction never executes with the reference to the fragment that was retained, but does complete if I create a brand new fragment. (I did try calling executePendingTransactions on the new childFragmentManager)
class Tab1Fragment: Fragment() {
var subfragment: DefaultSubfragment? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLabel = "label 1"
subfragment!!.buttonText = "button 1"
}
addRootContentToContainer(R.id.child_container, content = subfragment!!)
return rootView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
Your problem looks similar to the issue described here:
https://code.google.com/p/android/issues/detail?id=74222
unfortunately this issue will probably not be fixed by google.
Using retained fragments for UI or nested fragments is not a good idea - they are recomended to be used in place of onRetainNonConfigurationInstance, so ie. for large collections/data structures. Also you could find Loaders better than retained fragments, they also are retained during config changes.
btw. I find retained fragments more of a hack - like using android:configChanges to "fix" problems caused by screen rotations. It all works until user presses home screen and android decides to kill your app process. Once user will like to go back to your app - your retained fragments will be destroyed - and you will still have to recreate it. So its always better to code everything like if your resources could be destroyed any time.
The accepted answer to my question above points out a reported bug in the support library v4 in which nested fragments (and child fragment managers) are no longer retained on configuration changes.
One of the posts provides a work-around (which seems to work well).
The work around involves creating a subclass of Fragment and uses reflection.
Since my original question used Kotlin code, I thought I would share my Kotlin version of the work around here in case anyone else hits this. In the end, I am not sure I will stick with this solution, since it is still somewhat of a hack, it still manipulates private fields, however if the field name is changed, the error will be found at compile time rather than runtime.
The way this works is this:
In your fragment that will contain child fragments you create a field retainedChildFragmentManager, that will hold the childFragmentManager that will be lost during the configuration change
In the onCreate callback for the same fragment, you set retainInstance to true
In the onAttach callback for the same fragment, you check to see if retainedChildFragmentManger is non-null, if so you call a Fragment extension function that re-attaches the retainedChildFragmentManager, otherwise you set the retainedChildFragmentManager to the current childFragmentManager.
Finally you need to fix the child fragments to point back to the newly created hosting activity (the bug leaves them referencing the old activity, which I think results in a memory leak).
Here is an example:
Kotlin Fragment extensions
// some convenience functions
inline fun Fragment.pushContentIntoContainer(containerId: Int, content: Fragment) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.addToBackStack("tag")
transaction.commit()
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
// here we address the bug
inline fun Fragment.reattachRetainedChildFragmentManager(childFragmentManager: FragmentManager) {
setChildFragmentManager(childFragmentManager)
updateChildFragmentsHost()
}
fun Fragment.setChildFragmentManager(childFragmentManager: FragmentManager) {
if (childFragmentManager is FragmentManagerImpl) {
mChildFragmentManager = childFragmentManager // mChildFragmentManager is private to Fragment, but the extension can touch it
}
}
fun Fragment.updateChildFragmentsHost() {
mChildFragmentManager.fragments.forEach { fragment -> // fragments is hidden in Fragment
fragment?.mHost = mHost // mHost is private also
}
}
The Fragment Hosting the child Fragments
class Tab1Fragment : Fragment() , TabRootFragment {
var subfragment: DefaultSubfragment? = null
var retainedChildFragmentManager: FragmentManager? = null
override val title = "Tab 1"
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLable = "label 1x"
subfragment!!.buttonText = "button 1"
addRootContentToContainer(R.id.child_container, content = subfragment!!)
}
return rootView
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (retainedChildFragmentManager != null) {
reattachRetainedChildFragmentManager(retainedChildFragmentManager!!)
} else {
retainedChildFragmentManager = childFragmentManager
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
}

Categories

Resources