How to create constructor of custom view with Kotlin

Viewed 56728

I'm trying to use Kotlin in my Android project. I need to create custom view class. Each custom view has two important constructors:

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

MyView(Context) is used to instantiate view in code, and MyView(Context, AttributeSet) is called by layout inflater when inflating layout from XML.

Answer to this question suggests that I use constructor with default values or factory method. But here's what we have:

Factory method:

fun MyView(c: Context) = MyView(c, attrs) //attrs is nowhere to get
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }

or

fun MyView(c: Context, attrs: AttributeSet) = MyView(c) //no way to pass attrs.
                                                        //layout inflater can't use 
                                                        //factory methods
class MyView(c: Context) : View(c) { ... }

Constructor with default values:

class MyView(c: Context, attrs: AttributeSet? = null) : View(c, attrs) { ... }
//here compiler complains that 
//"None of the following functions can be called with the arguments supplied."
//because I specify AttributeSet as nullable, which it can't be.
//Anyway, View(Context,null) is not equivalent to View(Context,AttributeSet)

How can this puzzle be resolved?


UPDATE: Seems like we can use View(Context, null) superclass constructor instead of View(Context), so factory method approach seems to be the solution. But even then I can't get my code to work:

fun MyView(c: Context) = MyView(c, null) //compilation error here, attrs can't be null
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }

or

fun MyView(c: Context) = MyView(c, null) 
class MyView(c: Context, attrs: AttributeSet?) : View(c, attrs) { ... }
//compilation error: "None of the following functions can be called with 
//the arguments supplied." attrs in superclass constructor is non-null
9 Answers

Custome View with kotlin here's sample code.

class TextViewLight : TextView {

constructor(context: Context) : super(context) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

}

TL;DR most of the time, it should be enough to just define your custom view as:

class MyView(context: Context, attrs: AttributeSet?) : FooView(context, attrs)

Given this Java code:

public final class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

its Kotlin equivalent would use secondary constructors:

class MyView : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
}

That syntax is useful when you really want to call different super-class constructors depending on whether the view is created in code or inflated from XML. The only case that I know of for this to be true is when you are extending the View class directly.

You can use a primary constructor with default arguments and a @JvmOverloads annotation otherwise:

class MyView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null
) : View(context, attrs)

You don't need @JvmOverloads constructor if you don't plan to call it from Java.

And if you only inflate views from XML, then you can just go with the simplest:

class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs)

If your class is open for extension and you need to retain the style of the parent, you want to go back to the first variant that uses secondary constructors only:

open class MyView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
}

But if you want an open class that overrides the parent style and lets its subclasses override it too, you should be fine with @JvmOverloads:

open class MyView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = R.attr.customStyle,
        defStyleRes: Int = R.style.CustomStyle
) : View(context, attrs, defStyleAttr, defStyleRes)

There are several ways to override your constructors,

When you need default behavior

class MyWebView(context: Context): WebView(context) {
    // code
}

When you need multiple version

class MyWebView(context: Context, attr: AttributeSet? = null): WebView(context, attr) {
    // code
}

When you need to use params inside

class MyWebView(private val context: Context): WebView(context) {
    // you can access context here
}

When you want cleaner code for better readability

class MyWebView: WebView {

    constructor(context: Context): super(context) {
        mContext = context
        setup()
    }

    constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
        mContext = context
        setup()
    }
}

Added a complete example of creating a custom view by inflating XML layout with multiple constructors

class MyCustomView : FrameLayout {
    private val TAG = MyCustomView ::class.simpleName

    constructor(context: Context): super(context) {
        initView()
    }

    constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
        initView()
    }

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int
    ):   super(context, attrs, defStyleAttr) {
        initView()
    }

    /**
     * init View Here
     */
    private fun initView() {
       val rootView = (context
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
            .inflate(R.layout.layout_custom_view, this, true)

       // Load and use rest of views here
       val awesomeBG= rootView.findViewById<ImageView>(R.id.awesomeBG)
      
}

in XML add your layout_custom_view view file

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  
    <ImageView
        android:id="@+id/awesomeBG"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/bg_desc"
        android:fitsSystemWindows="true"
        android:scaleType="centerCrop" />

    <!--ADD YOUR VIEWs HERE-->
 
   </FrameLayout>

It seems, constructor parameters are fixed by type and order, but we can add own like this:

class UpperMenu @JvmOverloads
constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,parentLayout: Int,seeToolbar: Boolean? = false)

    : Toolbar(context, attrs, defStyleAttr) {}

where parentLayout ,seeToolbar are added to it so :

 val upper= UpperMenu (this,null,0,R.id.mainParent, true)
Related