So I'm rewriting some legacy code of an android app.
Part of that change includes introducing view models. And part of that includes changing a UserManager
class that used to be an object
to an AndroidViewModel
.
class UserManager(application: Application) : AndroidViewModel(application) {
private val userData: MutableMap<User, MutableMap<String, Any>> = object : HashMap<User, MutableMap<String, Any>>() {
override fun get(key: User): MutableMap<String, Any>? {
val former = super.get(key)
val current = former ?: mutableMapOf()
if (current !== former) this.put(key, current)
return current
}
}
init {
restoreActiveUsers()
}
override fun onCleared() {
persistActiveUsersData()
}
private fun restoreActiveUsers() {
val decodedUsers: List<User> = ... load users from persistent storage ...
decodedUsers.forEach { userData[it] } //create an entry in [userData] with the user as key, if none exists
...
}
}
The init
block is new because it used to be called on the Object instance from outside, prior to my transformation, and it's the source of my confusion.
Because trying to run the application like this gives me an exception at decodedUsers.forEach { userData[it] }
because
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.bla.bla.bla.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class com.bla.bla.bla..user.service.UserManager
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
...
Caused by: java.lang.RuntimeException: Cannot create an instance of class com.,bla.blab.bla.user.service.UserManager
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:275)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)
...
at com.,bla.blab.bla.app.ui.MainActivity.getUserManager(Unknown Source:7)
at com.,bla.blab.bla.app.ui.MainActivity.onCreate(MainActivity.kt:71)
at android.app.Activity.performCreate(Activity.java:7802)
...
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:267)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)
...
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.Map.get(java.lang.Object)' on a null object reference
at com.bla.bla.bla.user.service.UserManager.restoreActiveUsers(UserManager.kt:178)
at com.bla.bla.bla.user.service.UserManager.<init>(UserManager.kt:60)
I checked with the debugger and userData
really IS null
.
But that doesn't make sense.
Because I had no other ideas and in spite of AndroidStudio's protests, I switched to a secondary constructor.
constructor(application: Application) : super(application) {
restoreActiveUsers()
}
And that did the trick.
I'm struggling to understand why, though.
According to jvm specs:
Whenever a new class instance is created, memory space is allocated for it with room for all the instance variables declared in the class type and all the instance variables declared in each superclass of the class type, including all the instance variables that may be hidden (§8.3).
If there is not sufficient space available to allocate memory for the object, then creation of the class instance completes abruptly with an OutOfMemoryError. Otherwise, all the instance variables in the new object, including those declared in superclasses, are initialized to their default values (§4.12.5).
Just before a reference to the newly created object is returned as the result, the indicated constructor is processed to initialize the new object using the following procedure:
Assign the arguments for the constructor to newly created parameter variables for this constructor invocation.
If this constructor begins with an explicit constructor invocation (§8.8.7.1) of another constructor in the same class (using this), then evaluate the arguments and process that constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason; otherwise, continue with step 5.
This constructor does not begin with an explicit constructor invocation of another constructor in the same class (using this). If this constructor is for a class other than Object, then this constructor will begin with an explicit or implicit invocation of a superclass constructor (using super). Evaluate the arguments and process that superclass constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, continue with step 4.
Execute the instance initializers and instance variable initializers for this class, assigning the values of instance variable initializers to the corresponding instance variables, in the left-to-right order in which they appear textually in the source code for the class. If execution of any of these initializers results in an exception, then no further initializers are processed and this procedure completes abruptly with that same exception. Otherwise, continue with step 5.
Execute the rest of the body of this constructor. If that execution completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, this procedure completes normally.
If I'm reading this correctly, instance variables should always get initialised before the constructor body is executed.
That would imply that the init{...}
is executed before the constructor.
But that also doesn't make sense because according to these docs,
The Java compiler copies initializer blocks into every constructor.
which would have them executed after the instance variable initialisation, no?
So ... what is going on, here?
Why is userData
in above class null
when it shouldn't be?
Can't find any error on Kotlin' side, may be Android behavior.
During an instance initialization, the initializer blocks are executed in the same order as they appear in the class body, interleaved with the property initializers
Check out the code example in kotlindoc
Note that code in initializer blocks effectively becomes part of the primary constructor. Delegation to the primary constructor happens as the first statement of a secondary constructor, so the code in all initializer blocks and property initializers is executed before the secondary constructor body. Even if the class has no primary constructor, the delegation still happens implicitly, and the initializer blocks are still executed
Your code should execute
first UserManager(application: Application)
,
then AndroidViewModel(application)
,
then private val userData: MutableMap<User, MutableMap<String, Any>> = ...
,
then init { restoreActiveUsers() }
I tried writing this example in my EDI (extending normal class instead of AndroidViewModel
), but I could not reproduce the exception:
private open class Boo (private val input: Int)
private class Foo : Boo(1) {
private val logger = LoggerFactory.getLogger(this::class.java)
val userData: MutableMap<String, MutableMap<String, Any>> = object : HashMap<String, MutableMap<String, Any>>() {
override fun get(key: String): MutableMap<String, Any>? {
val former = super.get(key)
val current = former ?: mutableMapOf()
if (current !== former) this.put(key, current)
return current
}
}
init {
restoreActiveUsers()
}
private fun restoreActiveUsers() {
(1..3).forEach { _ -> logger.info { "${userData["notInside"]}" } }
}
}
Output:
{}
{}
{}
-
Your problem indicates a very weird behavior, since the field userData
is a non-nullable val
and does not produce a compile error, which it would do if the init block would be written above the field! So when it comes purely to kotlin - the field has to be initialized before.
I have no experience with Android development and don't know how the initializing part works there, but I strongly suggest looking there for the problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With