78

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
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256
Andrii Chernenko
  • 9,873
  • 7
  • 71
  • 89
  • In your factory method you say that attrs is nowhere to get, but prevents you from passing null instead of attrs? – Andrey Breslav Dec 20 '13 at 05:53
  • 1
    @AndreyBreslav usually when defining Android view subclass constructors, we call corresponding superclass constructors (like shown in Java example). Calling `super(context, null)` worked for `AdapterView` subclass, but I'm not sure there won't be any side effects for all other view classes in the framework, so it would be nice to be able to call specific superclass constructor. – Andrii Chernenko Dec 20 '13 at 06:56
  • 1
    @AndreyBreslav please take a look at the update. Seems like you're right, since `attrs` is defined as non-null in superclass constructor, I can't pass null to it. – Andrii Chernenko Dec 22 '13 at 11:24
  • looks like this can not be addressed very well in the present state of Kotlin, but you can try to pass an empty instance of AttributeSet by default... – Andrey Breslav Dec 23 '13 at 11:36

10 Answers10

104

Kotlin supports multiple constructors since M11 which was released 19.03.2015. The syntax is as follows:

class MyView : View {
    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
        // ...
    }
 
    constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {}
}

More info here and here.

Edit: you can also use @JvmOverloads annotation so that Kotlin auto-generates the required constructors for you:

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

Beware, though, as this approach may sometimes lead to the unexpected results, depending on how the class you inherit from defines its constructors. Good explanation of what might happen is given in that article.

aga
  • 27,954
  • 13
  • 86
  • 121
  • ...and if you want to do findViewById do that after onFinishInflate(): https://stackoverflow.com/a/3264647/2736039, I had to go back to java to find that the issue wasnt in kotlin impl – Ultimo_m Nov 06 '18 at 18:29
  • What's the use for `defStyle` if you forbid subclassing? Either make the class `open`, or get rid of `defStyle`. – arekolek Sep 26 '19 at 23:30
71

You should use annotation JvmOverloads (as it looks like in Kotlin 1.0), you can write code like this:

class CustomView @JvmOverloads constructor(
    context: Context, 
    attrs: AttributeSet? = null, 
    defStyle: Int = 0
) : View(context, attrs, defStyle)

This will generate 3 constructors just as you most likely wanted.

Quote from docs:

For every parameter with a default value, this will generate one additional overload, which has this parameter and all parameters to the right of it in the parameter list removed.

colriot
  • 1,978
  • 1
  • 14
  • 21
  • 4
    "...take into account that this solution will not be feasible for all types of views. Some of them (as `TextView` for instance) need to call `super` parent to initialize the style." – [Custom Views in Android with Kotlin (KAD 06)](https://antonioleiva.com/custom-views-android-kotlin/) – Travis Sep 20 '17 at 19:28
  • As travis reported, bad practice to use this pattern as you'll have to dig parent constructor implementation to accordingly set defaults attributes each time you create a custom view. Well, i guess it's okay if you warn doc it. – PedroCactus Dec 05 '17 at 10:28
  • ...and if you want to do findViewById do that after onFinishInflate(): https://stackoverflow.com/a/3264647/2736039, I had to go back to java to find that the issue wasnt in kotlin impl – Ultimo_m Nov 06 '18 at 18:29
  • How can I override both versions of constructors? I want to create CustomView programatically where only context is provided – Sazzad Hissain Khan Jun 11 '19 at 09:37
  • @SazzadHissainKhan with `@JvmOverloads` and default values for params several separate constructors will be generated in Java. If you want to cal different `super(...)` variants then you need to implement separate constructors in Kotlin as well – colriot Jun 11 '19 at 11:01
  • Why would you need the `defStyle` parameter if you don't make the class `open`? Just get rid of it and avoid problems it may cause. – arekolek Sep 26 '19 at 23:22
14

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)
}

}
arlomedia
  • 8,534
  • 5
  • 60
  • 108
Sagar Jethva
  • 986
  • 12
  • 26
11

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)
arekolek
  • 9,128
  • 3
  • 58
  • 79
5

This does seem to be an issue. I've never run into this because my custom views have either been created only in xml or only in code, but I can see where this would come up.

As far as I can see, there are two ways around this:

1) Use constructor with attrs. Using the view in xml will work fine. In code, you need to inflate an xml resource with the desired tags for your view, and convert it to an attribute set:

val parser = resources.getXml(R.xml.my_view_attrs)
val attrs = Xml.asAttributeSet(parser)
val view = MyView(context, attrs)

2) Use the constructor without attrs. You can't place the view directly in your xml, but it's easy about to place a FrameLayout in the xml and add the view to it through code.

ajselvig
  • 688
  • 1
  • 5
  • 12
1

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()
    }
}
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256
0

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>
Hitesh Sahu
  • 41,955
  • 17
  • 205
  • 154
0

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)
Mori
  • 2,653
  • 18
  • 24
0

When you have some view (BottomSheetDialog) that already can show text and want to add formatted string, you should add two constructors.

class SomeDialog : BottomSheetDialog {

    private val binding = DialogSomeBinding.inflate(layoutInflater)

    // Base constructor that cannot be called directly
    private constructor(
        context: Context,
        title: CharSequence
    ) : super(context) {
        setContentView(binding.root)
        binding.title.text = title
    }

    // Constructor with simple CharSequence message
    constructor(
        context: Context,
        title: CharSequence,
        message: CharSequence
    ) : this(context, title) {
        binding.message.text = message
    }

    // Constructor with formatted SpannableString message
    constructor(
        context: Context,
        title: CharSequence,
        message: SpannableString
    ) : this(context, title) {
        binding.message.text = message
    }
}

Usage:

val span = SpannableString(getString(R.string.message, name))
...

SomeDialog(
    context = requireContext(),
    title = getString(R.string.title),
    message = span
).show()
CoolMind
  • 26,736
  • 15
  • 188
  • 224
-6

You can try new Library Anko for Kotlin from JetBrains (also you can contribute on github). Currently it is in beta, but you can create views with such code

    button("Click me") {
         textSize = 18f
         onClick { toast("Clicked!") }
    }

Have a look at this library

b-boy sh1ft
  • 179
  • 8