Secondary constructors doesn't let us enjoy the language


#1

When creating a secondary constructor, first I have to call the primary constructor and only then I’m allowed to code, here’s an example when it’s interrupting:

// That's what I want to write!
class SomeViewHolder(val view: View, val firstTextView: TextView, val secondTextView: TextView){

    constructor(view: View){
        var textView1 = view.findViewById(R.id.some_id_1)
        var textView2 = view.findViewById(R.id.some_id_2)
        
        // maybe here I would modify the textViews

        // and then initializing the class
        this(view, textView1, textView2)
    }
}

// That's what I have to write
class SomeViewHolderIHaveToWrite(val view: View){

    val firstTextView: TextView
    val secondTextView: TextView

    init {
        firstTextView = view.findViewById(R.id.some_id_1)
        secondTextView = view.findViewById(R.id.some_id_2)
    }
}

// That's also an example of what I have to write- with more complex parameters it will be impossible to construct it
class SomeViewHolderIHaveToWrite2(val view: View, val firstTextView: TextView, val secondTextView: TextView){
    constructor(view: View): this(view, view.findViewById(R.id.some_id_1), view.findViewById(R.id.some_id_2))
}

#2

I do not quite understand what the problem is:

class SomeViewHolderIHaveToWrite(val view: View){
    val firstTextView = view.findViewById(R.id.some_id_1) as TextView
    val secondTextView = view.findViewById(R.id.some_id_2) as TextView
}

if you prefer to have optional view parameters:

class SomeViewHolderIHaveToWrite(
	val view: View,
	val firstTextView: TextView = view.findViewById(R.id.some_id_1) as TextView,
	val secondTextView: TextView = view.findViewById(R.id.some_id_2) as TextView
)

not an android expert, but why not just use one of this:

class SomeViewHolderIHaveToWrite(val view: View) {
    @BindView(R.id.some_id_1) lateinit var firstTextView: TextView
    @BindView(R.id.some_id_2) lateinit var secondTextView: TextView

    init {
    	ButterKnife.bind(this);
    }
}
import kotlinx.android.synthetic.main.activity_main.*

fun foo() {
	welcomeMessage.text = "Hello Kotlin!"
}

#3

I would like two kinds of constructors- the first is primary- that will be at the class header,
It will also define my variables.

The second constructor will get a single View and will initialize the other variables the way he want.

Eliminate this code because the initialization could take more than one line of code
class SomeViewHolderIHaveToWrite(
val view: View,
val firstTextView: TextView = view.findViewById(R.id.some_id_1) as TextView,
val secondTextView: TextView = view.findViewById(R.id.some_id_2) as TextView
)

about the frameworks- it’s a simple example, so it’s irrelevant.

The thing is I want to declare my variables in the primary constructor at the head of the class and still be able to use them as if I would have written them inside the class. for example:

that:

class SomeViewHolderIHaveToWrite(val view: View){
val firstTextView: TextView
val secondTextView: TextView

}

should be equivalent with all means to that:

class SomeViewHolder(val view: View, val firstTextView: TextView, val secondTextView: TextView)

#4

there are many solutions

most obvious one, decompose complex initialization statements into a simple pure functions:

private fun foo(view: View): TextView = TODO("complex initialization here")
private fun bar(view: View): TextView = TODO("complex initialization here")

class SomeViewHolderIHaveToWrite(
    val view: View, 
    val firstTextView: TextView = foo(view),
    val secondTextView: TextView = bar(view)
)

nullable arguments:

class SomeViewHolderIHaveToWrite(
    val view: View, 
    firstTextView: TextView? = null, 
    secondTextView: TextView? = null
){
    val firstTextView: TextView = firstTextView ?: run {
        TODO("complex initialization here")
    }
    val secondTextView: TextView = secondTextView ?: run {
        TODO("complex initialization here")
    }
}

fabric methods:

// hide primary constructor for uniformity/consistency
class SomeViewHolderIHaveToWrite private constructor(
    val view: View, 
    val firstTextView: TextView,
    val secondTextView: TextView
){
    companion object {
        fun create(view: View): SomeViewHolderIHaveToWrite {
            TODO("complex initialization here")
        }

        fun create(view: View, firstTextView: TextView, secondTextView: TextView) = 
            SomeViewHolderIHaveToWrite(view, firstTextView, secondTextView)
    }
}

“fake” constructors:

class SomeViewHolderIHaveToWrite(
    val view: View, 
    val firstTextView: TextView,
    val secondTextView: TextView
)

fun SomeViewHolderIHaveToWrite(view: View): SomeViewHolderIHaveToWrite {
    TODO("complex initialization here")
}

#5

Thank you for your answers, cool workarounds, at the end there’s always a workaround, just thought to mention the need for a workaround around this problem.


#6

But why?
That’s not what the primary constructor is for.


#7

The suggested answers are not work arounds, they’re idiomatic code.

Namely:

(tabs aren’t showing in the quote)

this is exactly what you want. It does the same thing as your code (except maybe you want them as var instead of val if you plan on modifying them.)

also, if you only want to modify the default, then set it equal to a function that returns the (modified) TextView.


#8

The language let you declare your variables at the class header along with the primary constructor, so if there’s a way to declare it on there, why are they limitations?


#9

The code you mentioned won’t work when you need to initialize the textviews in multiple lines.


#10

any valid kotlin expression can act as field/parameter initializer. So you can use run or let, but its a bit ugly:

class SomeViewHolderIHaveToWrite(
    val view: View,
    val firstTextView: TextView = run {
        TODO("multi-line")
        TODO("initialization")
    }
    val secondTextView: TextView = run {
        TODO("multi-line")
        TODO("initialization")
    }
)

#11

Cool didn’t know that


#12

The design of secondary constructors is influenced by a restriction we find in Java. There, the first statement must call another constructor. That is why Kotlin team decided to use : this() syntax. Yes, they might have been able to generate a factory method instead. Various considerations are at play here though. Happy coding.


#13

Nah. While Java requires it, the JVM doesn’t. It can’t as you can initialise with any expression (also in Java). The reason is more like in Java. It is invalid to operate on partially constructed values. If you do something before invoking the parent this means that the parent is not yet initialised at that point. Any access of the object at that time is (likely) to be invalid. The same holds with primary/secondary constructors. The reason it is designed as is (and why Java does it) is because it is a large surface for programmer error.


#14

Makes sense actually. You can call static methods in the parameter expressions. Those are no different than calling them before.


#15

Swift resolve this problem with a two phase initialisation system. I don’t know if it would be convenient for kotlin.