ことの発端
以前、Android6(Marshmallow)のランタイムパーミッションを処理しやすくするために、ヘルパーを作ってみたときに、Kotlinだったら、もっと簡単に書けるんじゃないかと思って、こんなコードを書いてみました。
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()
}
}
}
}
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で実装してるはずなのにー、それって何処行っちゃったの?
という疑問がわいたので、もっと単純な構成で、確認してみました。
コンパイルしてデコンパイル
interface Foo {
fun foo(): String
}
interface Bar: Foo {
fun bar(): String;
override fun foo(): String {
return bar();
}
}
class Buzz: Bar {
override fun bar(): String {
return "Buzz"
}
}
を用意して、コンパイルした後、できあがったclassファイルをデコンパイルしてみます。
public interface Foo {
public abstract String foo();
}
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();
}
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で、既に実装されていたのでした(サポートライブラリの闇は深いね)。
なので、
class MainActivity : AppCompatActivity(), PermissionHelper {
:
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super<PermissionHelper>.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
を追加してやれば、コンパイル可能になります。
だけど、これじゃ当初の目的(ボイラープレートなコードをなくす)は果たしてないんだよね。 orz