androidkotlinmutablemaplateinit

Newbie Kotlin on Android MutableMap initialisation question


Stripped down to the bare essence, my problem is:

class MainActivity : AppCompatActivity() {

    lateinit var testArray: Array<String>
    lateinit var testMap: MutableMap<String, Array<Array<String>>>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        testArray = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
        testMap["a"] = arrayOf(
            arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"),
            arrayOf("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))

    }
}

Why do I get this error for testMap...

lateinit property testMap has not been initialized

...but no error for testArray?

Edit for improved question quality: I understand that lateinit variables must be initialised later, before they are used - but isn't that what I am doing? It seems to work with testArray, but not with testMap. Why are they different?

Edit 2: This variation...

testMap = mutableMapOf("a" to arrayOf(
    arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"),
    arrayOf("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")))

...works.


Solution

  • I don't know if you got what's happening here, but just in case

    Usually when you create a field (a top-level variable, not inside a function or anything), you have to initialise it with a value:

    // needs to be initialised to something!
    val coolString1: String
    
    // now we're going places
    val coolString2: String = "ok here"
    

    But sometimes you want to define that variable, but you don't actually have the thing you want to assign to it yet (very common in Android, when the actual setup for Activities and Fragments happens in lifecycle callbacks, long after the object was constructed). You want to initialise it, later. That's what lateinit is for

    // has to be a var - and look, you're not assigning a value, and it's fine!
    lateinit var coolString: String
    
    fun calledLater() {
        coolString = "sup"
    }
    

    So lateinit is you promising to initialise that variable before anything tries to access it. The compiler is trusting you to take care of it.

    But did you?

    // no array assigned! Will init later
    lateinit var testArray: Array<String>
    // no map assigned! Will init later
    lateinit var testMap: MutableMap<String, Array<Array<String>>>
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            // assigning that array you promised to! perfect
            testArray = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
    
            // trying to add something to a map that hasn't been created yet!
            // you're accessing it before it's been assigned, you broke your promise
            testMap["a"] = arrayOf(
                arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"),
                arrayOf("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
        }
    

    So you haven't actually assigned a map to that variable yet - there's nothing there to add values to. Normally you'd get warned/prevented from doing this kind of thing, but lateinit allows you to take control, tell the compiler "don't worry, I got this" and safely initialise something later without having to make it nullable. But you have the responsibility to make sure it will always be initialised before it's accessed

    It's an easy enough fix here - just make a map, you can use mapOf the same way you're using arrayOf if you like, mapOf("a" to arrayOf(... But it's good to know what you need to be aware of in general. lateinit can be really useful, but you need to know what you're doing!