I have an activity which has a ViewModel and the activity observes a liveData of ViewModel
when I first start the activity, it works but when I return to it again, I need to be just like new but it since it's still observing the liveDatas , it shows me old data. what should I do? (oh and keep in mind that I can't create a new activity )
This is my Activity
class LoginActivity : AppCompatActivity() {
lateinit var idEditText: EditText
lateinit var createAccountTextView: TextView
lateinit var loginButton: Button
private lateinit var userRepository: UserRepository
private lateinit var loginActivityViewModel: LoginActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
userRepository = UserRepository(application)
loginActivityViewModel = ViewModelProviders.of(
this,
LoginActivityViewModelFactory(userRepository)
).get(LoginActivityViewModel::class.java)
idEditText = findViewById(R.id.idEditText)
loginButton = findViewById(R.id.enterButton)
createAccountTextView = findViewById(R.id.createAccountTextView)
loginButton.setOnClickListener {
val loginID = idEditText.text.toString()
loginActivityViewModel.userEntry(loginID)
}
createAccountTextView.setOnClickListener {
val createAccountIntent = Intent(this, CreateAccountActivity::class.java)
startActivity(createAccountIntent)
finish()
}
}
override fun onResume() {
super.onResume()
loginActivityViewModel.idEditTextMutableLiveData.observe(this, Observer {
idEditText.error = it
})
loginActivityViewModel.onSuccessMutableLiveData.observe(this, Observer {
val mainIntent = Intent(this, MainActivity::class.java)
mainIntent.putExtra(loginValue, UserUI(it.username, it.userId.toInt()))
startActivity(mainIntent)
})
}
}
The point of using Livedata is to get the latest update to a data source. If your data source is persistent it means the data returned will always be the same.
What you need to do is clear the datasource on application/activity start. Then you can populate the data source as you go.
Livedata will always return data as long as the data source has data.
A way around it is to avoid returning a livedata object altogether and just return the object itself.
Related
when i try to initialize data class it gives me error, kotlin.UninitializedPropertyAccessException: lateinit property article_ has not been initialized. How do i resolve this error and whats way to initialize data class.
model class
data class Article(
val id: Int,
val author: String,
val content: String,
val description: String?)
MainActivity
class TestActivity : AppCompatActivity() {
lateinit var binding: ActivityTestBinding
lateinit var article_: List<Article>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_test)
val mainAdapter = MainAdapter(article_)
binding.recView.apply {
this.layoutManager = LinearLayoutManager(this#TestActivity)
this.adapter = mainAdapter
}
}
}
Change code like this
binding = DataBindingUtil.setContentView(this,R.layout.activity_test) article_ = ArrayList()
ANSWER:
In onCreate() before this line val mainAdapter = MainAdapter(article_) just initialize the article_ by writing article_ = ArrayList() and the error will be gone.
Cause of Error
You are encountering this error because You need to initialize the lateinit variable article_ before using it and you are not doing it.
If you want to check that either the variable is initialized or not then use ::article_.isInitialized.
Feel free to ask if something is unclear.
lateinit should only be used when you have a specific reason not to initialize the variable on construction. Here I can see no reason not to make it
var article_: List<Article> = listOf()
or perhaps
val article_: MutableList<Article> = mutableListOf()
and guarantee such error can't happen in the first place.
I need to pass this:
private lateinit var memes: MutableList<Memes>
which has this model:
class Memes (
#SerializedName("id") var id: Long,
#SerializedName("title") var title: String
)
from activity a to b.
I've altready seen couple "solutions" and none of them works!
This is the last that I've tried:
val extras = Bundle()
val memesArrayList = ArrayList(memes)
val i = Intent(context, GalleryShow::class.java)
i.putExtras(extras)
i.putStringArrayListExtra("list", memesArrayList)
(context as Activity).startActivityForResult(i, 777)
However, I get Type mismatch: inferred type is ArrayList<Memes!> but ArrayList<String!>? was expected on memesArrayList.
EDIT:
This is my latest attempt now:
In activity A inside recyclerview item:
val extras = Bundle()
extras.putString("gal", "animals")
extras.putString("query", "")
val i = Intent(context, GalleryShow::class.java)
i.putExtra("list", memes.toTypedArray())
i.putExtras(extras)
(context as Activity).startActivityForResult(i, 777)
and this is inside activity B:
private lateinit var memes: MutableList<Memes>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery_show)
memes = (this?.intent?.extras?.get("list") as? Array<Memes>)?.asList()!!.toMutableList()
}
You can use simply intent.putExtra instead of worrying about which variant like put_____Extra to use.
When extracting the value, you can use intent.extras to get the Bundle and then you can use get() on the Bundle and cast to the appropriate type. This is easier than trying to figure out which intent.get____Extra function to use to extract it, since you will have to cast it anyway.
The below code works whether your data class is Serializeable or Parcelable. You don't need to use arrays, because ArrayLists themselves are Serializeable, but you do need to convert from MutableList to ArrayList.
// Packing and sending the data:
val i = Intent(context, GalleryShow::class.java)
i.putExtra("list", ArrayList(memes)) // where memes is your MutableList<Meme> property
startActivityForResult(i, 777)
// Unpacking the data in the other activity:
memes = intent.extras?.get("list") as MutableList<Meme>
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 :)
This question already has answers here:
What is a NullPointerException, and how do I fix it?
(12 answers)
Closed 2 years ago.
When I'm trying start new activity with this code im getting tihs error:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getPackageName()' on a null object reference
class ApiConnection {
var baseApiURl = "http://localhost:8081"
var data = arrayListOf<User>()
fun connectApi(): ApiService {
val retrofit = Retrofit.Builder()
.baseUrl(baseApiURl)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
fun loginRequest(
login: String,
password: String
){
val service = connectApi()
val call = service.login(login, password)
call.enqueue(object: Callback<UserResponse> {
override fun onFailure(call: Call<UserResponse>?, t: Throwable?) {
Log.v("retrofit", "call failed")
}
override fun onResponse(call: Call<UserResponse>?, response: Response<UserResponse>?) {
addDataToList(response)
}
})
}
fun addDataToList(response: Response<UserResponse>?) {
var mainActivity = MainActivity()
data.add(
User(
response!!.body()!!.idUser,
response!!.body()!!.login,
response!!.body()!!.password,
response!!.body()!!.name,
response!!.body()!!.surname,
response!!.body()!!.lastLogin
)
)
mainActivity.loginIntoServer(data)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val api = ApiConnection()
button_checkIfThisAccountExit_mainActivity.setOnClickListener {
api.loginRequest(editText_login_activityMain.text.toString(), editText_password_mainActivity.text.toString())
}
}
fun loginIntoServer(data: ArrayList<User>) {
val context = this
val intent = Intent(context, Main2Activity::class.java)
startActivity(intent)
}
}
Any ideas what can make this error? I'm using retrofit to connect with my API and after parsing the result data and adding it to the list i would like to run new activity (something like a login system. After putting username/password I want change the activity).
Source of your problem is var mainActivity = MainActivity(), You never create instance of activity by invoking the constructor because that is the job of android system, see this
Correct solution to your problem would be LiveData.
but if you want to make your current code work as it is, you will have to pass a context object in your ApiConnection class,
fun addDataToList(context: Context, response: Response<UserResponse>?) {
data.add(
User(
response!!.body()!!.idUser,
response!!.body()!!.login,
response!!.body()!!.password,
response!!.body()!!.name,
response!!.body()!!.surname,
response!!.body()!!.lastLogin
)
)
val intent = Intent(context, Main2Activity::class.java)
context.startActivity(intent)
}
Also update the signature of loginRequest to accept a Context object from your activity, so that it look as following
fun loginRequest(context: Context, login: String, password: String)
And now call it from your activity as
api.loginRequest((this as Context), editText_login_activityMain.text.toString(), editText_password_mainActivity.text.toString())
How in your opinion this code should look?
Only corrently working code isnt enought for me if you know what I mean.
I also want to have the code which is looking good.
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"
...>