Posted at

Kotlinにおける実装のあるinterfaceの仕組み

More than 3 years have passed since last update.


ことの発端

以前、Android6(Marshmallow)のランタイムパーミッションを処理しやすくするために、ヘルパーを作ってみたときに、Kotlinだったら、もっと簡単に書けるんじゃないかと思って、こんなコードを書いてみました。


PermissionHelper.kt


interface PermissionHelper: ActivityCompat.OnRequestPermissionsResultCallback {

fun execute(activity: Activity) {
if (ContextCompat.checkSelfPermission(activity, PERMISSION) != PackageManager.PERMISSION_GRANTED) {
// 以前に許諾して、今後表示しないとしていた場合は、ここにはこない
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, PERMISSION) && message != null) {
// ユーザに許諾してもらうために、なんで必要なのかを説明する
val builder = AlertDialog.Builder(activity)
builder.setMessage(message)
builder.setPositiveButton(if (caption == null) "OK" else caption) { dialog, which ->
// 許諾要求
requestPermission(activity)
}
builder.show()
} else {
// 許諾要求
requestPermission(activity)
}
} else {
// 許諾されているので、やりたいことをやる
onAllowed()
}
}

private fun requestPermission(activity: Activity) {
val permissions = arrayOf(PERMISSION)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}

val message: String?
val caption: String?
val REQUEST_CODE: Int
val PERMISSION: String

fun onAllowed()
fun onDenied() {}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == REQUEST_CODE) {
if (permissions.size > 0 && permissions[0] == PERMISSION && grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 許諾されたので、やりたいことをやる
onAllowed()
} else {
onDenied()
}
}
}
}



MainActivity.kt

class MainActivity : AppCompatActivity(), PermissionHelper {

override val message: String? get() = null
override val caption: String? get() = null
override val REQUEST_CODE: Int get() = 123
override val PERMISSION: String get() = Manifest.permission.WRITE_EXTERNAL_STORAGE

val takePhoto1: Button by bindView<Button>(R.id.take_photo_1)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

takePhoto1.setOnClickListener {
execute(this)
}
}

override fun onAllowed() {
throw UnsupportedOperationException()
}
}


ところが、onRequestPermissionsResult()がどーたらこーたらでエラーになってしまって、PermissionHelperで実装してるはずなのにー、それって何処行っちゃったの?

という疑問がわいたので、もっと単純な構成で、確認してみました。


コンパイルしてデコンパイル


Foo.kt

interface Foo {

fun foo(): String
}



Bar.kt

interface Bar: Foo {

fun bar(): String;

override fun foo(): String {
return bar();
}
}



Buzz.kt

class Buzz: Bar {

override fun bar(): String {
return "Buzz"
}
}


を用意して、コンパイルした後、できあがったclassファイルをデコンパイルしてみます。


Foo.java

public interface Foo {

public abstract String foo();
}



Bar.java

public interface Bar extends Foo {

public static final class DefaultImpls {

public static String foo(Bar $this) {
return $this.bar();
}
}

public abstract String bar();

public abstract String foo();
}



Buzz.java

public final class Buzz implements Bar {

public String bar() {
return "Buzz";
}

public Buzz() {
}

public String foo() {
return Bar.DefaultImpls.foo(this);
}
}


Barで実装したはずの、foo()は内部クラスのDefaultImplsにちょっと形を変えて潜り込ませて、foo()自体は、abstractのまま残しています。そして、Buzzでfoo()を実装して、その中で、潜り込ませたfoo()を呼び出しています。

第1引数でthisを渡すあたり、pythonを彷彿とさせますが、まぁ、C++なんかでも使われているオブジェクトのメソッドを(メソッドではないという意味の)関数で実現するオーソドックスな方法です。ちなみに、このパターンは、拡張関数でも使用されています。


まとめ


  • interfaceでの実装は、内部クラスのstaticなメソッドとして隠蔽される

  • 継承元の仮想メソッドは仮想のまま引き継がれる

  • interfaceを実装するクラスで、仮想メソッドが実装され、その際に、interfaceの内部クラスのメソッドが呼び出される

ということですね。


蛇足

Kotlinの仕組みについて理解が深まったのはいいのですが、発端になった問題は解決していません。

MainActivityに、PermissionHelperの内部クラスを使った形で、onRequestPermissionsResult()メソッドができないのは、おかしいんじゃないの?

と、頭を抱えていたのですが、エラーメッセージをよく見てみると、


because it inherits many implementations of it


と言う文字列が。

onRequestPermissionsResult()は、Fragmentからの呼び出しランタイムパーミッションの処理を呼び出したときのために、FragmentActivityで、既に実装されていたのでした(サポートライブラリの闇は深いね)。

なので、


MainActivity.kt

class MainActivity : AppCompatActivity(), PermissionHelper {

:

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super<PermissionHelper>.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}


を追加してやれば、コンパイル可能になります。

だけど、これじゃ当初の目的(ボイラープレートなコードをなくす)は果たしてないんだよね。 orz