I have an single activity application with jetpack navigation, I need an object variable for all my application in many fragments. So I use a ViewModel, and I've created a Parent Fragment class which provide the ViewModel :
class MyViewModel : ViewModel() {
var myData : CustomClass? = null
...
}
open class ParentFragment : Fragment {
val model : MyViewModel by activityViewModels()
lateinit var myData : CustomClass
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.myData?.let {
myData = it
}
}
}
myDatashould not be null where I use ParentFragment, but sometimes, randomly I get kotlin.UninitializedPropertyAccessException: lateinit property myData has not been initialized when I use myData
Is it possible that my ViewModel doesn't keep myData? How can I be sure that my property has been initialized ?
UPDATE : Try 1
I've tried this code in my ParentFragment:
open class ParentFragment : Fragment {
val model : MyViewModel by activityViewModels()
lateinit var backingData : CustomClass
val myData : CustomClass
get() {
if (!::backingData.isInitialized)
model.getData()?.let {
backingData = it
}
return backingData
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.getData?.let {
backingData = it
}
}
}
But the problem doesn't disappear when I call myData, it seem's the ViewModelloses my data
UPDATE 2 : More code details
Before to go inside a fragment which extends ParentFragment, I set my data in ViewModel and then I navigate to the next fragment as below :
// Inside FirstFragment
if (myData != null) {
model.setData(myData)
findNavController().navigate(FirstFragmentDirections.actionFirstToNextFragment())
}
Is it possible that my NavController does navigation before the data was setted ?
EDIT 3 : Try to use custom Application class
According to an answer below, I've implemented a custom Application class, and I've tried to pass my object through this class :
class MyApplication: Application() {
companion object {
var myObject: CustomClass? = null
}
}
But unfortunately, there is no change for me. Maybe my object is too big to allocate correctly ?
Try this:
class MyViewModel : ViewModel() {
var myObject : CustomClass? = null
...
}
open class ParentFragment : Fragment {
lateinit var model : MyViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model = ViewModelProvider(this).get(MyViewModel::class.java)
if(model.myObject == null) {
// initialize myObject, will be persisted by ViewModel
}
}
}
Note that MyViewModel and its member objects should not hold any references to Activity, Fragment, or Context, that include any indirect references to Context such as UI Views.
I will not recommend LiveData (or MutableLiveData) in your case, because a "feature" of LiveData is that their values are posted and updated asynchronously, hence call to observe may be too late.
Your wording hints some design flaws, namely:
You are refering to your data as object variable and to make it accessible at all times you chose to use a ViewModel. To me it sounds that you overthought your options.
Suggestion
Your object lifecycle appears to be managed manually by yourself. Therefore you should just use a static variable. This translates to Kotlin as a property within an (companion) object. I suggest you declare a custom Application class within your manifest and in its onCreate-method, you allocate your object and put it into the companion object of this class. Of course you can allocate it at any given time later on as well.
This will result in the following:
Access is always be possible via YourApplication.mData within your code.
Objects which relying on implementations outside the JVM can be managed properly.
For example: If you already bound to a port you won't be able to do this on a successive call - When the viewModel restores its state, for example. Maybe the underlying implementation did not report an error back to Java but allocating did not succeed. To manifest this assumption you would need to provide an description of your object variable. But As an famous example in the world of Android for this behaviour, try creating a soundPool via the SystemServices. You will experience lints about the correct usage of this object.
Deallocating can be done in the onTerminate() method of your
Application.class // edit_4: Doc of super.onTerminate() says the system will just kill your app. Therefore one needs to deallocate within an your activity. See code snippets below.
Clarification
The ViewModel of the JetPack Components is mainly responsible for saving and restoring the state of the view and binding to its model.
Meaning it handles the lifecycle across activities, fragments and possibly views. This is why you have to use an activity as the lifecycle owner in case you want to share an viewModel across multiple fragments. But I still suppose your object is more complex than just a POJO and my above suggestion results in your expected behaviour.
Also note that when multithreading, you shall not rely on the correct order of the lifecycle methods. There are only limited lifecycle-callbacks which are guaranteed to be called in a specific order by the android system, but the frequently used ones are unfortunately not included here. In this case, you should start processing at a more approrpiate time.
Even though the data should be similiar to the previous state, the exact reference depends on the hashCode implementation, but this is an JVM specific.
// edit:
ParentFragment is also bad naming, since you created a class which others shall inherit instead of refer to. If you want to access a specific variable within all your fragments, this needs to be implemented as an object (Singleton), since the Navigation component will prevent you from accessing the fragmentManager directly.
In plain android, one fragment can always refer to its parentFragment, iff this parentFragment has used its own childFragmentManager to commit the fragmentTransaction. Meaning also that fragments added by your Activity-fragmentManager have never an parentFragment.
// edit_2+3:
ViewModelProvider(activity!!, ViewModelFactory())[clazz]
is the correct call for creating and accessing a sharedViewModel:
The lifecycle owner needs to be the activity, otherwise after each fragmentTransaction done there will be a callback to the onCleared() method and the viewModel will release all references to avoid memory leaks.
// edit_4:
That your object was not correctly initialized was just an assumption which only would oocure if you tried to initialize it again. For example if you use an get()-method on an val where not appropriate.
Nonetheless, handling your object this way ensures that its lifecycle is outside your fragments. Here is an code example to clarify my wording:
// edit_5: To assert that the object reference is not damaged, include null checking (only if construction of CustomClass is non trivial)
Declare your CustomApplication
class CustomApplication : Application() {
companion object SharedInstances {
/**
* Reference to an object accessed in various places in your application.
*
* This property is initialized at a later point in time. In your case, once
* the user completed a required workflow in some fragment.
*
* #Transient shall indicate that the state could also be not Serializable/Parcelable
* This _could_ require manually releasing the object.
* Also prohibits passing via safeArgs
*/
#Transient var complex: CustomClass? = null
}
}
Intialization and Usage within your classes:
class InitializeComplexStateFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (complex != null) return#onViewCreated // prohibit successive initialization.
if (savedInstanceState != null) { /* The fragment was recreated but the object appears to be lost. */ }
// do your heavy lifting and initialize your data at any point.
CustomApplication.SharedInstances.complex = object : CustomClass() {
val data = "forExampleAnSessionToken"
/* other objects could need manual release / deallocation, like closing a fileDescriptor */
val cObject = File("someFileDescriptorToBindTo")
}
}
}
class SomeOtherFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
CustomApplication.SharedInstances.complex?.let {
// do processing
}
?: propagateErrorStateInFragment()
}
private fun propagateErrorStateInFragment() { throw NotImplementedError("stub") }
}
Deallocation if required
class SomeActivity: Activity() {
override fun onStop() {
super.onStop()
/* with multiple activities the effort increases */
CustomApplication.complex?.close()
}
}
You can check by using isInitialized on your property.
As the documentation says:
Returns true if this lateinit property has been assigned a value, and false otherwise.
You could initialize your property as null and do a null-check with the let as you already do though, no need to use lateinit and be careful with it, it is not a substitute for using a nullable var
You can use like this:
class MyViewModel : ViewModel() {
var mData: MutableLiveData<CustomClass>? = null
init {
mData = MutableLiveData<CustomClass>()
mData!!.value = CustomClass()
}
fun getData(): LiveData<CustomClass>? {
return mData
}
}
And your fragment :
open class ParentFragment : Fragment {
lateinit var model : MyViewModel
lateinit var myObject : CustomClass
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model = ViewModelProvider(this).get(MyViewModel::class.java)
model.getData()?.observe(viewLifecycleOwner, Observer {
myObject = it
})
}
}
Ideally you should tie the sharedVM lifecycle to activity and then use the same sharedVM instance in all fragments. Also initialise the myObject in parentFragment/ activity class using setter(). Then get the object using getter().
sample code:
// SharedViewModel
var myObject : CustomClass? = null
fun setMyObject(obj : CustomClass?){
myObject = obj
}
fun getMyObject():CustomClass?{
return myObject
}
// Activity
val model: SharedViewModel by viewModels()
model.setMyObject(objectValue)
// ParentFragment
private val model: SharedViewModel by activityViewModels()
val obj = model.getMyObject()
Hope this helps you.Happy Coding :)
Related
I have a ViewModel class:
#HiltViewModel
open class AppViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle
): ViewModel(){
val isLoading: MutableState<Boolean> = mutableStateOf(false)
fun setIsLoading(isLoading: Boolean){
this.isLoading.value = isLoading
}
}
This class should hold the general App state.
Then I have another ViewModel class, inside which I want to be able to modify the AppViewModel state. E.g. when fetching data I want to set isLoading to true and render the progress bar.
The ChildViewModel class that should modify AppViewModel state:
class ChildViewModel #Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
): AppViewModel(savedStateHandle){
...
fun onTriggerEvent(event: RestApiEvents) {
try {
viewModelScope.launch {
// this should change the state in AppViewModel
isLoading.value = true
when(event) {
is SearchEvent -> {
search(event.s)
}
else -> {
Log.d(TAG, "Event not found")
}
}
// this should change the state in AppViewModel
isLoading.value = false
}
}
catch (e: Exception){
e.printStackTrace()
}
}
private suspend fun search(s: String) {
...
}
}
So, I'm new to compose and I was trying to make the exact same thing.
The compiler didn't let me inject the AppViewModel as a dependency to the ChildViewModel, so I ended up doing the following:
Created a data class (AppStateHolder) to store loading bool value.
At AppViewModel I inject AppStateHolder as dependency.
Added to appmodule the hilt reference generator with #Singleton and #Provides annotation.
I inject at ChildViewModel reference to AppStateHolder.
AppStateHolder has a mutable state for the isLoading boolean.
At AppViewModel init block I collect the appStateHolder values and update the mutable state accordingly.
When necessary at ChildViewModel I update AppStateHolder values.
So you can inject AppStateHolder, update its values, and when you make this, AppViewModel will be notified and updates its own state.
The setback is, if you havea lot of ui state variables at AppViewModel you have to replicate them at AppStateHolder and always observe them at AppViewModel.
I know it's not the best option, but I will be looking for other ways or till someone comes with a better solution.
imageSorry, I'm new to Java/Kotlin mobile apps...
Below code snippet from RegisterFragment.kt which is the main class:-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val username = binding.text
PackageSdk.getInstance().hasDuplicateUserKey(**username**, object : PackageResponseCallback<ResultResponse> {
override fun onSuccess(result: ResultResponse) {
Log.i(TAG, "Result code : " + result.rtCode)
}
override fun onFailed(errorResult: ErrorResult) {
Log.e(TAG, "Error code : " + errorResult.errorCode)
}
})
whereas below is the data class named RegisteredUserView.kt
data class RegisteredUserView(
val username: String
//... other data fields that may be accessible to the UI
)
I usually used toString() to the param value of "username" but I will get this bug
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference.
but if I just leave only the username, I will have "Type mismatch. Required:String Found: EditText" type of error. Do I have to create a function to pass value from data class to RegisterFragment class in order to have proper param input. If yes then how? If no then what way to assign param input? Btw, the
val username = binding.text
is fetch from layout_fragmentregister (an EditText) which its id = text
The error you are getting is because
val username: String
is null
your data class doesn't do much besides pass data along. You should use an instance of it and a view model to pass data to another fragment. A typical domain data class looks like this
data class User(
val userID: String,
val userName: String,
val emailAddress: String,
val photoUrl: String,
val firstName: String,
val lastName: String,
val age: Int,
val score: Int,
val sex: Int,
val emailVerified:Boolean)
to create an instance of the data class create a private var to store that data.
private var currentUser = User(someFunctionThatReturnsUserData)// the data must match the model
for test purposes and better understanding you can create a function with a test user
fun generateTestUser():User{return User("TEST ID","TEST NAME","TEST EMAIL")}
you can decide how to display the data in currentUser by accessing any of the properties. i prefer to do this in the view model as it removes a lot of logic from the UI thread and doesn't get reset on config changes.
currentUser.userId
currentUser.userName
currentUser.emailAddress
and so on. Usually best practice to create a private and public variables. Private to manipulate data, public to allow views to see the data but not modify it.
in your view model
private val _uid: MutableLiveData<String> by lazy { MutableLiveData<String>() }
val uid:LiveData<String> //this returns a string of live data for the ui to observe changes on.
get() = _uid //get the value from the private val
fun getUserId(){_uid.value = "some string"}
these are just examples and probably wont work with a simple copy/paste but hopefully helps a little.
Desired functionality:
I have an Activity that has a value received from backend which indicates to use either one of two layouts. Let's call this value layoutType and let's assume for simplicity in this example code below that we don't care how it will be assigned. Thus, I have two layout xml files, let's call them layout1.xml & layout2.xml.
Implementation:
I'd like to use View Binding. I've created a variable of type ViewBinding and I tried assigning to it either a Layout1Binding or a Layout2Binding. A summary of this logic in pseudocode is this:
private ViewBinding binding;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(layoutType == 1){
binding = Layout1Binding.inflate(getLayoutInflater());
} else {
binding = Layout2Binding.inflate(getLayoutInflater());
}
setContentView(binding.getRoot());
}
Result: Of course, this doesn't work and variable binding appears to have no inner children that can be referenced. Also, of course if I convert the variable's type to Layout1Binding and layoutType equals 1, then I can use it correctly. Same goes if I use Layout2Binding and layoutType is not equal to 1. All these make sense, since ViewBinding is just an Interface implemented by the generated classes Layout1Binding & Layout2Binding.
Question: How can I achieve the above desired behaviour by using only one binding variable which can be assigned to two different generated classes? Is there any other alternative way?
Here's an example of how I used an adapter for a custom view that uses three layouts. In my case, most views in the different layout files had common IDs.
Define your binding adapter class
class MyCustomViewBindingAdapter(
b1: Layout1Binding?,
b2: Layout2Binding?,
b3: Layout3Binding?
) {
val text =
b1?.myTextView
?: b2?.myTextView
?: b3?.myTextView
val badgeText = b3?.myBadgeTextView
}
Create a binding variable
private lateinit var binding: MyCustomViewBindingAdapter
Create a method to get the binding adapter
private fun getBinding(layoutType: Int): MyCustomViewBindingAdapter {
val inflater = LayoutInflater.from(context)
return when(layoutType) {
TEXT_ONLY -> {
val textBinding = Layout1Binding.inflate(inflater, this, true)
MyCustomViewBindingAdapter(textBinding, null, null)
}
IMAGE_AND_TEXT -> {
val imageTextBinding = Layout2Binding.inflate(inflater, this, true)
MyCustomViewBindingAdapter(null, imageTextBinding, null)
}
TEXT_AND_BADGE -> {
val textBadgeBinding = Layout3Binding.inflate(inflater, this, true)
MyCustomViewBindingAdapter(null, null, textBadgeBinding)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
Be sure to initialize the binding before using it
binding = getBinding(layoutType)
My project is currently using the following SingleLiveEvent implementation.
class SingleLiveEvent<T>(
private val allowMultipleObservers: Boolean = false
) : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
private val observers = mutableSetOf<Observer<in T>>()
#MainThread
override fun observe(
owner: LifecycleOwner,
observer: Observer<in T>
) {
if (!allowMultipleObservers && hasActiveObservers()) {
Timber.tag(TAG)
.w("Multiple observers registered but only one will be notified of changes.")
} else {
observers.add(observer)
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observers.forEach { observer ->
observer.onChanged(t)
}
}
})
}
...
When we do this inside of onResume or onCreateView
SingleLiveEvent.observe(viewLifecycleOwner) {
requireContext()
}
We sometimes receive a crash saying that the Context is null. However, shouldn't requireContext() always give us a valid Context value since it's attached to the viewLifecycleOwner?
Your implementation is flawed - it's possible to register multiple observers as long as they attach in inactive state (or existing observers became inactive), then observer wrapper you passed to super.observe will fail because you're forcing a call to onChanged for all observers disregarding their actual state.
You should replace hasActiveObservers() with hasObservers(). But personally I'd look into solution implementing MediatorLiveData since this implementation doesn't properly handle observers that enter destroyed state, leaving them in your observers list forever.
requireContext() does nothing special except for null-check comparing to getContext().
Answer from Google:
How should we fix that?
Firstly we should fix potentials bugs.
Find all places where fragment can be detached at some point of time
Change code like in the sample below
In Java:
Context context = getContext();
if (context == null) {
// Handle null context scenario
} else {
// Old code with context
}
Source: https://medium.com/#shafran/fragment-getcontext-vs-requirecontext-ffc9157d6bbe
I'm an beginner in kotlin and im trying to pass a context as a parameter, but isnt working...
these are my codes:
FUNCTION saveDatabase
private fun saveDatabase(context : Context){
val fightersName = Match(1, fighter1.toString(), fighter2.toString(),
minute.toInt(), round.toInt())
val db = DBContract(context)
db.insertData(fightersName)
}
CALLING THE FUNCTION
saveDatabase(context)
WARNING
Typemismatch
Required: Context
Found: Context?
This class is a fragment that extends of a Fragment()
your function requires a non null Context object, whereas you are calling it with a nullable and mutable Context object. If you are sure your context is not null, call
saveDatabase(context!!)
!! means that you vouch for the object to be non null
Or you can check your function for safety, then change your function to
private fun saveDatabase(context : Context?){
if(context != null){
val fightersName = Match(1, fighter1.toString(), fighter2.toString(),
minute.toInt(), round.toInt())
val db = DBContract(context)
db.insertData(fightersName)
}
}
The getContext method that you're accessing as the context property in Kotlin has a nullable type Context? - since it will return null when your Fragment isn't attached to an Activity.
One way to deal with the error is to first fetch its value, and perform a null check before you call your function:
val context = context
if (context != null) {
saveDatabase(context)
}
The same check using let, in two different forms:
context?.let { ctx -> saveDatabase(ctx) }
context?.let { saveDatabase(it) }
You can also use requireContext if you are absolutely sure that your Fragment is attached to an Activity - this returns a non-nullable Context, or throws an exception if there isn't one available.
saveDatabase(requireContext())
it so easy. Try as follow
private fun saveDatabase(context : Context?){
val fightersName = Match(1, fighter1.toString(), fighter2.toString(),
minute.toInt(), round.toInt())
val db = DBContract(context)
db.insertData(fightersName)
}
If you are new in Android with kotlin you will surely need an "always available" context. This is the way:
class App : Application() {
companion object {
lateinit var instance: App
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
then you just need to pass:
val db = DBContract(App.instance)
Be sure of modifying the manifest:
<application
android:name=".App"
...>