11

I have tried many solutions found in google by the keywords: multiple constructors, scala, inheritance, subclasses.

None seems to work for this occasion. ImageView has three constructors:

ImageView(context)
ImageView(context,attribute set)
ImageView(context,attribute set, style)

In scala you can only extend one of them. And the solution of using the more complete constructor (ImageView(context,attribute set, style)) and passing default values does not work either because the constructor ImageView(context) does something completely different than the other two constructors.

Some solutions of using a trait or a companion object does not seem to work because the CustomView must be a class! I mean I am not the only one who uses this class (so I could write the scala code any way I wanted) there is also the android-sdk who uses this class and yes it must be a class.

target is to have a CustomView which extends ImageView and all of these work:

new CustomView(context)
new CustomView(context,attribute set)
new CustomView(context,attribute set, style)

Please let me know if you need any further clarification on this tricky matter!

George Pligoropoulos
  • 2,919
  • 3
  • 33
  • 65
  • Can you provide specific examples that require CustomView to be a class? – yakshaver Dec 11 '12 at 21:22
  • yes of course! in a regular layout in android you write some xml. Google Android sdk parses this xml and creates a CustomView instance. It does so (probably) by executing `new CustomView(context, attribute set)`. While in parts of my own code I have to create a CustomView with no attributes so I would have to call `new CustomView(context)`. (no! passing null to the attribute set won't do the job) – George Pligoropoulos Dec 12 '12 at 11:50

4 Answers4

7

According to Martin Odersky (the creator of Scala), that is not possible.

In http://scala-programming-language.1934581.n4.nabble.com/scala-calling-different-super-constructors-td1994456.html:

"is there a way to call different super-constructors within different class-constructors - or does all have to go up to the main-constructor and only one super-constructor is supported?

No, it has to go through the main constructor. That's one detail where Scala is more restrictive than Java."

I think your best approach is to implement your views in Java.

Rui Gonçalves
  • 1,355
  • 12
  • 28
5

It sounds like you may just be better off writing the subclass in Java. Otherwise, if your assumption about the SDK using the three argument constructor is correct, then you could use a trait and a class with a companion object. The SDK would use the three argument constructor of CustomView which also implements a trait containing any additional behavior you need:

trait TCustomView {
  // additional behavior here
}

final class CustomView(context: Context, attributes: AttributeSet, style: Int)
  extends ImageView(context, attributes, style) with TCustomView

In the application code, you could use the one, two or three argument version in this way:

object CustomView {
  def apply(c: Context, a: AttributeSet, s: Int) =
    new ImageView(c, a, s) with TCustomView
  def apply(context: Context, attributes: AttributeSet) =
    new ImageView(context, attributes) with TCustomView
  def apply(context: Context) = new ImageView(context) with TCustomView
}

CustomView(context)
CustomView(context, attributes)
CustomView(context, attributes, style)

Seems like a lot of work. Depending on your goals, you might be able to add additional behavior with implicits:

implicit def imageViewToCustomView(view: ImageView) = new {
  def foo = ...
}
Blaisorblade
  • 6,438
  • 1
  • 43
  • 76
yakshaver
  • 2,472
  • 1
  • 18
  • 21
  • I see... Using the three parameter constructor would throw an exception because inside the sdk there is a call like that: `new CustomView(context, attributes)`. So you definately need both CustomView(2 params) and CustomView(3 params) for the sdk. Thankfully the default value of the third parameter is simply zero so you can have these two. Now what if there is some code in the sdk I haven't used yet that calls `new CustomView(context)` ?! – George Pligoropoulos Dec 12 '12 at 18:08
  • You would be out of luck and left two more sensible options: Java or implicits. – yakshaver Dec 12 '12 at 18:41
1

This question lead me to several design considerations which I would like to share with you.

My first consideration is that if a Java class has been correctly designed the availability of multiple constructors should be a sign of the fact that some class properties might have default value.

Scala provides default values as a language feature, so that you do not have to bother about providing multiple constructors at all, you can simply provide default value for some arguments of your constructor. This approach leads to a much cleaner API than three different constructors which produce this kind of behaviour as you are making clear why you do not need to specify all the parameters.

My second consideration is that this is not always applicable in case you are extending classes of a third-party library. However:

  • if you are extending a class of an open source library and the class is correctly designed, you can simply investigate the default values of the constructor argument
  • if you are extending a class which is not correctly designed (i.e. where overloaded constructors are not an API to default some parameters) or where you have no access to the source, you can replace inheritance with composition and provide an implicit conversion.

    class CustomView(c: Context, a: Option[AttributeSet]=None, s: Option[Int]=None){
    
       private val underlyingView:ImageView = if(a.isDefined)
                                                if (s.isDefined)
                                                    new ImageView(c,a.get,s.get)
                                                else
                                                    new ImageView(c,a.get)
                                            else
                                                new ImageView(c)
    }
    
    object CustomView {
        implicit def asImageView(customView:CustomView):ImageView = customView.underlyingView
    }
    
Edmondo
  • 19,559
  • 13
  • 62
  • 115
  • 2
    I completely agree with you regarding the design considerations you mentioned. However, in that case it won't help him much, as Android constructs instances of views using reflection (and that's why having all three constructors is needed). His class must implement `ImageView` in order to avoid `ClassCastExceptions`; he can, of course, have an inner instance that he can construct correctly, but he also had to redirect all `ImageView` methods to that instance, which is a burden. Between doing that and writing his class in Java... – Rui Gonçalves Dec 21 '12 at 21:20
  • Thanks for this answer I think it is helpful overall! But yes the android sdk got us: `Caused by: android.view.InflateException: Binary XML file line #43: Class is not a View views.CardView` I will keep in mind for future designs though in other kinds of projects. Maybe a little refactoring on the android code so we could have a class correctly designed as you mentioned would be the best – George Pligoropoulos Dec 22 '12 at 00:39
  • Thank you Rui for your comment. You are right ! Is there any reason why Android SDK creates instances with reflection using different constructors? What happens if you provide a single constructor? – Edmondo Dec 23 '12 at 11:47
  • @Edmondo1984 Different sets of attributes in XML files cause calls to different constructors, as you can see [here](http://stackoverflow.com/questions/9195713/do-i-need-all-three-constructors-for-an-android-custom-view). I am not aware of why they didn't define only the three-param constructor, as it would suffice for all use cases I can imagine... – Rui Gonçalves Dec 23 '12 at 23:34
0

According to the Android View documentation View(Context) is used when constructed from code and View(Context, AttributeSet) and View(Context, AttributeSet, int) (and since API level 21 View(Context, AttributeSet, int, int)) are used when the View is inflated from XML.

The XML constructor all just call the same constructor, the one with the most arguments which is the only one with any real implementation, so we can use default arguments in Scala. The "code constructor" on the other hand may have another implementation, so it is better to actually call in from Scala as well.

The following implementation may be a solution:

private trait MyViewTrait extends View {
  // implementation
} 

class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
    extends View(context, attrs, defStyle) with MyViewTrait {}

object MyView {
  def apply(context: Context) = new View(context) with MyViewTrait
}

The "code constructor" may then be used like:

var myView = MyView(context)

(not a real constructor).

And the other once like:

var myView2 = new MyView(context, attrs)
var myView3 = new MyView(context, attrs, defStyle)

which is the way the SDK expects them.

Analogously for API level 21 and higher the class can be defined as:

class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0, defStyleRes: Int = 0)
    extends View(context, attrs, defStyle, defStyleRes) with MyViewTrait {}

and the forth constructor can be used like:

var myView4 = new MyView(context, attrs, defStyle, defStyleRes)

Update:

It gets a bit more complicated if you try to call a protected method in View, like setMeasuredDimension(int, int) from the trait. Java protected methods cannot be called from traits. A workaround is to implement an accessor in the class and object implementations:

private trait MyViewTrait extends View {
  protected def setMeasuredDimensionAccessor(w: Int, h: Int): Unit
  def callingSetMeasuredDimensionAccessor(): Unit = {
    setMeasuredDimensionAccessor(1, 2)
  }
} 

class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
    extends View(context, attrs, defStyle) with MyViewTrait {
  override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
    setMeasuredDimension(w, h)
}

object MyView {
  def apply(context: Context) = new View(context) with MyViewTrait {
    override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
      setMeasuredDimension(w, h)
  }
}
Community
  • 1
  • 1
petter
  • 1,765
  • 18
  • 22