タグ「Kotlin」の 記事 30 件中 1 ~ 30 件を表示しています。

ボタン連打されてしまい不正な動作をするのはよくある話

Android アプリ開発においてボタン、というより View に対しタップイベントを仕込む際に View.OnClickListener() を使用するというのは基本だと思うが、何も考えずに実装すると連打された時にイベントが 2 回、3 回発行されてしまう。 これが問題になってくるのが例えば登録処理などの部分で、データが 2 重に登録されてしまったり既に登録されている為不正なレスポンスが返却されてきてしまったりする。

対応策に関しては使い古された話題なので検索すればいくらでも出てくるが、例えばお手軽にAndroidのButtonの連打を防止してみたの記事のようにボタン押下直後にボタンの押下を無効にし Handler を使って 1 秒後に再度有効にする、といった処理を挟む。

ただ、毎回このようなお決まりの処理を書くのは辛いので Kotlin の Extension を使って共通化してみた、という記事である。

実装例

このような Extension を定義する:

/**
 * 短時間での連打による複数回実行を防ぐ setOnClickListener 実装.
  *
  * @param listener setOnClickListener
  */
 fun View.setOnSingleClickListener(listener: () -> Unit) {
     val delayMillis = 1000 // 二度押しを防止する時間
     var pushedAt = 0L
     setOnClickListener {
         if (System.currentTimeMillis() - pushedAt < delayMillis) return@setOnClickListener
         pushedAt = System.currentTimeMillis()
         listener()
     }
 }

私は Handler ではなく pushedAt という押下時の時間を持たせて押下時に現在時刻と比較することにより 1 秒以内の連打を防止する実装を行った。 勿論多くの記事がやっているように Handler でも良いが個人的には Handler インスタンスをその都度 new するより生成コストの低い Long を持たせる方が好きだ。

これを以下のように使用できる:

val register = view?.findViewById<AppCompatButton>(R.id.register)
register.setOnSingleClickListener {
    // 連打してはいけない登録処理...
}

Google Drive REST API

以前 Python で Google Drive にファイルアップロード という記事を書いたがそれの Kotlin (Java) 版である。 Java に関しては Google API Client が用意されているのでそれを導入して指定の手順を踏めば良い。 書くコードは多めなのだが手順としては分かりやすかった。

公式サイトの Quick Start を辿ればファイル一覧取得までは出来るのだが、ここでは「指定のディレクトリにファイルアップロード」までを書くものとする。

公式の Quick Start を Kotlin で書く

まず Google API Developer Console で対象プロジェクトに対し Google Drive API が有効になっていることを確認する。 有効になっていれば「API とサービス -> 認証情報」から「OAuth 2.0 クライアント ID」の認証情報を作成する (既にあればそれを使用する)。 作成したらそれを押下すると「クライアント ID」と「クライアントシークレット」が表示され、画面上部に「JSON をダウンロード」というメニューが表示されるのでそれを押下しダウンロードする。 ファイル名を crient_secret.json とリネームし Java (Gradle) プロジェクトの /src/main/resources 上に配置する。

build.gradle には公式の通り以下を追加する:

dependencies {
    compile 'com.google.api-client:google-api-client:1.23.0'
    compile 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
    compile 'com.google.apis:google-api-services-drive:v3-rev110-1.23.0'
}

公式の Quick Start 相当のコードは以下となる。 ファイル一覧を取るだけでもコード量は少し長いが、そのまま書けば動いた:

...(略)...
typealias DriveFile = com.google.api.services.drive.model.File  // java.io.File と被るので別名をつける

fun index() {
    val transport = GoogleNetHttpTransport.newTrustedTransport()
    val jsonFactory = JacksonFactory.getDefaultInstance()
    val input = javaClass.getResourceAsStream("/client_secret.json")  // クラスパス上ルートディレクトリにあるものとする
    val clientSecrets = GoogleClientSecrets.load(JacksonFactory.getDefaultInstance(), InputStreamReader(input))
    val scopes = Collections.singletonList(DriveScopes.DRIVE)  // Quick Start と異なり書き込み権限を与えておく
    val flow = GoogleAuthorizationCodeFlow.Builder(transport, jsonFactory, clientSecrets, scopes)
            .setDataStoreFactory(FileDataStoreFactory(File("credentials")))  // 認証情報を格納するディレクトリ
            .setAccessType("offline")
            .build()
    val credentials = AuthorizationCodeInstalledApp(flow, LocalServerReceiver()).authorize("user")
    val service = Drive.Builder(transport, jsonFactory, credentials)
            .setApplicationName("アプリ名")
            .build()
    val files = service.files().list()
            .setPageSize(10)
            .setFields("nextPageToken, files(id, name)")
            .execute()
            .files
    println("files: ${files.map { it.name }}")  // ファイル名の一覧が表示される
}

Collections.singletonList(DriveScopes.DRIVE) の箇所が公式 Quick Start では Collections.singletonList(DriveScopes.DRIVE_METADATA_READONLY) になっているが、これだと読み込みだけの限られた処理しかできない (ファイルを書き込もうとすると例外がスローされる)。 後の手順でファイルアップロードを行うために全権限を付与している。

java.io.File を使わなければならないのに Google Drive API の方のファイルオブジェクトが同名の com.google.api.services.drive.model.File になっているのが少し不便に感じるのでここでは typealias で別名をつけておくとコードがスッキリする。

尚、初回実行時に認証が入るのだが、コンソールに Please open the following address in your browser: (URL) といった出力がされるのでそのリンクを踏んで認証を完了させる。 この認証情報は setDataStoreFactory() で指定したディレクトリに入るので、また認証を行いたい場合はこの中のファイルを削除すればもう一度行うことができる。

指定ディレクトリにファイルアップロード

上記コードからの続きで以下を書く:

    val file = DriveFile()
    file.name = "hogefuga"  // ファイル名
    file.parents = arrayListOf("1b5-2LHlr1j0t6dRIsODNlMRXVJ5TGvos")  // 親ディレクトリ ID
    val content = FileContent("text/plain", File("C:\\Users\\hoge\\fuga.txt"))
    service.files().create(file, content).execute()  // execute() を忘れると実行されないので注意

指定ディレクトリの ID は Web 上の Google Drive にアクセスし対象ディレクトリに移動すると URL に表示されるのでそれを使用すれば良い。

アノテーションを定義する方法が面倒な時

Spring Boot でのバリデーション実装に関しては昨日の記事に書いた。 今回は @NotBlank@Size など標準で用意されているアノテーションだけではまかないきれないようなバリデーションを行いたい時にどうするかについて書く。

まず、独自のバリデーションを行いたい場合は自分でアノテーションクラスを作成して Form クラスの対象フィールドに定義するといった方法がある。 これに関しては「Spring バリデーション 独自」などで検索すればいくらでも出てくるのでここでは言及しない。 アノテーションクラスを定義すればアノテーションを付与するだけでどこにでも使えるようになるので便利なのだが、私は以下の理由で後述の方法をとることにした:

  • 普遍的に使うのではなくただ 1 箇所で使いたいだけなのにアノテーションクラスを定義して使うのは面倒
  • RepositoryService などのモデル層が絡むようなバリデーションを使いたい

尚、自分でガリガリ書けば (Spring Validation を使用しなければ) どのようなバリデーションも思いのままだが、ここではそういうことではなくあくまで BindingResult の対象のフィールドにエラーが入った状態で Thymeleaf テンプレートが表示され、対象のフィールドに th:errors="(Form フィールド名)" でエラー表示がされるといったように組み込みのバリデーションと同じ流れで表示されるようにしたい。

BindingResult.rejectValue()

最初 BindingResult.addError() というメソッドを見つけたので何度か試していたのだが、全くうまくいかないので諦めて検索したら正しくは BindingResult.rejectValue() というメソッドを使用するのだという情報を見つけた。 このメソッドは rejectValue({Form フィールド名}, {messages.properties キー}) のようにして任意のタイミングで messages.properties (設定を変更していない場合は ValidationMessages.properties) に定義されているバリデーションエラーメッセージを対象のフィールドに対し設定することができる。 また、バリデーションエラーにプレースホルダがある場合 (例えば「{0} には日付の形式で入力してください。」のような) に rejectValue({Form フィールド名}, {messages.properties キー}, {プレースホルダに渡したい引数の配列}, {デフォルトメッセージ}) のようにして引数を渡すこともできる。

実装例

ここでは例として「同一日付に対する記事は登録できない (但し更新時は自分自身を対象外とする)」というバリデーション実装を行う。 messaegs.properties に以下リソースが定義されているものとする:

validation.date-format={0} は日付形式で入力してください。
validation.date-already-registered=その日付の記事は既に登録されています。

コントローラ側に組み込みのバリデーションを実施した後今回の独自バリデーションを以下のように実装する:

/**
 * 記事を追加する.
 *
 * @param model モデル
 * @param form PostForm
 * @param result BindingResult
 * @param id postId
 * @return template 名
 */
@PostMapping("/posts/add", "/posts/{id}")
fun savePost(model: Model, @Validated @ModelAttribute("form") form: PostForm, result: BindingResult,
        @PathVariable("id") id: Int?): String {
    model.addAttribute("tags", tagService.findAll())

    // 組み込みバリデーションエラーに引っかかった場合
    if (result.hasErrors()) {
        return "/posts/add"
    }

    // 日付の形式が間違っている場合はエラー
    val date: LocalDate
    try {
        date = LocalDate.parse(form.date)
    } catch (e: DateTimeParseException) {
        e.printStackTrace()
        result.rejectValue("date", "validation.date-format", arrayOf("日付"), "")
        return "/posts/add"
    }

    // 既に登録されている年月日の場合はエラー (但し更新時は自分自身を対象にしない)
    val post2 = postService.findByDate(date)
    if (post2 != null && (id == null || id != post2.id)) {
        result.rejectValue("date", "validation.date-already-registered")
        return "/posts/add"
    }

    TODO("保存処理")
    return "redirect:/posts"
}

これで同一日付で記事を登録しようとした時に「その日付の記事は既に登録されています。」といったエラーメッセージが表示される。

導入と properties の統合及び UTF-8 化

まず build.gradle に以下を定義する:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-validation')
}

これですぐに使えるわけだが、その前にバリデータの設定をする。 後述するがバリデーションメッセージに関してはデフォルトで ValidationMessages.properties に定義されているものを使用するのだが、これも後述するがバリデーションメッセージのプレースホルダに適用されるフィールド名は messages.properties のものが使用されるので 2 箇所に書くことになってしまう。 できれば messages.properties に両方書くようにしたい。 更にデフォルトで properties ファイルは UTF-8 エンコーディングになっていない。 これも UTF-8 に変更したい。

これを実現するには以下のような WebMvcConfigurer を実装したコンフィギュレーションファイルを定義する:

@Configuration
class Configuration : WebMvcConfigurer {

    /**
     * バリデータを返す.
     *
     * @return バリデータ
     */
    override fun getValidator(): Validator? {
        val source = ReloadableResourceBundleMessageSource().also {
            it.setBasename("classpath:messages")  // ValidationMessages.properties でなく messages.properties を使用する
            it.setDefaultEncoding("UTF-8")  // エンコーディングとして UTF-8 を使用する
        }
        return LocalValidatorFactoryBean().also { it.setValidationMessageSource(source) }
    }
}

ちなみに Spring 4 以前は WebMvcConfigurerAdapter というインターフェースを使用していたようだが Spring 5 (Spring Boot 2.0) では非推奨となり WebMvcConfigurer を使用するようになった。

messages.properties でなく messages.yaml などというファイルを置くとよろしくやってくれるのかどうか試したのだが駄目だった。 残念。

バリデーション例

例えば以下のように messages.properties に定義してあるものとする:

# フィールド名
date=日付
name=名前
tag=タグ
markdown=Markdown

# バリデーションメッセージ (デフォルトの差し替え)
javax.validation.constraints.NotBlank.message={0} を入力してください。

# カスタムバリデーションメッセージ
validation.max-length={0} は {1} 文字以下で入力してください。
validation.not-selected={0} を選択してください。

javax.validation.constraints.NotBlank.message に関してはバリデーションで使用する @NotBlank アノテーションでのエラー時に表示されるメッセージの差し替えである。 この場合 @NotBlank アノテーションのパッケージ名を含めたクラス名が javax.validation.constraints.NotBlank の為それに .message を加えたものを定義しておくとデフォルトメッセージの差し替えができる。 @NotEmpty@Size なども同様となる。

尚、こういう全体的に適用できるバリデーションメッセージだけでなく項目ごとに個別に指定したいバリデーションエラーメッセージがある。 例えば @Size によるバリデーションエラーは「0 文字以上 64 文字以下」のような表示になってしまうが、多くの場合は「0 文字」は不要で最大桁数のみ通知すればいいはずだ。 こういう場合に任意のプロパティ名でカスタムバリデーションメッセージを定義しておく。

フィールド名に関してはフォームの POST 時に使用する Form インスタンスのフィールド名と同じにしておく。

Form

class PostForm {

    // 必須
    @NotBlank
    var date: String = ""

    // 必須かつ 64 文字以内
    @NotBlank @Size(min = 0, max = 64, message = "{validation.max-length}")
    var name: String = ""

    // 選択必須
    @NotEmpty(message = "{validation.not-selected}")
    var tag: Array<Int> = arrayOf()

    // 必須
    @NotBlank
    var markdown: String = ""
}

上記の例のようにバリデーションエラー時に表示されるカスタムメッセージを messages.properties を使用して指定したい場合は {validation.max-length} のような記法で書く。 {} を付けずに書くと任意の文字列を指定できるが、折角 messages.properties が使えるのにハードコーディングすることもないだろう。

Controller

@Controller
class MyController {

    /**
     * 記事追加画面を表示する.
     *
     * @param model モデル
     * @param form PostForm
     * @return template 名
     */
    @GetMapping("/posts/add")
    fun addPost(model: Model, @ModelAttribute("form") form: PostForm): String = "posts/add"

    /**
     * 記事を追加する.
     *
     * @param model モデル
     * @param form PostForm
     * @param result BindingResult
     * @return template 名
     */
    @PostMapping("/posts/add")
    fun savePost(model: Model, @Validated @ModelAttribute("form") form: PostForm, result: BindingResult): String {
        if (result.hasErrors()) {
            return "/posts/add"
        }
        
        // TODO 登録処理
        return "redirect:/posts"
    }
}
  • 登録画面初期表示時の GET と POST を分ける (Form に対するバリデーション指定の為)
  • BindingResultForm の直後の引数として定義する (※位置が違うと正しく機能しないので注意) とメソッド内で result.hasErrors() でバリデーションエラーの有無を取得できる (BindingResult がないとメソッドの中まで処理が進まずに弾かれてしまう)
  • 登録画面初期表示時の GET のメソッドの方にも Form を含めたほうが良い (フォーム項目の初期データの表示時に Form に直接値をセットすれば良い)
  • FormModel は HTML 内の <form> によって POST される項目か否かで使い分けると良さそうに見える

Thymeleaf

<form th:action="@{/posts/add}" method="POST" th:object="${form}">
    <div>
        <input type="text" id="date" name="date" th:value="*{date}" th:classappend="${#fields.hasErrors('*{date}') ? 'is-invalid' : ''}">
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{date}')}" th:errors="*{date}"></div>
    </div>
    <div>
        <input type="text" id="name" name="name" th:value="*{name}" th:classappend="${#fields.hasErrors('*{name}') ? 'is-invalid' : ''}">
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{name}')}" th:errors="*{name}"></div>
    </div>
    <div>
        <select class="custom-select" id="tag" name="tag" multiple="multiple" th:classappend="${#fields.hasErrors('*{tag}') ? 'is-invalid' : ''}">
            <option th:each="t : ${tags}" th:value="${t.id}" th:text="${t.name}" th:selected="${#arrays.contains(form.tag, t.id)}"></option>
        </select>
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{tag}')}" th:errors="*{tag}"></div>
    </div>
    <div>
        <textarea id="markdown" name="markdown" class="form-control" rows="16" th:classappend="${#fields.hasErrors('*{markdown}') ? 'is-invalid' : ''}">[[*{markdown}]]</textarea>
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{markdown}')}" th:errors="*{markdown}"></div>
    </div>
    <div>
        <input type="submit" value="保存"/>
    </div>
</form>

<form> の内容を全部書いたので若干見づらいが重要なのは ${#fields.hasErrors('(Form フィールド名)')} でそのフィールドにエラーがあるかどうかが取得でき th:errors="(Form フィールド名)" で対応するエラーメッセージが要素内のテキストノードに格納されるということだ。 コードにも例示したがエラーの有無で入力フォームの見た目を変えたい場合は th:classappend を使用して class 属性を追加して CSS で見た目を変更すれば良い。

ここまで定義した内容で名前以外すべて空、名前は最大文字数をオーバーした状態で submit すると以下のようにエラーメッセージが表示される:

  • 日付 を入力してください。
  • 名前 は 64 文字以下で入力してください。
  • タグ を選択してください。
  • Markdown を入力してください。

Android 開発で未だに完全な Java 8 が使えないということもあり (Stream API でさえも最低ビルドターゲットを引き上げないと使用することができない)、どうしても Java 8 Time API への移行ができずにいにしえの DateCalendar などを駆使してイマイチな日付処理を行う癖がついてしまっていた。 Spring Boot ならばちゃんと Java 8 Time API が使用できるので古い API は捨て去りたい。

しかし Thymeleaf が標準で備えている #dates.format() などのユーティリティメソッドは Date を対象としておりそのままでは LocalDateTime などは使用することができない。 これでは困ってしまうので拡張機能を導入する。

解決法

build.gradle に以下を追加する:

dependencies {
    compile('org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.1.RELEASE')
}

どこでもいいのだが component-scan 対象パッケージのどこかに以下のような @Bean 定義を置く:

@Configuration
class Configuration {

    /**
     * Java 8 Time Dialect を返却する.
     *
     * @return Java 8 Time Dialect
     */
    @Bean
    fun java8TimeDialect() = Java8TimeDialect()
}

これだけで Thymeleaf 上で LocalDateLocalDateTime を扱うことができるようになる。 例えば #date.format() にあたるのは #temporals.format() で以下のように書く:

<div th:text="${#temporals.format(firstDate, 'yyyy/M/d')}"></div>

他の使い方は Thymeleaf - Module for Java 8 Time API compatibility の GitHub に書いてあるので参照すればすぐ分かるだろう。

server.xml の場合

Tomcat はデフォルトで PUT と DELETE のリクエストボディが無効になっているらしく POST と同じような感じでフォームデータを PUT, DELETE してもすべてクリアされてしまう。 これを有効にするには server.xml に以下の様に設定を行う:

<Connector port="8080" protocol="HTTP/1.1" 
           connectionTimeout="20000"
           redirectPort="8443"
           parseBodyMethods="POST,PUT,DELETE"
           URIEncoding="UTF-8" />

Spring Boot 組み込み Tomcat の場合

Spring Boot で Application クラスを実行して組み込みの Tomcat が立ち上がった際もこの設定を有効にしたい。 この場合 application.yaml ではなく @Configuration アノテーションを付けたクラスに @Bean として TomcatServletWebServerFactory を返すメソッドを書く:

@Configuration
class Configuration {

    /**
     * 組み込み Tomcat のデフォルトで PUT, DELETE に Request Body が許可されていないので許可する.
     *
     * @return TomcatServletWebServerFactory
     */
    @Bean
    fun tomcatEmbeddedServletContainerFactory(): TomcatServletWebServerFactory = object : TomcatServletWebServerFactory() {
        override fun customizeConnector(connector: Connector?) {
            super.customizeConnector(connector)
            connector?.parseBodyMethods = "POST,PUT,DELETE"
        }
    }
}

Tomcat の他の設定をいじりたい場合も同じようにここに追記することができるようだ。

JPA の Entity では単一キーである id 列を持たせるような構造を定義することが出来るが SQLite の INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT な列に関して適切な定義がよく分からなかったのでメモ。 例えば以下のようなテーブルがあるとする:

create table comments (
    id integer not null primary key autoincrement,
    name varchar(16) not null,
    body text not null,
    created datetime not null,
    modified datetime not null,
);

このような AUTOINCREMENTPRIMARY KEY があると SQLite は内部的に sqlite_sequence というテーブルに各テーブルのシーケンスを格納するという挙動をする。 sqlite_sequence テーブルの DDL は以下のようになっている:

CREATE TABLE sqlite_sequence (name, seq);

name にはテーブル名、seq には現在のシーケンス値が格納される。 この sqlite_sequence テーブルを Hibernate 側に教えてやればよい。 この場合の Entity 定義は以下のようになる:

@Entity
@Table(name = "comments")
data class Comment(
        @Id
        @GeneratedValue(generator = "sqlite_comments")  // Generator 名 (何でもよい)
        @TableGenerator(
                name = "sqlite_comments",  // @GeneratedValue.generator と合わせる
                table = "sqlite_sequence",  // SQLite のシーケンステーブル名と合わせる
                pkColumnName = "name",  // sqlite_sequence のシーケンスカラム名 (name 固定)
                valueColumnName = "seq",  // sqlite_sequence のシーケンス値名 (seq 固定)
                pkColumnValue = "comments",  // sqlite_sequence.name に格納されている値 (テーブル名)
                initialValue = 1,  // シーケンス初期値. 多くの場合 1
                allocationSize = 1  // AUTO INCREMENT される場合の増減値. 何故かデフォルト 50 になっているので 1 を指定する
        )
        var id: Int? = null,
        var name: String = "",
        var body: String = "",
        @CreatedDate var created: Date = Date(),
        @LastModifiedDate var modified: Date = Date()
)

他のフレームワークには用意されていたりする

テンプレートにおける表示用の加工処理は大体 Thymeleaf が標準で備えているが、例えば CakePHP における Helper だったり Django における独自のテンプレートタグの作成のようにテンプレートの機能だけでは賄いきれない、多くはプレゼンテーション層における HTML への変換のためのロジックを使いたい時がある。 こういう時に Spring Boot における Thymeleaf 上ではどうすればいいのだろうか、というのが今回のテーマである。

勿論 ControllerModel (Form) に対応する HTML タグへの変換処理を書いたりすることはできるが MVC において本来コントローラやモデルでプレゼンテーション層の処理を書くのは好ましくないので避けたいところだ。 いろいろ試行錯誤してみた結果、プレゼンテーション層のヘルパクラスをコンポーネントとして登録して Thymeleaf 側から呼び出すのが一番シンプルな気がした。

ヘルパークラス定義

今回は Markdown で書かれたテキストを HTML に変換したいとする。 以下のようなヘルパークラスを任意のパッケージに定義する:

/**
 * Thymeleaf テンプレート上で使用するヘルパ.
 */
@Component
class Helper {

    /**
     * Markdown を HTML に変換して返す.
     *
     * @param markdown Markdown
     * @return HTML に変換された Markdown
     */
    fun toHtml(markdown: String): String {
        val (parser, renderer) = Parser.builder().build() to HtmlRenderer.builder().build()
        return renderer.render(parser.parse(markdown))
    }
}

@Component アノテーションを付与することにより Spring 管理下のコンポーネントとして機能する。 これを Thymeleaf 上で使用するには以下のように ${@helper.toHtml(xxx)} といった @ を頭につけた記法となる:

<!-- HTML エスケープされないように th:utext を使用する -->
<div class="post-content" th:utext="${@helper.toHtml(post.markdown)}"></div>

後はこういう要件が出てくる度にこの Helper クラスにメソッドを追加していけばよい。

定数の参照はどうする

同じような悩みとして Thymeleaf 上から Kotlin の定数を参照したいというのがある。 一応 Thymeleaf 上で ${T(パッケージ.クラス名).static フィールド名} という記法で任意の static フィールドやメソッドを呼び出すことはできる。 ただ、例えば Kotlin で object を使用して static を表現したとする:

object Consts {
    val DAYS = arrayListOf('月', '火', '水', '木', '金', '土', '日')
}

上記の定数を Thymeleaf 側で参照するには ${T(com.kojion.etc.Consts).INSTANCE.DAYS} のようにしなければならない。 Kotlin の object が Java コード側から見るとシングルトンな INSTANCE という static フィールドを介してアクセスするようになっているので INSTANCE といちいち付けなければならず、あまり綺麗とは言えない。

この場合あえて Kotlin でなく Java で書いてみる:

public class Consts {
    public static final List<String> DAYS = Arrays.asList("月", "火", "水", "木", "金", "土", "日");
}

これで ${T(com.kojion.etc.Consts).DAYS} とアクセスできるので少しシンプルになった。 Thymeleaf 側からは Kotlin でなく Java として見なければならないのが少し辛いところだ。

パッケージ名を書くのも気になる場合は、先程のヘルパークラスと同様にコンポーネントとして登録して定数定義するのがいいのかもしれない:

@Component
object Consts {
    val DAYS = arrayListOf("月", "火", "水", "木", "金", "土", "日")
}

これで Thymeleaf 側から ${@consts.DAYS} でアクセスできるようになった。 object で定義しているので Kotlin 側から定数としてアクセスしたい場合も自然だ。

手軽に入れ物を作る場合便利

Kotlin にはデータクラスという JavaBeans のようにデータを入れることに特化したクラスを簡単に作成するための仕組みがある。 詳しくは公式リファレンスを参照すればよいが、簡単に書くと以下を自動で用意してくれる:

  • equals() / hashCode()
  • "User(name=John, age=42)" 形式の toString()
  • 宣言した順番でプロパティに対応する componentN() 関数
  • copy() 関数

尚、上記の恩恵を受けられるのはプライマリコンストラクタに指定したプロパティのみということに注意が必要である。 クラスの本文に書いたフィールドに関しては一切データクラスの影響を受けない。 以下それを検証する。

以下のような Kotlin というデータクラスが定義されているものとする:

// a, b, c, d の 4 つのプロパティを受け取るプライマリコンストラクタ
data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    var e: Int = 0  // ただのフィールド (再代入可)
    val f: Int = 0  // ただのフィールド (再代入不可)
}

この Kotlin クラスを Java 側から使ってみる:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin(0, 0, 0, 0);
        kotlin.setA(1);
        kotlin.setB(1);
        kotlin.setE(1);

        // プライマリコンストラクタに指定してあるフィールドのみ toString() の対象になる
        System.out.println(kotlin);  // Kotlin(a=1, b=1, c=0, d=0)

        // プライマリコンストラクタに指定してあるフィールドのインスタンスの equals() がすべて true ならば true
        final Kotlin kotlin2 = new Kotlin(1, 1, 0, 0);
        kotlin2.setE(2);  // 関係ない値を違う値にする
        System.out.println(kotlin2);  // Kotlin(a=1, b=1, c=0, d=0)
        System.out.println(kotlin.equals(kotlin2));

        kotlin2.setA(2);  // 関係ある値を違う値にしてみる
        System.out.println(kotlin.equals(kotlin2));  // false

        // コピーしても関係ない値はコピーされない
        System.out.println(kotlin2.getE());  // 先ほど変更したので 2
        final Kotlin kotlin3 = kotlin2.copy(3, 3, 3, 3);  // Kotlin で呼ぶと任意のプロパティのみ変更可
        System.out.println(kotlin3.getE());  // 2 ではなく初期値の 0
    }
}

デフォルトコンストラクタの作成条件

前述の Kotlin クラスでは引数なしのコンストラクタ、いわゆるデフォルトコンストラクタが作成されない。 データクラスにおけるデフォルトコンストラクタの作成条件はプライマリコンストラクタのすべてのプロパティに初期値が存在することとなっている。 つまり前述の Kotlin クラスを以下のように書き換える:

// a, b, c, d すべてに初期値を設定
data class Kotlin(var a: Int = 0, var b: Int = 0, val c: Int = 0, val d: Int = 0) {
    var e: Int = 0
    val f: Int = 0
}

するとコンストラクタがデフォルトコンストラクタとプライマリコンストラクタの 2 種に増えている:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin();  // デフォルトコンストラクタを呼ぶ
        final Kotlin kotlin2 = new Kotlin(1, 2, 3, 4);  // 従来のすべての引数ありコンストラクタも作成される
    }
}

尚、以下のようにデフォルトコンストラクタを明示的に宣言しても良い:

data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    constructor(): this(0, 0, 0, 0)  // 明示的なデフォルトコンストラクタ
    var e: Int = 0
    val f: Int = 0
}

JPA Entity ではデフォルトコンストラクタの定義が必要

ここからは Spring Boot での JPA の話である。 例えば以下のようにデータクラスで Entity を定義する:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int,
        var date: String,
        var name: String,
        var body: String,
        var enabled: Boolean,
        var created: Date,
        var modified: Date
)

これを PostRepository から DB アクセスを行うと以下のエラーが表示される:

org.hibernate.InstantiationException: No default constructor for entity:  : com.kojion.entity.Post

先ほどの教訓から解決法は明らかだ。 以下のようにすべてデフォルト値を指定してやれば良い:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        var date: String = "",
        var name: String = "",
        var body: String = "",
        var enabled: Boolean = false,
        var created: Date = Date(),
        var modified: Date = Date()
)

toString() が循環呼び出しされてしまう場合データクラスの対象外にする

例えば以下のように相互にアソシエーションを張っている場合に起きる:

// Post は複数の Tag を持つ
@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        @ManyToMany
        @JoinTable(name = "posts_tags", joinColumns = [JoinColumn(name="post_id")], inverseJoinColumns = [JoinColumn(name="tag_id")])
        var tags: List<Tag> = arrayListOf()
)

// Tag も複数の Post を持つ
@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0,
        @ManyToMany(mappedBy = "tags")
        var posts: List<Post> = arrayListOf()
)

この例だと PostTag が中間テーブル posts_tags を通して多対多のアソシエーションが張られている。 ここで同じように PostRepository から取得した Entity を出力しようとすると java.lang.StackOverflowError: null とクラッシュする。 PosttoString() しようとしてフィールドの List<Tag> に対しても toString() を試み、更に Tag にも子の List<Post> があり……というわけである。

この例の場合 Post が何の Tag を持つかは見たいが Tag が何の Post を持っているかはそこまで見たくない (必要ならば別途取ってくれば良い)。 そこで Tag 側の @ManyToMany 定義されているプロパティをプライマリコンストラクタの範囲から出すことで TagtoString() しようとした時に子の List<Post> を見に行かなくなる:

@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0
) {
    @ManyToMany(mappedBy = "tags")
    var posts: List<Post> = arrayListOf()
}

最初に注意として JDK は今のところ必ず 1.8 (Java 8) を使用すること。 JDK 9.0 を使用してしまうと以下の手順の中で原因不明のエラーが表示されてしまう。 Kotlin 側のプラグインもまだ 1.8 までしか用意されていない。

まず PostgreSQL の例

Spring Boot やその他 Java EE プロジェクトでデータベースを扱う際は JPA を使用することが多いと思うが、まずここでは IntelliJ IDEA を使用し Spring Boot で PostgreSQL を使用できる環境を作る手順を示す。 新規プロジェクトの Spring Initializr の依存関係で以下を選択する:

  • Web (Spring MVC)
  • Thymeleaf
  • JPA
  • PostgreSQL

この状態でアプリケーションクラスを実行すると以下のエラーとなる:

Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified and no embedded datasource could be auto-configured.

エラーメッセージの通り application.yaml に以下を定義する:

spring:
  datasource:
    url: jdbc:postgresql://localhost/postgres
    driverClassName: org.postgresql.Driver
    username: postgres
    password: postgres

これで実行するとまたエラーとなる:

java.lang.reflect.InvocationTargetException: null
...(中略)...
Caused by: java.sql.SQLFeatureNotSupportedException: org.postgresql.jdbc.PgConnection.createClob() メソッドはまだ実装されていません。

これでは何だか分からないので Stack Overflow に聞いてみると application.yaml に以下の設定を追加すれば良いようだ:

spring:
  jpa:
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        dialect: org.hibernate.dialect.PostgreSQLDialect

この状態で再度アプリケーションクラスを実行し、先ほどのエラーが表示されずに正しく Tomcat が起動する事を確認する。

SQLite の場合

SQLite の場合 PostgreSQLDialect に値する SQLiteDialect といった実装が最初から用意されていない。 ただ、これに関しては既に作成して Maven リポジトリに上げている方がいらっしゃるのでありがたく使用させていただくことにする。 それと SQLite の JDBC ドライバも必要なので build.gradle に以下を追加する:

dependencies {
    compile('com.enigmabridge:hibernate4-sqlite-dialect:0.1.2')
    runtime('org.xerial:sqlite-jdbc')
}

これに従い application.yaml を以下に書き換える:

spring:
  datasource:
    # SQLite のファイル位置を指定
    url: jdbc:sqlite:./db.sqlite3
    driverClassName: org.sqlite.JDBC
  jpa:
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        dialect: com.enigmabridge.hibernate.dialect.SQLiteDialect

アプリケーションクラスを実行し、正しく Tomcat が立ち上がることを確認する。

簡単にテストしてみる

JPA の詳細に関しては検索すればいくらでも出てくるのでここには記載しない。 まず該当するテーブルが SQLite 内にあるものとして以下のような感じで Entity クラスを用意する:

@Entity
@Table(name = "post")
data class Post(
        @Id @GeneratedValue(strategy = GenerationType.AUTO) var id: Int = 0,
        var date: String = "",
        var name: String = "",
        var text: String = "",
        var enabled: Boolean = false,
        var created: Date = Date(),
        var modified: Date = Date()
)

Kotlin だと data classEntity が用意できるのでとても便利だ。 toString() を実装しなくてもいい感じにクラス内のデータを出力してくれる。 var にして意味のない初期値を与えなければならないところがちょっと格好悪いが仕方がないところだろうか。

対応する PostRepository クラスを以下のように定義する:

interface PostRepository : JpaRepository<Post, String> {
    fun findById(id: Int): Post
}

定義したリポジトリを使用してコントローラから実行してみる (本来は Service から実行するのが筋だがここでは例のため簡単にする):

@Controller
class SampleController {

    @Autowired
    lateinit var postRepository: PostRepository

    @GetMapping("/")
    fun sample(model: Model): String {
        val post = postRepository.findById(1)  // ID をキーにして 1 件取得
        System.out.println(post)  // Post の中身が出力される
        return "sample"
    }
}

特に意味はないがテンプレートが必要なので /resources/templates/sample.html として以下を用意:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
<p>Hello World!!</p>
</body>
</html>

実行して http://localhost:8080 にアクセスし、標準出力に正しく DB 取得結果が表示されることを確認する。

Spring Web の例は前回行ったので、今回は Spring Batch での Hello World を実施する。

要件とやりたいこと

  • IntelliJ IDEA + Kotlin + Spring Boot 2.0.0 + Gradle
  • 複数の Job を作成しコマンドライン引数で実行を分ける
  • ユニットテストでの実行を行えるようにする

初期設定

IntelliJ IDEA の新規プロジェクトから Spring Initializr を選択。 Gradle Project で言語を Kotlin にしパッケージングは jar を選択。

依存関係のところでは勿論 Batch を入れるのだが Spring Batch はデータベースが使用できるようになっていないと実行できない。 その為対応する JDBC ドライバが必要なのでこれも導入する。 ここでは PostgreSQL がローカルに既にインストールされているものとする。 生成されたプロジェクトの build.gradle の依存関係は以下のようになる:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-batch')
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    runtime('org.postgresql:postgresql')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.batch:spring-batch-test')
}

この時点で Application クラスを実行すると以下のようにエラーが出力される:

Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified and no embedded datasource could be auto-configured.

DB の URL 設定がなされていないというわけで、プロパティファイルより YAML で書いたほうが便宜がいいということですでにある application.properties を削除し application.yaml として以下を定義する:

spring:
  datasource:
  url: jdbc:postgresql://localhost/postgres
  username: postgres
  password: postgres
  driverClassName: org.postgresql.Driver

この状態で同様に実行し、まだバッチの実装をしていないので何も起きないがとりあえず上記のエラーが出ずに正常にアプリケーションの実行が終了することを確認する。

Tasklet の実装

ネットの情報を見てみると CommandLineRunner で実装する方法と Tasklet 若しくは Reader, Processor, Writer で逐次処理をする方法があるようだが、ここでは Spring Batch 公式の Quick Start に記載してある通り Tasklet を使用する。 以下のような BatchConfiguration クラスを定義する:

@Configuration
@EnableBatchProcessing
class BatchConfiguration(val jobBuilderFactory: JobBuilderFactory, val stepBuilderFactory: StepBuilderFactory) {

    @Component
    class Tasklet1 : Tasklet {
        override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
            System.out.println("Hello World!")
            return RepeatStatus.FINISHED
        }
    }

    @Component
    class Tasklet2 : Tasklet {
        override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
            System.out.println("Hello World2!")
            return RepeatStatus.FINISHED
        }
    }

    @Bean
    fun step1(): Step? = stepBuilderFactory.get("step1")?.tasklet(Tasklet1())?.build()

    @Bean
    fun step2(): Step? = stepBuilderFactory.get("step2")?.tasklet(Tasklet2())?.build()

    @Bean
    fun job1(): Job? = jobBuilderFactory.get("job1")?.start(step1())?.build()

    @Bean
    fun job2(): Job? = jobBuilderFactory.get("job2")?.start(step2())?.build()
}

従来の Spring だと、この場合の JobBuilderFactoryStepBuilderFactory を DI する為に @Autowired アノテーションを付けていたようだが、最近のバージョンだと付けなくても注入してくれるようだ。 勿論 @Autowired を付けても良い。

この例だと job1()job2() という 2 つのジョブが定義されていることになる。 ジョブは更にステップという単位に分割され順々に実行することができるようだが、ここでは 1 ジョブ 1 ステップ構成ということでそれぞれ step1()step2() を定義している。 また Tasklet の定義はラムダ式でも良いのだが、後述するが Tasklet 単位でのユニットテストを行いたいのであえてクラスとして定義している。

この状態で実行してみると以下のエラーが出る:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
...(中略)...
Caused by: org.postgresql.util.PSQLException: ERROR: relation "batch_job_instance" does not exist

この batch_job_instance というテーブルは何なのだろうか。 調べてみると、どうも Spring Batch がジョブの実行状態を管理するために内部的に生成するメタデータらしい。 このメタデータのテーブルがまだ DB に無いため does not exist と言われてしまうわけだ。

このメタデータを生成したい場合は application.yaml に以下のように書けば良い:

spring:
  batch:
    initialize-schema: always

これで先ほどのように実行すれば問題なく動作するのだが、このメタデータで管理されているのが逆に煩わしく感じる。 多くのプロジェクトの場合ジョブの管理など不要で、実行したい時に実行できればそれでいいはずだ。 以下、このメタデータを使わなくてもバッチを実行できるようにする。

メタデータを使用しないでいい方法

これに関しては Spring Batchのメタデータテーブルを作らせない/使わせないが大変参考になった。 記事を参考に同様の実装を Kotlin で行う。 以下のような MyBatchConfigurer を定義する:

@Component
class MyBatchConfigurer : BatchConfigurer {

    private val transactionManager = ResourcelessTransactionManager()

    private val mapJobRepositoryFactoryBean = MapJobRepositoryFactoryBean(transactionManager).also { it.afterPropertiesSet() }

    private val jobRepository = mapJobRepositoryFactoryBean.`object`!!

    private val jobExplorer = MapJobExplorerFactoryBean(mapJobRepositoryFactoryBean).also { it.afterPropertiesSet() }.`object`!!

    private val jobLauncher = SimpleJobLauncher().also {
        it.setJobRepository(jobRepository)
        it.afterPropertiesSet()
    }

    override fun getJobRepository(): JobRepository = jobRepository

    override fun getJobLauncher(): JobLauncher = jobLauncher

    override fun getJobExplorer(): JobExplorer = jobExplorer

    override fun getTransactionManager(): PlatformTransactionManager = transactionManager
}

この状態で再度実行すると Tasklet1Tasklet2 の実装が呼び出され Hello World! と Hello World2! が出力される。

Tasklet のユニットテストを行う

以下のように @Autowired を使用して注入した Tasklet インスタンスに対して普通に実行してみる:

@RunWith(SpringRunner::class)
@SpringBootTest
class TestApplicationTests() {

    @Autowired
    lateinit var tasklet1: BatchConfiguration.Tasklet1

    @Autowired
    lateinit var tasklet2: BatchConfiguration.Tasklet2

    @Test
    fun tasklet1() {
        tasklet1.execute(null, null)
    }

    @Test
    fun tasklet2() {
        tasklet2.execute(null, null)
    }
}

こうすると以下のように 2 重に実行されてしまう:

Hello World!
Hello World2!
Hello World!
Hello World2!

Spring Boot Batch のデフォルトの挙動としてアプリケーションの main() が実行された時点ですべてのジョブを実行するので、その実行の後にこのテストケースでの各 Tasklet が実行されてしまう。 やりたいのは各 Tasklet の実行だけであり、アプリケーション起動時の全ジョブの実行は不要だ。 この実行を無効化するには application.yaml に以下のように書く:

spring:
  batch:
    job:
      enabled: false  # main() での全ジョブ実行を行わない

再度ユニットテストを実行し Hello World! が 2 重に表示されないことを確認する。

JAR から実行するジョブを分けたい

このアプリを JAR ファイルにする。 IntelliJ IDEA のサイドメニューの Gradle から Tasks -> build -> bootJar を選択すると JAR ファイルがビルドされプロジェクトの build/libs 下に置かれる。 これをコマンドラインから job1() だけ実行したい場合は以下のように呼び出す:

java -jar (JAR ファイルパス) --spring.batch.job.names=job1 --spring.batch.job.enabled=true

コマンドライン引数で application.yaml の設定を一時的に上書きすることができるので、もし先ほどの設定で spring.batch.job.enabled=false が指定されている場合は JAR ファイルを実行してもバッチが実行されないので spring.batch.job.enabled=true を明示的に渡すことで実行できる。

全 Kotlin 化されたコジごみカレンダー v3.0 公開に書いた通り 1 年半前にコジごみカレンダーを Java から Kotlin に全面刷新したのだが、コジ時計の方はずっと Android Studio でなく Eclipse ベースのコードのままで少し変更しようにもかなりの労力を傾けて修正作業をしなければならず、どうしても手が進まない状況になってしまっていた。 そこで今回コジ時計の方も少しずつ古いコードを見ながら Kotlin で移植するという作業を進めており、今日やっとアプリ内に表示している広告の承認が下りたので Google Play に公開することができた。

1 年半前と違って Kotlin を業務で使用するのにも慣れている状態になっているので、古い Java コードを移植すると機能によっては半分以下のステップ数になった。 Kotlin の記述力はとにかく強力で、もう Java に戻りたいとは全く思わないほどだ。

また、デザインのトレンドもマテリアルデザイン (フラットデザイン) に完全に移っているので、古いグラデーションを多用するようなデザインは排除し、マテリアルデザインを意識した「単色基調でのっぺりとしたデザイン」に調整した。

フェードインやフェードアウト

点滅の前に一番よくあるアニメーションであるフェードインやフェードアウトについて言及しておく。 Android 2.x 系の時は Animation クラスを使用した古いやり方をしていたと思うが Android 4.x 以上では ObjectAnimator を使う:

// ビューの alpha 値を 1 から 0 に変化させるアニメーション
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationEnd(animation: Animator?) {
            view.visibility = View.GONE  // アニメーション終了時にビュー自体を非表示にする
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    duration = 1000  // 1,000 ミリ秒で実行
    start()
}

この ObjectAnimator.ofFloat() を使うと任意の時間内に任意のプロパティを滑らかに変更させることができるので、ほとんどの場面はこれでいけるはずだ。 この例ではフェードアウトさせるアニメーションなのだが、フェードアウトさせた後にビュー自体の存在判定を消さないと当たり判定として残り続けてしまう (alpha = 0 では透明度が 0 になっているだけでタップした際のイベントは走ってしまう)。 その為アニメーション終了時に visibility に対し GONE をセットしている。 これ自体は Android 2.x 時代からの使い古された手法だ。

点滅

ビューを点滅 (blink) させるというのは marquee と共に古いホームページの技法のように思えるが、今回実装する機会があったのでメモ。 数百ミリ秒ごとに visibility を切り替えてもいいのだが、それだとチカチカとした感じの古いタイプの点滅になってしまう。 フェードイン・フェードアウトを繰り返すタイプの点滅にしたい。

ObjectAnimator に逆方向にリピートする為の設定があったので、これを使えば短いコード量で実現できた:

ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0.0f).apply {
    repeatCount = ObjectAnimator.INFINITE  // 無限に繰り返す
    repeatMode = ObjectAnimator.REVERSE  // 逆方向に繰り返す
    duration = 500
    start()
}

repeatModeObjectAnimator.RESTART という定数も入れることができるが、こちらだとただ単に 1.0 から 0.0 へのアニメーションを繰り返すような動きになる。 ObjectAnimator.REVERSE にすると alpha に対し 1.0 から 0.0 へのアニメーションが終わった後 0.0 から 1.0 へ再度アニメーション、その後そのパターンが永遠に繰り返されることになる。

Spring Boot は Pleiades 適用済の IntelliJ IDEA でビルドするものとする。

IntelliJ IDEA で「新規 -> プロジェクト」を選択。 プロジェクトの種類は Spring Initializr (typo ではない) を選択。 プロジェクト JDK は今のところ 1.8 で行ったほうがいいようだ。 まだリリースされたばかりの Java 9 だと問題が出ることが多かった。 グループ、成果物は適当に入力。 型として Gradle Project を選択 (Maven でやりたい人は Maven Project を選択)、言語は Kotlin を選択。 パッケージングはこの時点では Jar を選択しておく。 依存関係だがとりあえずは以下の 3 点を入れておけば良い:

  • Core -> DevTools
  • Web -> Web
  • Template Engines -> Thymeleaf

尚、この画面の上部で Spring Boot のバージョンを選択できるのだが 1.5.7 ではなく 2.0.0.BUILD-SNAPSHOT を選択しておくのが良い。 Spring のバージョンが 5.0.x となり Kotlin に対応している。

プロジェクトを作成すると何やら Gradle の設定画面が出るがそのまま「次へ」を選択。 Gradle の依存関係ライブラリの取得が始まるので終わるまで待つ。 終わったらサンプルページを表示するためのコントローラとテンプレートを用意する。 Kotlin ディレクトリのどこかのディレクトリに以下のように SampleController を用意する:

@Controller
class SampleController {
    
    @GetMapping("/")
    fun sample(model: Model): String = "sample"
}

そして resources/templates 下に以下のように sample.html を用意する:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
    <p>Hello World!!</p>
</body>
</html>

この状態で TestApplication.kt を右クリックして実行すると組み込みの Tomcat が起動して http://localhost:8080/ にアクセスすると Hello World!! が表示される。 尚、この時点だとテンプレートファイルを書き換えても TestApplication.kt を実行し直さないとブラウザ上の HTML が書きかわらない。 ホットデプロイに関しては過去記事 Spring Boot + IntelliJ IDEA でホットデプロイを参考に設定してほしい。

昔対応した問題だったのだが、最近再度同じようなアプリを作成した時に忘れていてハマったので備忘を残しておく。

Google Maps Android API v2 で長時間同じ地図画面で待ち受けるようなアプリを作成する場合、地図を長時間触っていると StackOverflowError で落ちる場合がある。 これが地図を拡大したりちょっとスクロールした際に突然落ちるのでプロダクションでこれが起きてしまうと「このアプリは地図をちょっと触っているだけで落ちる」という散々な評価をいただくことになるし、長時間触っていないと再現しないという関係上開発時に気づかないことが多い。 例えば以下のようなスタックトレースが吐かれる:

java.lang.StackOverflowError: at com.google.maps.api.android.lib6.gmm6.util.e.b(:com.google.android.gms.DynamiteModulesB@11746036:22)
at com.google.maps.api.android.lib6.gmm6.store.cache.s.a(:com.google.android.gms.DynamiteModulesB@11746036:8)
at com.google.maps.api.android.lib6.gmm6.store.n.a(:com.google.android.gms.DynamiteModulesB@11746036:49)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:80)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:70)
...

これは Google Maps Android API v2 の「インドア表示」で起こる。 ほとんどの要件ではインドア表示など不要だと思われるので、以下のように設定で OFF にすれば起きなくなるので地図を使用する画面では機械的に以下を入れてしまっていいのではないかと思う:

mapFragment.getMapAsync { it.isIndoorEnabled = false }  // インドア表示 OFF
mapFragment.getMapAsync { it.uiSettings.isTiltGesturesEnabled = false }  // チルト操作 (傾けて地図を 3D 表示) OFF

2 番目の「チルト操作 OFF」も可能であれば設定した方がいい。 確かこれも ON にしていると OutOfMemoryError で落ちることがあった気がした (確証とれず)。

ちなみに OutOfMemoryError で落ちる件に関しては Stack Overflow に書かれている通り AndroidManifest.xmllargeHeap = true を宣言するなり適切なタイミングで System.gc()GoogleMap.clear() を呼ぶ必要が出てくるだろう。

面倒なのでできれば相手をしたくないが

さて、前回Service のプロセスがアプリと同一の場合、つまり通常の場合に Binder を用いて双方向でやり取りする方法に関して考察したが、今回は止むを得ない事情でプロセスを分けなければならなくなった場合に Messenger を用いてプロセス間通信を行う為の実装例に関して記載する。

Service 側の実装に関してコード例を示す:

companion object {

    /** クライアント側からの replyTo 受信. */
    val MESSAGE_RECEIVING_REPLY_TO = 1

    /** クライアントへのメッセージ送信. */
    val MESSAGE_SEND = 2
}

/** サービス側の処理を行うハンドラ. */
private class MyHandler : Handler() {

    /** クライアントの Messenger. */
    var clientMessenger: Messenger? = null

    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MESSAGE_RECEIVING_REPLY_TO -> clientMessenger = msg.replyTo
            else -> super.handleMessage(msg)
        }
    }
}

/** ハンドラ. */
private val mHandler = MyHandler()

/** サービス側のメッセンジャ. */
private val mMessenger = Messenger(mHandler)

override fun onBind(intent: Intent?): IBinder = mMessenger.binder

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // サービス実行から 5 秒後にコールバック実行
    Handler().postDelayed({
        val bundle = Bundle().apply { putString("message", "ほげふが") }
        mHandler.clientMessenger?.send(Message.obtain(null, MESSAGE_SEND, bundle))
    }, 5000)
    return Service.START_STICKY_COMPATIBILITY
}

こちらでもプロセスが同一の場合と同様に onBind()IBinder を返却しているが、こちらは Messenger に対応する Binder である (後で Messenger に復元可能な Binder) ところが異なる。 mMessenger というフィールドを持っているが、これは Handler インスタンスを Messenger クラスでラップしたものである。 Handler クラスの方を拡張することによって相手から投げられたメッセージをどう処理するかを実装する。

上記の実装例では msg.replyTo をフィールドに持っているが、これは Service からみてどのクライアントの Messenger でメッセージを送信すれば良いのかを判別するためのものである。 つまり Service 側の任意のタイミングでクライアント側にメッセージを送信したい場合は以下の手順を踏む必要がある:

  1. Service の接続確立時にクライアント側から replyTo を教えるためのメッセージを Service 側の Messenger を使って送信
  2. Service 側の送信したいタイミングでフィールドに持っている replyTo を使用してクライアントに送信

上記コードでは onStartCommand() 内で 5 秒後に実際にクライアントに対してメッセージを送信している。

Activity / Fragment 側のコード

/** クライアント側の処理を行うハンドラ. */
private class MyHandler(val activity: MainActivity) : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MyService.MESSAGE_SEND -> activity.button.text = (msg.obj as Bundle).getString("message")
            else -> Log.w(javaClass.simpleName, "Unknown what: ${msg?.what}")
        }
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mServiceMessenger = Messenger(binder)

        // サービス側に replyTo を介してクライアント側の Messenger を教える
        val message = Message.obtain(null, MyService.MESSAGE_RECEIVING_REPLY_TO)
        message.replyTo = mClientMessenger
        mServiceMessenger?.send(message)
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mServiceMessenger = null
    }
}

/** サービス側の Messenger. */
private var mServiceMessenger: Messenger? = null

/** クライアント側の Messenger. */
private val mClientMessenger = Messenger(MyHandler(this))

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

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

Service との接続が確立した時に渡ってきた IBinder を逆に Messenger のコンストラクタに渡すことで Service 側の Messenger を復元している。 それを使用することで Service 側にメッセージを送信することができる。 先ほど記載したように、メッセージの replyToActivity, Fragment 側の Messenger を詰めて送信しておく。

後は Service 側と同様にこちら側でも Handler クラスを拡張して Service から渡ってきたメッセージを処理する為のコードを書く。

まとめ

  1. ServicebindService() する
  2. Service とのコネクション確立時に ServiceMessenger を使用して Service に対しクライアント側の MessengerreplyTo に詰めて送信することで教える
  3. Service 側で Handler の拡張クラスの実装によって渡ってきた replyTo を格納しておく
  4. replyTo を使用して任意のタイミングでクライアントに対しメッセージ送信
  5. クライアント側の Handler の拡張クラスでの実装で UI の表示処理などを行う

手順をしっかり理解しないと間違えそうな内容なので、やはり手軽ではないと思う。

プロセスが同一の場合の例

大抵の場合アプリケーション内に Service を置く場合は必要に迫られない限りアプリケーションと同一プロセスとすると思われる。 この場合 Fragment 側から Service を操作したい場合は bindService を使用し Binder 経由で操作を行うのが一般的だ。

あまり意味のない例で恐縮であるが、例えば以下のような「現在時刻のタイムスタンプを返すサービス」を実装したものとする:

/**
 * 現在時刻を返すサービス.
 */
class MyService : Service() {

    inner class MyBinder : Binder() {
        fun getTime() = Date().time
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    override fun onBind(intent: Intent?): IBinder {
        Log.d(javaClass.simpleName, "onBind start.")
        return mBinder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(javaClass.simpleName, "onUnbind start.")
        return super.onUnbind(intent)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(javaClass.simpleName, "onStartCommand start.")

        // TODO 本当は何か UI に関係ない部分で待機するような処理が入るはず

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(javaClass.simpleName, "onCreate start.")
    }

    override fun onDestroy() {
        Log.d(javaClass.simpleName, "onDestroy start.")
        super.onDestroy()
    }
}

上記の場合 onBind() でサービス内で定義されている MyBinder を返却しているが、クライアント (Fragment) 側からこのインスタンスを使用してサービスの機能を利用する。 よって、例えば「ボタンを押したらサービスからデータを取ってきて画面に表示する」といった実装を行う場合は以下のようなコードになる:

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

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

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    super.onDestroy()
}

この例だと bindService() しかしていないので Service 側としては onCreate()onBind() は呼ばれるが onStartCommand() はコールされないことに注意する。 onStartCommand() はあくまで startService() した時に呼ばれる処理となっている。

ServiceConnection.onServiceConnected() 内で Service 側で定義したクラスである MyService.MyBinder にキャストしているが、これが可能なためにこの MyService.MyBinder に対し自由にメソッドやフィールドを追加することにより Service に対して好きに処理を行うことができるようになる。

Service から任意のタイミングでクライアントに処理を行わせたい場合

上記の Binder の例はあくまでクライアント側から任意のタイミングで Service に対し処理を行わせたい場合に使用できる。 逆方向である Service からクライアントに対し処理を行わせる場合であるが、以下の 3 通りが考えられる:

  • Binder を介してクライアント側からコールバックを渡す
  • EventBusRxJava などのライブラリを使用して任意のイベントを飛ばす
  • BroadcastReceiver をクライアント側に実装して Service からの sendBroadcast() を受け取る

今は EventBus などのライブラリを使用すると任意の位置からイベントを伝搬できるのでとても便利だ。 ここでは 1 番目の Binder を用いて実現する方法に関して書く。

例えばサービスが起動された 5 秒後にクライアント側で実装されたコールバックを実行する例を示す。 サービス側の実装として、まずクライアント側で実装すべきコールバックのインターフェースを開示する:

class MyService : Service() {

    /** サービスで 5 秒後に実行されるコールバック. */
    interface Callback {
        fun doIt()
    }

    inner class MyBinder : Binder() {
        fun getTime() = Date().time

        // クライアント側からコールバックを渡す為のメソッドを追加
        fun set(callback: Callback?) {
            mCallback = callback
        }
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    /** クライアント側で実装されたコールバック */
    private var mCallback: Callback? = null

    override fun onBind(intent: Intent?): IBinder = mBinder

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // サービス実行から 5 秒後にコールバック実行
        Handler().postDelayed({
            mCallback?.doIt()
        }, 5000)
        return Service.START_STICKY_COMPATIBILITY
    }
}

後は以下のようにクライアント側でコールバックの実装を行い、サービスのコネクションが張れたタイミングでコールバックを Binder を通してサービス側に渡してやれば良い:

private val mCallback = object : Callback {
    override fun doIt() {
        button.text = "サービス側から呼ばれた"
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
        mBinder?.set(mCallback)  // サービス側に定義したコールバックを渡す
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder?.set(null)
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

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

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

これで 5 秒後にボタンに「サービス側から呼ばれた」という文字列が表示される。

プロセスを分けるとどうなるか

さて Android の場合同一アプリ内であっても別のプロセスを使用した Service を定義することができる。 これは AndroidManitest.xml に以下のように定義を加えることにより実現できる:

<service
    android:name=".MyService"
    android:process=":remote"/>

android:process=":remote" の部分であるが、:remote の箇所は別に :hoge などの他の名前でも良い。 このようにして先ほどのコードを実行してみると ServiceConnection.onServiceConnected()MyService.MyBinder にキャストしている箇所で ClassCastException がスローされるようになる:

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder  // 駄目. binder は BinderProxy が渡ってくる!!
    }
}

デバッグ実行で見てみると分かるが Service 側で渡した Binder の参照がそのまま渡ってくるのではなく BinderProxy というプロキシクラスのインスタンスが渡ってくるようになる。 その為このようにキャスト前提の実装にしてしまうと失敗する。 また EventBus などのライブラリもプロセス間通信には使用することができない。

この場合は公式にある通り Messenger を使用するわけだが、長くなったのでそのコード例は次回とする。

Android Studio に warning を出されるようになった

Kotlin で以下のような構文を書くと Android Studio に warning を出されるようになった:

if (value?.isEmpty() ?: false)  // Equality check can be used instead of elvis
if (value?.isEmpty() == true)  // これなら OK

この場合は elvis 演算子 ?: ではなく == で代用できるというわけである。 何故私が上の elvis 演算子 を使った構文を使ってしまっていたかというと、紛れもなく Java では false は基本型 boolean であるという事実が頭にあったからであり Java の概念に引きずられて if (value?.isEmpty() == true) の方の例が些か直感的でないように感じてしまったからだ。 以下どういうことなのか考察する。

Java では

Java では以下のような比較は syntax error となる:

if (null == false)  // syntax error. false はオブジェクトではない!!
if (null == Boolean.FALSE)  // これなら OK

Java は歴史的な経緯にて全てのフィールドやメソッドがオブジェクトというわけではなく基本型 (プリミティブ型) とオブジェクト型に分類される。 そもそも基本型が null になることはなく基本型に対する == 比較は基本型且つ型が一致していないと行えない。

基本型はオブジェクトではないので基本型に対するメソッド呼び出しを行うことはできない。 メソッド呼び出しを行いたい場合は対応するラッパー型 (オブジェクト) に変換した上でメソッド呼び出しを行う。 上記の例の場合 boolean のラッパー型は Boolean なので false に対しメソッド呼び出しを行いたいならば Boolean.FALSE を使えば良い。

Boolean.TRUEBoolean.FALSE はそれぞれ java.lang.Boolean クラスに対するインスタンスである。 一方 Java における == 演算子の役割はオブジェクトの参照が等しいことの比較なので以下のような null との比較は false となる:

System.out.println(null == Boolean.TRUE);  // false
System.out.println(null == Boolean.FALSE);  // false

一方 Boolean.equals() の実装も「比較対象が Boolean クラスでない場合 (null も含む) 常に false となる」となっているので以下も合点がいく:

System.out.println(Boolean.TRUE.equals(null));  // false
System.out.println(Boolean.FALSE.equals(null));  // false

まとめると Java では nulltrue でも false でもない第三の値である。 一方以下のように nullnull の同値比較は true となる事に注意する:

System.out.println(null == null);  // true

Kotlin に話を戻す

話を戻すが Kotlin では以下だとコンパイルが通らない:

if (value?.isEmpty())  // 駄目. value が null の場合戻り値が null となるので評価値が Boolean? となる

Kotlin は null-safety な言語なので上記のような書き方をすると valuenull だった場合に NullPointerException がスローされずに isEmpty() メソッド呼び出しが行われないという挙動になる。 よって null の場合を ?: で付け加えるのは自然に見える。

以下再掲する:

if (value?.isEmpty() == true)  // なんとなく null == true がマズそうに見える

Kotlin では全てのローカル変数はオブジェクトの為 null == true の評価が可能である。 しかし ==null オブジェクトに対する equals() のシンタックスシュガーに見えるので何となく null に対するメソッド呼び出しに失敗しそうに見えるのだが、Kotlin 公式に以下のように記載があった:

Structural equality is checked by the == operation (and its negated counterpart !=). By convention, an expression like a == b is translated to a?.equals(b) ?: (b === null)

つまり Kotlin においては前述の if 文は以下と等価である:

if (value?.isEmpty()?.equals(true) ?: (true === null))

これは比較対象の左辺が null だった場合左辺と右辺が共に null の時のみ true となる、と読み解ける。 つまり Java における「nulltrue とも false とも一致しないし null == null = true である」という原則を生かしつつ、常にそのまま第三の値として == で比較できるように言語の文法レベルで配慮が効いているということだろう。

素の AsyncTask は冗長過ぎる

例えば「重い処理をワーカースレッドで行い戻ってきた文字列を Toast で表示する」といった処理を AsyncTask で共通化せずに無名クラスで実装すると以下のようになる:

object : AsyncTask<Unit, Unit, String>() {
    override fun doInBackground(vararg p0: Unit?): String {
        Thread.sleep(3000)
        return "めっせーじ"
    }

    override fun onPostExecute(result: String?) {
        Toast.makeText(context, result, Toast.LENGTH_LONG).show()
    }
}.execute()

Kotlin かつちょっとした処理を書こうとしているだけなのにえらく冗長なコードになっている。 この問題点は AsyncTask が 3 つも raw type (ジェネリクスの型パラメーター) を要求する抽象クラスになっていることに加え、Kotlin の場合無名クラスを定義するのに object : (継承元クラス) といった割と仰々しい構文を使わなければならないことだ。

このような無名クラスを使うのではなく、Kotlin のシンプルなラムダ式を渡せばいいようにしたい。

SimpleAsyncTask

以下、内部的には AsyncTask を使用するが各種メソッドの override が必要な時のみラムダ式を渡せばいいように実装してみた:

class SimpleAsyncTask<R>(private val background: () -> R,
        private val pre: () -> Unit = {}, private val post: (R) -> Unit = {},
        private val cancelled: () -> Unit = {}) : AsyncTask<Unit, Unit, R>() {
    override fun onPreExecute() = pre()
    override fun doInBackground(vararg p0: Unit?): R = background()
    override fun onPostExecute(result: R) = post(result)
    override fun onCancelled() = cancelled()
}

AsyncTask が持つ経過表示を行うための機能 onProgressUpdate() はほとんど使用しないのでバッサリ切った。 また、パラメータを AsyncTask().execute(param) のように渡せる機能も共通化しないのであれば必要ないので、今回即時実行するケースのみにターゲットを絞り削った。 これで以下のように使える:

// 一番最初の AsyncTask の例と等価のコード
SimpleAsyncTask(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() }).execute()

あとすこし改善

かなりシンプルにはなったが AsyncTaskexecute() をコールし忘れて発火されないという凡ミスがよくある。 どうせパラメータを渡さないのだから execute() を書く必要はない。 そこで、以下の関数をトップレベルに配置する:

/**
 * SimpleAsyncTask を使用し非同期で実行する.
 *
 * @param R 非同期実行スレッドの戻り型
 * @param background 非同期実行処理
 * @param pre 非同期実行前処理 (未セットならば処理を行わない)
 * @param post 非同期実行後処理 (未セットならば処理を行わない)
 * @param cancelled 非同期実行キャンセル時処理 (未セットならば処理を行わない)
 */
fun <R> doAsync(background: () -> R, pre: () -> Unit = {}, post: (R) -> Unit = {}, cancelled: () -> Unit = {})
    = SimpleAsyncTask<R>(background, pre, post, cancelled).execute()

これで以下のように書ける:

doAsync(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() })

Kotlin の型推論が強力なおかげで <R> を明示する必要はない。 とてもシンプルで素晴らしい。

onSaveInstanceState 後に表示しようとするとクラッシュする DialogFragment

DialogFragment は例えば以下のように表示できる:

// MainActivity 内のコード
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    MyDialogFragment().show(supportFragmentManager, null)  // 表示処理
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

通常はこれで問題ないのだが、例えば以下のようにして画面を開いた直後に「スリープ状態にする」「画面を閉じる」など行うとクラッシュする:

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

    // 5 秒待ってから表示. この間にスリープやバックグラウンドに回すとクラッシュ!!
    Handler().postDelayed({ MyDialogFragment().show(supportFragmentManager, null) }, 5000)
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

上記のように数秒待ってからダイアログ表示することなどそうそうないと思われるかもしれないが、昨今は通信を伴ったアプリの開発が当たり前であり通信の結果やエラー表示をダイアログで行っていたりするとよくこのエラーを目にすることとなる。

クラッシュの内容は java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState といった感じで恐らく Fragment を使っている人であればよく見ているのではないかと思う。 要するに Activity や Fragment の onPause() から onResume() 間には Fragment (DialogFragment) を操作できないというわけであるが、これの回避が思ったより難しい。

onPause() から onResume() 間に操作しようとしている DialogFragment の show()dismiss()ouResume() の後に pending するという方法でうまく回避できたのでここに記しておく。

前提

今回は前提として私が Qiita に以前書いた DialogFragment の共通化処理を入れているものとする。 例えば以下のような MyDialogFragment が用意されているものとする:

class MyDialogFragment : AppCompatDialogFragment() {

    /** コールバック */
    private val mCallback: Callback by lazy {
        if (targetFragment is Callback) {
            targetFragment as Callback
        } else if (activity is Callback) {
            activity as Callback
        } else {
            throw UnsupportedOperationException()
        }
    }

    /** リクエストコード */
    private val mRequestCode: Int
        get() = if (arguments.containsKey("request_code")) arguments.getInt("request_code") else targetRequestCode

    /**
     * MyDialog で何か処理が起こった場合にコールバックされるリスナ.
     */
    interface Callback {

        /**
         * MyDialog で positiveButton, NegativeButton, リスト選択など行われた際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param resultCode DialogInterface.BUTTON_(POSI|NEGA)TIVE 若しくはリストの position
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogSucceeded(requestCode: Int, resultCode: Int, params: Bundle) = Unit

        /**
         * MyDialog がキャンセルされた時に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogCancelled(requestCode: Int, params: Bundle) = Unit

        /**
         * MyDialog で multiChoice した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param position チェック位置
         * @param checked チェックされたか否か
         */
        fun onMyDialogMultiChoiceClicked(requestCode: Int, position: Int, checked: Boolean) = Unit

        /**
         * MyDialog で dateSet した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param year 年
         * @param month 月
         * @param date 日
         */
        fun onMyDialogDateSet(requestCode: Int, year: Int, month: Int, date: Int) = Unit
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = DialogInterface.OnClickListener { _, which ->
            dismiss()
            mCallback.onMyDialogSucceeded(mRequestCode, which, arguments.getBundle("params"))
        }
        val isProgress = arguments.getBoolean("is_progress")
        val title = arguments.getString("title")
        val message = arguments.getString("message")
        val positive = arguments.getString("positive")
        val negative = arguments.getString("negative")
        val items = arguments.getStringArray("items")
        val multiChoiceItems = arguments.getStringArray("multi_choice_items")
        val multiChoiceItemValues = arguments.getBooleanArray("multi_choice_item_values")
        val date = arguments.getSerializable("date") as Date?
        isCancelable = arguments.getBoolean("cancelable")

        if (isProgress) {
            return ProgressDialog(activity).apply { message.let { setMessage(it) } }
        } else if (date != null) {
            val cal = Calendar.getInstance(Locale.getDefault())
            cal.time = date
            val picker = DatePickerDialog(activity, { _, year, month, date ->
                mCallback.onMyDialogDateSet(mRequestCode, year, month, date)
            }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE))
            title?.let { picker.setTitle(it) }
            message?.let { picker.setMessage(it) }
            return picker
        } else {
            val builder = AlertDialog.Builder(activity)
            title?.let { builder.setTitle(it) }
            message?.let { builder.setMessage(it) }
            positive?.let { builder.setPositiveButton(positive, listener) }
            negative?.let { builder.setNegativeButton(negative, listener) }
            items?.let { builder.setItems(items, listener) }
            multiChoiceItems?.let {
                builder.setMultiChoiceItems(multiChoiceItems, multiChoiceItemValues) {
                    _, position, checked ->
                    mCallback.onMyDialogMultiChoiceClicked(mRequestCode, position, checked)
                }
            }
            return builder.create()
        }
    }

    override fun onCancel(dialog: DialogInterface?) {
        mCallback.onMyDialogCancelled(mRequestCode, arguments.getBundle("params") ?: Bundle())
    }
}

通常のタイトルやメッセージを持ったダイアログ表示に加えて、複数のアイテムから 1 つを選択するものや複数チェックできるもの、あと DatePickerDialog での日付選択や ProgressDialog での経過表示も対応している。 これを呼ぶ場合用にもショートカット用のメソッドを以下のように用意する:

/**
 * MyDialogFragment を表示する.
 *
 * @param requestCode リスクエストコード
 * @param params Bundle パラメータ
 * @param title タイトル
 * @param message メッセージ
 * @param positive positiveButton ラベル
 * @param negative negativeButton ラベル
 * @param items シングルタップ選択用の items
 * @param multiChoiceItems 複数選択用の items
 * @param multiChoiceItemValues 複数選択用の items (値)
 * @param date DatePicker 用の初期値
 * @param tag Tag
 * @param cancelable キャンセル出来るか否か
 */
fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
        message: Any? = null, positive: Any? = null, negative: Any? = null,
        items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
        date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
    fun getStringArray(x: Any): Array<String> = when (x) {
        is Int -> resources.getStringArray(x)
        is ArrayList<*> -> x.toArray(arrayOfNulls(0))
        is Array<*> -> x.map(Any?::toString).toTypedArray()
        else -> throw IllegalArgumentException()
    }
    if (this !is MyDialogFragment.Callback) {
        throw UnsupportedOperationException("Using MyDialogFragment must implement Callback to Fragment")
    }
    val args = Bundle()
    title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
    message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
    items?.let { args.putStringArray("items", getStringArray(it)) }
    multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
    multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
    date?.let { args.putSerializable("date", it) }
    positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
    negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
    args.putBoolean("cancelable", cancelable)
    args.putBundle("params", params)

    MyDialogFragment().apply { arguments = args }.show(supportFragmentManager, tag)
}

ちょっと複雑そうに見えるのは、各種引数をリソース ID と文字列の双方で渡せるように考慮を入れているからである (その為多くの型を Any? にしている)。 このメソッドを使用して前述の MyDialogFragment を show() するには以下のように書く:

// 5 秒待ってから表示
Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)

この時点では onPause() 後の pending の対応が入っていないため先ほどと同じようにスリープにしたり画面を閉じたりすると IllegalStateException がスローされる。

対処

この対処を行う上で残念なのが結局 Fragment を拡張するようなパターンが必要 (BaseFragment のようなもの) になってしまうということだ。 どうしても onResume() と密に結びついてしまうので仕方がない。 これを避けようとすると作成した Activity や Fragment の onResume() の前に毎回お決まりのコードを入れなければならないので若干辛いものがある。

abstract class MyFragment : Fragment(), MyDialogFragment.Callback {

    /** 復帰のために貯めておく PTDialogFragment の表示データ */
    private val mDialogData: MutableList<Bundle> = mutableListOf()

    /** 復帰のために貯めておく dismissDialog のデータ */
    private val mDismissingTags: MutableList<String> = mutableListOf()

    /**
     * ProgressDialog を dismiss する.
     *
     * @param tag Tag
     */
    fun dismissDialog(tag: String = "progress") {
        if (isResumed) {
            fragmentManager.findFragmentByTag(tag)?.let {
                if (it is AppCompatDialogFragment) {
                    it.dismiss()
                } else {
                    throw UnsupportedOperationException()  // DialogFragment ではない
                }
            }
        } else {
            mDismissingTags += tag  // resume でない場合は pending する
        }
    }

    /**
     * ProgressDialog を表示する.
     *
     * @param message メッセージ
     * @param tag Tag
     * @param cancelable キャンセルできるか否か
     */
    fun showProgressDialog(message: Any? = null, tag: String = "progress", cancelable: Boolean = true) {
        val args = Bundle()
        args.putBoolean("is_progress", true)
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, 0)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    /**
     * PTDialogFragment を表示する.
     *
     * @param requestCode リスクエストコード
     * @param params Bundle パラメータ
     * @param title タイトル
     * @param message メッセージ
     * @param positive positiveButton ラベル
     * @param negative negativeButton ラベル
     * @param items シングルタップ選択用の items
     * @param multiChoiceItems 複数選択用の items
     * @param multiChoiceItemValues 複数選択用の items (値)
     * @param date DatePicker 用の初期値
     * @param tag Tag
     * @param cancelable キャンセル出来るか否か
     */
    fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
            message: Any? = null, positive: Any? = null, negative: Any? = null,
            items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
            date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
        fun getStringArray(x: Any): Array<String> = when (x) {
            is Int -> context.resources.getStringArray(x)
            is ArrayList<*> -> x.toArray(arrayOfNulls(0))
            is Array<*> -> x.map(Any?::toString).toTypedArray()
            else -> throw IllegalArgumentException()
        }
        if (this !is MyDialogFragment.Callback) {
            throw UnsupportedOperationException("Using PTDialogFragment must implement Callback to Fragment")
        }

        // 既に detach されている場合は何もしない
        if (isDetached) return

        val args = Bundle()
        title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        items?.let { args.putStringArray("items", getStringArray(it)) }
        multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
        multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
        date?.let { args.putSerializable("date", it) }
        positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
        negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)
        args.putBundle("params", params)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, requestCode)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putInt("request_code", requestCode)
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    override fun onResume() {
        super.onResume()
        mDialogData.forEach {
            with(MyDialogFragment()) {
                arguments = it
                setTargetFragment(this@MyFragment, it.getInt("request_code"))
                show(this@MyFragment.fragmentManager, it.getString("tag"))
            }
        }
        mDialogData.clear()
        mDismissingTags.forEach { dismissDialog(it) }
        mDismissingTags.clear()
    }
}

こんな MyFragment を用意する。 そうして以下のように MyFragment を継承した Fragment 上で MyDialogFragment を表示してみる:

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

    savedInstanceState ?: supportFragmentManager.beginTransaction()
            .add(android.R.id.content, AFragment())
            .commit()
}

class AFragment : MyFragment() {
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        
        // 5 秒後表示. 今度はスリープしてもクラッシュせずに復帰時にちゃんと Dialog が表示される!!
        Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)
    }
}

今度はクラッシュしないし、画面復帰時に正しく表示しようとしていた Dialog が表示される。 肝は Dialog を表示しようとする時に isResumed でなかったら tag で表示しようとしている Dialog の情報を保存し onResume() 後に改めて表示するというところだ。 これを共通ダイアログに対して行っておくとこのようなボイラープレートコードをあちこちに仕込まなくて済むようになるので有用である。

最近会社での Android アプリの実装にも Kotlin を使いだしてというもの、記述力がとても高いので気にいってはいる。 特にエクステンションを用いた既存クラスの拡張はとても柔軟性が高く Java だと全てユーティリティメソッドに切っていたものがほとんどエクステンションになり明らかに記述量が減った。 後はトップレベルに関数を定義でき、どこからでもその参照をインポートすれば利用できるのも便利だ。 私はよく使う Bundle を簡単に作成できるような関数をトップレベルに定義して使用している。

ただ、正直もう少し何とかなったのではないか、若しくは Java よりも記述量が増えてしまって微妙に思えるところもあるのも事実だった。 以下 Kotlin を書く方ならご存知の事だと思うが列挙する。

Java のクラスインスタンスを取得する記法

Java のクラスインスタンス、いわゆる Class<T> を取得する記法は Java だと以下のような感じだ:

Hoge.class  // static に取得する場合
hoge.getClass()  // オブジェクトから取得する場合

一方 Kotlin は以下だ:

Hoge::class.java
hoge::class.java

正直見にくい。 そして Android アプリ開発では結構出てくる。

Intent(context, HogeActivity::class.java)

みたいな感じである。 ただ、これを長いから短く書けるようなショートカットを用意すればいいではないか、と思ってやってみたができない。 Kotlin のジェネリクスの型情報も Java と同じくコンパイル後に削除され普通の Object としてしか認識できないので T を受け取って Class<T> を吐くようなメソッドは定義できない (多分)。

配列初期化の記法

Java だと以下のような感じで、変数初期化の際に代入する場合に限り配列のリテラル表記が使用できる:

final String[] arrays = {"hoge", "fuga"};

だが Kotlin には配列のリテラル表記はなく { ... } の役割は代わりにラムダ式 (関数オブジェクト) で使用するようになっている。 Kotlin では arrayOf()intArrayOf() などといった関数を使用する:

val arrays = arrayOf("hoge", "fuga")
val intArrays = intArrayOf(1, 2, 3)

この intArrayOf()booleanArrayOf() などといった感じに Java のプリミティブ型に対応する配列を生成するようのメソッドが別々に用意されているあたりに苦しさを感じる。 とはいえ Java との相互運用性を考えると int[]Integer[] は区別できなければならず、内部的にこうなってしまうのは仕方がない部分かもしれない。

日本固有の悩みだろうか

Android で Google Map を配置したアプリを作成する場合は Google Maps Android API を使用することになると思うが、現状 Google Map 内に表示される文字サイズを変更するような API は用意されていないし Android 端末の設定の文字サイズを変更してもアプリ内の文字サイズは変わる (但しアプリ側が行儀よく sp 単位でフォントサイズ指定されている場合) のだが、Google Map 内の文字サイズは残念ながら連動しない。

老眼、つまり高齢化は先進国における世界的な流れだと思うのだが、何故 Google Maps Android API にそのような考慮がなされていないのか。 恐らく欧米はラテン文字を使用しているためフォントサイズが比較的小さくても視認性が高くそういった問題は余り出てこないのではないだろうか。 その点漢字は不利である。

高齢者向けのアプリを開発する場合どうしても「文字サイズを大きくする」というところを考慮しなければならない。 API が用意されていないので、逆に地図自体を大きくするのはどうかと考えた。 少しボヤケた感じにはなるが、意外と悪くないアプローチなのでここに書き残しておく。

まず地図自体を大きくする

SupportMapFragment のインスタンスを取得しそれに紐づく View に対し setScaleXsetScaleY を呼べば簡単に変更できる。 例えば 2 倍にする場合は以下でよい:

mMapFragment.view?.scaleX = 2.0f
mMapFragment.view?.scaleY = 2.0f

だがこれを愚直に行うと以下の問題が発生する:

  • Google ロゴや現在位置ボタン、ズームイン・アウトボタンが画面の領域外に見切れてしまう
  • ボタン類や配置したオブジェクト (ピン、吹き出しなど) も 2 倍になってしまう

ロゴやボタンが見切れないようにする

幸い GoogleMap#setPadding というメソッドで Google Map に対しパディングを設定できる。 パディングは端末の縦横ピクセルを取得し適切な値を設定しなければならない。 Android アプリ開発ではよくあるパターンではあるが View のサイズ取得はビューツリー構築後でないと計算されておらず 0 になってしまうところに注意する。 このように ViewTreeObserver#addOnGlobalLayoutListener を使うのがイディオムだ:

mMapFragment.view?.viewTreeObserver?.addOnGlobalLayoutListener {
    view?.let { view ->
        mMapFragment.getMapAsync { map ->
            // 2 倍だけでなく 1.5 倍などもサポートする場合もう少し工夫する必要あり
            val (width, height) = view.width to view.height
            val paddingLeft = (width / 4).toInt()
            val paddingTop = (height / 4).toInt()
            map.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop)
        }
    }
}

2 倍になってしまうボタンを使わずに自作する

2 倍になってしまう Google ロゴは仕方ないとして、ボタン類が 2 倍になっているのはどうにも不格好だ。 まずデフォルトのボタン類を以下で非表示にする:

// map は GoogleMap インスタンス (getMapAsync() での取得後)
map.uiSettings.isZoomControlsEnabled = false
map.uiSettings.isMyLocationButtonEnabled = false

続いて自作のアイコンを配置するわけだが、アイコン画像は Google Material Icons に自由に使えるアイコンが配布されているので、これを引っ張ってくれば良い。 そして GoogleMap#getMapAsync()GoogleMap インスタンスを取得できたタイミングで以下のような感じでイベントをセットすればよい:

myLocation.setOnClickListener {
    // TODO 本当は myLocation は deprecated なので FusedLocationApi を使用したものに置き換えるのがよい
    val latLng = LatLng(map.myLocation.latitude, map.myLocation.longitude)
    map.animateCamera(CameraUpdateFactory.newLatLng(latLng))
}
zoomIn.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomIn()) }
zoomOut.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomOut()) }

Kotlin スタートブックを引っ張り出してきて Kotlin のお勉強をしていた。 とはいえ Kotlin に関しては自分が過去に一人読書会して疑問点をまとめていたので、それを読み返すだけでも大分思い出せた。 やはり重要な部分は Blog にまとめておくと後で辿るのが容易なので便利だ。

非同期での DialogFragmentshow()/dismiss() が鬼門 (IllegalArgumentException がスローされる) なので、その辺りの「キレイな対策」を考えたい。 onSavedInstanceState() 後だったら DialogFragment を処理するタイミングを onResume() 後にずらす、といった感じの実装が Kotlin のトレイト……ではなくインターフェースでキレイに実装できるかといったところだ。

Java だとどうしても BaseFragment などの基底クラスを作成するという原始的な方法しか取れず、好ましくない。 Java 8 から実装を持てるインターフェースがあるが今のところ Android の Java 実装では使えないのも痛い……。

かなり手間をかけた全面刷新

コジごみカレンダー自体は 5 年近く前に初版をリリースした歴史ある Android アプリなのだが、アプリの構造が Android 2.2 時代のもののままなので更新するにも何かと制約がありモチベーションが保てなくなっていた。 なので現代の技術で全面リプレースするという構想は実は 2, 3 年前から持っていたのだが、既にかなり複雑なアプリに仕上がってしまっていたので移植しようとすると全ての機能を網羅しなければならず、なかなか最後まで続かない結果になっていた。 作っては中断、作ってはまた中断を繰り返しており、開発は頓挫状態となっていた。

Kotlin に感謝

では何故今回モチベーションが保てたのかというと間違いなく Kotlin のお陰だ。 既存の冗長な Java コードを Kotlin で書き換えるのが非常に気持ちよく、地味な移植作業を続けるのがそれ程苦ではなくなっていた。

今回 In-app Billing の既存のコード以外全て Kotlin で書き直している。 業務で Kotlin を使うのは流石に憚られたが、自分のアプリなら自由に使えるのでなかなか楽しい。

ただ 「Android 開発経験がそれほどない技術者は素直に Java で書いたほうがいい」と思った。 所々 Kotlin の文法ミスなのか Android SDK の API の使用方法が間違っているのか判断がつかない場面があったので、恐らく経験が浅いと解決できないのではないかと思われたからだ。

これが特に良かったという点

実際にコジごみカレンダーのコードを一部引用し述べる。

Fragment で Activity のインスタンスを使用する場合コンストラクタ等で行おうとするとまだ Activity に attach() されてないので getActivity()null になる。 これを避ける為には Fragment#onAttach() で Activity のインスタンスを参照するか Fragment#onActivityCreated()getActivity() するのが普通なのだが、そうすると以下のように退屈な代入を書かなければならない:

private ContentResolver mResolver;  // ここで代入できないから final にできない!!
...
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mResolver = getActivity().getContentResolver();
}

Kotlin なら by lazy { ... } を使えば実際に使用する箇所でコードブロックが走る (遅延実行) ので大丈夫だ:

// mResolver を onActivityCreated() 以降でしか参照しないならこれで問題ない
private val mResolver: ContentResolver by lazy { activity.contentResolver }

多数の分岐を持った変数代入を Java でやると以下だ:

// 読みやすいけど退屈
final int backgroundColor;
if (year == currentYear && month == currentMonth && date == currentDate) {
    backgroundColor = getResources().getColor(R.color.calendar_today);
} else if (targetMonth != month) {
    backgroundColor = getResources().getColor(R.color.super_light_gray);
} else if (j == 0 || dayType.isHoliday(cal) || dayType.isExtra(cal)) {
    backgroundColor = getResources().getColor(R.color.calendar_sunday);
} else if (j == 6) {
    backgroundColor = getResources().getColor(R.color.calendar_saturday);
} else {
    backgroundColor = Color.WHITE;
}
columnLayout.setBackgroundColor(backgroundColor);

Kotlin だと when 文があるのでかなり簡潔になる:

columnLayout.setBackgroundColor(when {
    year == currentYear && month == currentMonth && date == currentDate -> resources.getColor(R.color.calendar_today)
    targetMonth != month -> resources.getColor(R.color.calendar_not_target_month)
    j == 0 || Utils.isHoliday(cal) || Utils.isExtra(cal) -> resources.getColor(R.color.calendar_sunday)
    j == 6 -> resources.getColor(R.color.calendar_saturday)
    else -> Color.WHITE
})

後は拡張メソッド (エクステンション) やトップレベル関数、インライン関数やラムダ式なども勿論便利だった。 もうプライベートで行うコーディングは Java で行う気がしない。

ちょっと前までは AppCompatActivity を使用した場合に PreferenceFragment が Support Library の Fragment を継承していないのでそのまま使えず歯がゆい思いをしていたが Android Support Library の Fragment を継承した PreferenceFragmentCompat が登場しその問題が解決された。 しかし古いアプリを移植しようとして RingtonePreference (通知音選択) がそのまま使えない事に気付いた。 SwitchPreferece には SwitchPreferenceCompat が用意されているのに RingtonePreference にはそういったものがない。

仕方が無いので普通の Preference で自分で実装することにした。

呼び出し側

暗黙的インテントを投げることで同機能を実装できる:

    mNotificationSound.setOnPreferenceClickListener {
        val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI)

        val existingValue = pref().getString("setting.notificationSound", null)

        // Uri として null を指定すると通知音「なし」扱いになることに注意
        val uri: Uri? = when {
            existingValue == null -> Settings.System.DEFAULT_NOTIFICATION_URI
            existingValue.length > 0 -> Uri.parse(existingValue)
            else -> null
        }
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri)
        startActivityForResult(intent, 0)
        true
    }

「Preference の設定がまだされていない場合」「通知音がなし (サイレント) の場合」「通知音が設定されている場合」の 3 通りあることに注意。 その場合分けは上のコードの when 文に示されている。

onActivityResult() 側

暗黙的インテントの戻り値を受け取る為に onActivityResult() も実装する:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode === 0 && data != null) {
        val ringtone = data.getParcelableExtra<Parcelable?>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
        pref().edit().putString("setting.notificationSound", ringtone?.let { it.toString() } ?: "").apply()
    } else {
        super.onActivityResult(requestCode, resultCode, data)
    }
}

こちらは通知音がなしの場合は null で渡ってくるので Preference に空文字で設定しておく。 空文字で無くても null でさえなければ何でも構わない。 null だと未設定との区別がつかなくなる。

ちなみに上記 val ringtone には content://media/internal/audio/media/22 といった感じの CONTENT_URI 表現が入ってくる。 実際に Notification を鳴らす際にこのまま使うことができる。

第 10 章 インタフェース

インターフェース (ミックスイン) の説明は大体想像通りなのだが、クラスデリゲーションの仕組みだけなるほどと思った:

class GreeterWithRecording(private val greeter: Greeter) : Greeter by greeter {
    private val _targets: MutableSet<String> = mutableSetOf()
    val targets: Set<String>
        get() = _targets
    override fun sayHello(target: String) {
        _targets += target
        greeter.sayHello(target)
    }
}

上記のような例が紹介されていた。by greeter の部分がオーバライドしていないメンバは greeter に依存するという意味らしい。うまい説明ができないので詳しくは本誌参照のこと。

第 11 章 ジェネリクス

最初の理解としては大体 Java と同様なのだが、以下の記述は書きにくいのではないかと思った:

class Baz<T> where T : Hoge, T : Fuga  // T は Hoge と Fuga インターフェースを実装している

直感的には class Baz<T : Hoge, Fuga> なのだが。

Java も Kotlin もジェネリクスの実装は不変だが Kotlin のジェネリクスは共変や反変にすることもできる。 例えば A<T> のところを A<out T> (共変) や A<in T> (反変) などと記述する。

例えば Array<String>Array<CharSequence> ではない (StringCharSequence のサブタイプ) が Array<out CharSequence> だと Array<String> が含まれるし Array<in String> だと Array<CharSequence> も含まれるということ。

最後に具象型というトピックがあった。Kotlin のジェネリクスは Java と同様に実行時に型情報が消去される (Java では常に型パラメータは Object となる) が、そうではなく型情報を保持した状態で期待する動作を実現する方法があるらしい。

inline fun <reified T> Any.instanceOf(): Boolean = this is T

このような関数が紹介されており reified というキーワードが付いている。 普通の Java だと T は常に Object となるので is キーワードによる型チェックは失敗しそうに見えるが、これなら成功するらしい。

第 12 章 Null 安全

ここに書いてある内容は Swift などと同様なので新たに知ったような事は少ない。 普通こういった機構はオプショナルというが Kotlin では Nullable というようだ。

第 13 章 その他の話題

モダンな言語には大抵付いている演算子オーバーロードの話題から始まっていた。 Kotlin では operator fun ... で書くと演算子オーバーロードとなる。 あまり乱用すると訳が分からなくなるので、明らかに分かりやすくなるケースのみに使用していくのが良いかと思う。

その次は等価性の話題となっていた。 Kotlin では Java のように参照の一致を検査したい場合 === 演算子を使用する。 Kotlin では == 演算子は equals() と同等の意味となっている。 Java だと == が参照の一致の検査になっているせいで文字列の一致の比較など "str".equals(b) などといちいち書かなければならないので不便だった。

次が中置呼び出しに関する話題だった。 infix キーワードで定義するもので、例えば MyInt(1) plus MyInt(2) などという中置記法が可能となる。 ビルトインのものだと 1..10 step 3step が中置呼び出しとなっている。

オプショナル型の値が入っていたら何か処理をする

Kotlin には let という構文がある。Swift だと if-let 文が相当するし Java だと Optional#ifPresent() だ。 あるオプショナル型の変数に値が入っていたら何かする、といった具合に使用する:

val a: String? = null
a?.let { print(it) }  // let ブロック内で it で optional が外された形で使用可能
a?.let { print(it) } ?: print("nothing")  // elvis 演算子と組み合わせる事で null の場合の処理も記述可能

この a?.let が曲者な気がする。

typo するとバグの元になりそう

上記の例で以下のように間違えると意図した結果にならない:

val a: String? = null
a.let { print(it) }  // ? が無いので a が null でも print されてしまう!!
a?.let { print(it) }  // ? があるので正しく print されない

これでうっかりハマって暫く気づかなかった……。 ちなみに let のブロック内で it のメソッド呼び出しをしようとするとオプショナル型がアンラップされていないのに気づいてコンパイルエラーになる:

val a: String? = null
a.let { it.toInt() }  // オプショナル型 it への安全でないメソッド呼び出しでコンパイルエラー
a?.let { it.toInt() }  // これは OK (it.toInt() は実行されない)

Swift や Java ではこういうことはない

Swift の if-let ではこういうことはないし Java だとそもそも Optional 型でないと ifPresent メソッドをコールできないのでこういうことはない。

カラクリ

ソースを見ると実は Kotlin の let言語構造的なものではなく拡張メソッド (エクステンション) を使用して以下のように定義されている:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)  // 渡された関数オブジェクトを実行するだけ!!

この定義を見ると何てことはなく、ただ単に引数に渡された関数オブジェクトを実行するだけの拡張メソッドである。 つまり String? 型として let を呼べるし、呼ぶと上記 block の型は (String?) -> R となるだけのことだった。

let は実は以下の省略形と書けば理解が進む気がする:

val a: String? = null
a?.let { print(it) } // せいかい. 実は (String) -> Unit
a?.let { b: String -> print(b) }  // 冗長な書き方. 上と同じ
a.let { print(it) }  // 間違い. 実は (String?) -> Unit
a.let { b: String? -> print(b) }  // 冗長な書き方. 上と同じ

Kotlin スタートブック 一人読書会: 第 II 部 Kotlin 文法詳解 前回に引き続き Kotlin スタートブックの一人読書会を継続する。 今回は第 II 部である。第 II 部が長いので 2 回に分ける。

第 5 章 関数

可変長引数は Java だと 型... (例えば String... args) だが Kotlin だと vararg を頭につける:

fun sum(vararg ints: Int): Int

可変長引数に配列を渡す場合は先頭に * をつける。Python みたいだ:

sum(*intArrayOf(1, 2, 3))

vararg 指定された型 Int は、特化された配列クラス (IntArray) とみなされるので、IntArray のファクトリメソッド intArrayOf を使用しています。

とある。なるほど、やはり arrayOf で作成されるのは本当に Java の不自由な配列というわけだ。 確かに以下のコードを実行してみるとコンパイルエラーとなった:

sum(*arrayOf(1, 2, 3))  // Type inference failed. Expected type mismatch: inferred type is Array<Int> but IntArray was expected

結局 Java の不自由な配列 (Java のプリミティブ型とオブジェクト型でクラスが異なる) が裏に見えてしまうのはちょっとイケてない気がしてしまうが、致し方なかったのだろう。

その後再帰呼び出しの話が続くが、いきなり関数型プログラミングの話になり難しくなった。 Scala などでもそうだが巨大な再帰呼び出しでスタックオーバーフローにならないように tailrec キーワードを付けると TCO (末尾呼び出し最適化) が行われるようになる:

tailrec fun sum(numbers: List<Long>, accumulator: Long = 0): Long =
    if (numbers.isEmpty()) accumulator else sum(numbers.drop(1), accumulator + numbers.first())
    

また Kotlin でもローカル関数 (関数の中の関数) を定義できるとある。 個人的にこれも嬉しい仕様で、いわゆるヘルパー関数の影響の及ぶ範囲がわかりやすくなるのでコードが追いやすくなる。 Java 8 でも一応できる (Lambda 式をローカル変数に代入する) が書きやすいとは言い難い。

第 6 章 第一級オブジェクトとしての関数

初めに関数オブジェクトについての説明が入っていた:

Kotlin では関数も、数値や文字列などの他の値のように扱うことができます。...(中略)...このような性質を持つオブジェクトのことを、「第一級オブジェクト」(first-class object) と呼びます。

構成の関係上まだラムダ式が登場していなかったので簡単な具体例が出せなかったのだろうが、要するに以下のようなものだろう:

val plus = { x: Int, y: Int -> x + y }  // 足し算をする関数オブジェクト
print(plus(1, 2))  // 3

尚、冊子の方には関数オブジェクトという言葉は Kotlin 公式ドキュメントには登場しないので便宜上の用語として捉えて欲しい、と注釈してあった。

ただ Kotlin で関数 (関数オブジェクトではない) が定義されている場合に関数オブジェクトを取得する方法もあり、それは前置ダブルコロンで表現されるとのこと:

fun square(i: Int): Int = i * i
fun main(args: Array<String>) {
    println(::square)  // 前置 :: で関数オブジェクト取得
}

Java みたいな記法だ。 Java と同じくメソッド参照と言えば良いと思うのだが、冊子の中にはそういった記載は無い。 まぁこの場合だとメソッド参照でなく関数参照という事になるが。

その後いよいよラムダ式が登場した。

さらに、ラムダ式の引数が 1 つのときに限り、暗黙の変数 it を使用することができます。

val square2: (Int) -> Int = { it * it }

Swift だと引数が 2 個以上でも暗黙の変数 $0, $1, ... が使用できるので便利なのだが Kotlin では引数が 1 個の時しか使用できない。分かりやすさを重視したということだろうか。

その後インライン関数の説明が入っている:

インライン関数は、引数の関数オブジェクトが、コンパイル時にインライン展開される関数のことです。通常の関数に、アノテーション inline を付加するだけで、インライン関数になります。

inline はアノテーションだったのか。キーワードではなくて。

あくまで引数の関数オブジェクトがインライン展開されるようなので、引数が関数オブジェクトになっているような高階関数に使用するとパフォーマンスが向上するとのこと。以下の様な例があった:

inline fun log(debug: Boolean = true, message: () -> String) {
    // この中のコードが message() の箇所も含めて展開される
    if (debug) {
        println(message())
    }
}

コラムには同様に noinlinecrossinline もあると紹介されていた。

また、ラムダ式とは異なるものとして無名関数が紹介されていた:

val square2: (Int) -> Int = fun(i: Int): Int {
    return i * i
}

こんな書き方もできるのは初めて知ったが、ラムダ式を使わずこちらを使う意味が正直よく分からなかった。

第 7 章 オブジェクトからクラスへ

Java やその他の言語でインタフェースやクラスに馴染みのある読者は、この章を読み飛ばしてもいいでしょう。

と書いてあるようにほぼ Java と同じ概念の説明なので特筆すべきことは殆ど無い。

1 つだけ、Kotlin では Java でいうところの無名クラスの即時インスタンス化のような構文はオブジェクト式を用いて以下のように書く:

// 即時インスタンス生成
val bucket = object {
    ...
}

// Bucket インターフェース実装
val bucket = object : Bucket {
    ...
}

Android でも 2 つ以上メソッドを持つリスナの実装を行う際にこの記法が出てくる。

第 8 章 クラスとそのメンバ

プロパティの説明でバッキングフィールド (backing field) という聞き慣れない単語が出てきた。 Kotlin のクラスのプロパティ (フィールド) は自動的にバッキングフィールドが生成され、実際にはこのバッキングフィールドに値が格納されるとのこと。 バッキングフィールドを持たない例として以下の様な nameLength の例が紹介されていた:

class Person {
    var name: String = ""
        set(value) {
            println("${value}がセットされました")
            field = value
        }
    var age: Int = 0
    val nameLength: Int
        get(): Int = this.name.length
}

プロパティには上記の例のようなゲッターやセッターを定義できる。 セッター内で使用できる暗黙の変数 field に格納するとそのプロパティ値を格納したことになる。 直接 name = value などとするとセッターが無限に呼びだされ続けスタックオーバーフローになるとのこと。

プロパティは必ず初期化する必要があるが、それだと DI やユニットテストの時に不便なので lateinit キーワードを使うと初期化を遅らせることができる、という説明があった。

Kotlin におけるセカンダリコンストラクタ (2 つ以上コンストラクタを持つクラス) の定義はクラス内に constructor キーワードで定義するとのこと:

class Rational(val numerator: Int, val denominator: Int) {
    constructor(numerator: Int) :  this(numerator, 1)
}
class Rational(val numerator: Int, val denominator: Int = 1)  // この例の場合こちらの方がシンプル

また、既存のクラスを継承せずにメソッドを生やすことができる拡張関数 (エクステンション) に関して説明されていた。 Swift にもあるがとても便利だ。

第 9 章 継承と抽象クラス

Kotlin は、デフォルトではクラスを継承することができません。クラスを継承可能にするには、修飾子 open をクラスに対して付ける必要があります。

継承可能であることを明示するために open を付ける。 これは良い設計だと思う。 基本的に継承は影響が大きく、乱用するとコードがスパゲッティになりやすい。

また、オーバライド可能なメンバにも open を付けてオーバライド可能であることを明示する必要がある。

Java でいうところの全ての継承元のオブジェクトである Object にあたるのは Any である。 toString()equals()hashCode() がそれぞれ open で定義されている。

Kotlin の可視性修飾子には Java でいうところのパッケージプライベート (デフォルトのアクセス制限) とかいう訳のわからないものは勿論無く publicinternalprotectedprivate となっている。 internal は同一モジュール内に限りアクセス可能であることを示す修飾子である。 何も付けない場合はデフォルトで public となる。

待望の Kotlin 入門書

昨日待望の Kotlin に関する入門書が出版された。 都内のそれなりに大きい本屋に行ってみたが置いていなかったが、私はネットで予約しておいたので当日に入手することができた。 今回はこれの一人読書会を行う。

本書は Java エンジニア、特に Android アプリの開発経験者を対象としています。とはいえ、何かしらプログラミング経験をお持ちの方でしたら、無理なく、興味深く読み進めることができるでしょう。なぜなら……(以下略)

と本の帯にも書かれている。私はそこそこの Android アプリ開発の経験者であり Android 外でも業務系の Java を書いた人間なので対象読者にピッタリはまっている。 Kotlin に関しては Try Kotlin の Kotlin Koans (問題を解いていくと Kotlin の文法が覚えられる例題集) の基本的な部分を解いた程度の知識となっている。なので、以下の読書会の感想もあまりに基本的な部分は省くものとする。Kotlin の基礎文法に関しては Qiita などに幾らでも載っているのでそちらを参照。

第 1 章: ようこそ!Kotlin の世界へ

Kotlin が JetBrains 社によって開発されたことや Scala や Groovy などと同じく JVM 言語であること、Java の設計が最早古いものであり Kotlin がその欠点を克服していること、Kotlin の言語的な特徴、国内の幾つかの Android アプリも既に Kotlin で書かれリリースされていることが書かれている。まぁこの章は読み物として読んでおくだけでいいだろう。

第 2 章: Kotlin を始める

いきなり Hello World のコード例が出てくるが、「クラス」「シグネチャ」「変数」「仮引数」などの用語が説明無しで出てくるし、書き方が Java 経験前提のものになっているので、既に Java 未経験者は厳しい内容になっている気がする。まぁそうでもないとどうしても文章が冗長になってしまって読みにくいのでこれで良いのだが。

Try Kotlin の説明が入る。この Try Kotlin がサンドボックスとして良く出来ていて、先程も述べたように Kotlin の文法を覚える例題集 Kotlin Koans が秀逸だし、Kotlin をその場で書いて実行できる上 Command + Space (Ctrl + Space) でコード補完まで働く。わざわざ IDE をダウンロードする必要がない。

あと CUI コンパイラ (Java でいうところの javac) と REPL (対話的にプログラムを実行できるツール) と IntelliJ IDEA の説明もされているが、実際に Android 開発で使用する際は Android Studio 上で完結するので特に必要はない。また、プログラムを書いて実行するのも上記の Try Kotlin の方が手軽。

第 3 章: Kotlin ガイドツアー

Scala スケーラブルプログラミング (Scala コップ本) からヒントを得て有理数クラスを実装することにより Kotlin のチュートリアルを行うという事らしい:

class Rational (val numerator: Int, val denominator: Int)

コンストラクタありのクラス定義。Java と比べるととてもシンプル。ただこれだけだと toString した結果が hashCode を出力するような不格好なものとなるので以下の様な toString 実装例が書かれていた:

override fun toString(): String = "${numerator}/${denominator}"

override がアノテーションでなくキーワードだし、文字列の変数展開が ${...} で出来るのがやはり良い。 これや関数の定義が Scala や Swift と若干異なるのでこれらの言語を同時に書いていると混乱する。

また、以下のようにイニシャライザを書くことができるらしい:

init {
    require(denominator != 0, {"denominator must not be null"})
}

コンストラクタ内で変数代入以外の処理やチェックを行いたい場合に使えそうだ。 require という構文は assert 的なものらしい。 要するに条件を満たさない場合に IllegalArgumentException がスローされるとのこと。

Kotlin の演算子オーバーロードの書き方は operator fun ... である。 この本には例として operator fun plus(that: Rational): Rational の実装例が記載されていた。

また、Rational(1, 2) + 1 は可能だが 1 + Rational(1, 2) を可能にするために拡張関数が紹介されている。 Scala や Swift にもこういったものがあるが Java だと static なユーティリティメソッドを用意するといった不格好な方法しかないので個人的にこれは嬉しい。

例えば以下のように拡張関数を書くことで追加できるとあった:

operator fun Int.plus(r: Rational): Rational = r + this

第 4 章: 基本的な文法

この章は前の章よりも難易度が下がり Kotlin の基礎の基礎に関する説明が続く。 Kotlin を試し書きしたような人は流し読みでいい気がする。

私が初めて Kotlin を書いて正直戸惑ったのが配列のリテラル表記がないことだ。 例えば多くの言語では [1, 2, 3] などとして配列のインスタンスを書けるが Kotlin は arrayOf(1, 2, 3) などといった関数を使う。 List の場合は listOf(1, 2, 3) だ。 コンストラクタ表記 (例えば ArrayList(1, 2, 3)) ではないのは Java と同じく実装クラスでなくインターフェースを使うという意味だろうが、リテラルで書けないのはイマイチ格好良くない気がする……。 PHP 5.3 以前の array(1, 2, 3) を思い出してしまう (特に古い PHP は何でも連想配列なので度々この array() が出現し読みにくい)。 まぁ勝手に解釈すると arrayOf といった関数にしておくと初期化する要素の内容によって生成する実装クラスを変更できるので有利だ。

Qiita で Bundle を少しだけ簡単に書くという記事を書いた。今回はそれの Kotlin バージョンとなる。

ソースコード

/**
 * Bundle オブジェクトを作成する utility method.
 *
 * @param args key と value の pair
 * @return Bundle
 */
fun bundle(vararg args: Pair<String, Any?>): Bundle = Bundle().put(*args)

/**
 * すでにある Bundle オブジェクトに key と value を複数追加する utility method.
 *
 * @receiver Bundle
 * @param args key と value の pair
 * @return Bundle
 */
fun Bundle.put(vararg args: Pair<String, Any?>): Bundle = args.fold(this, { bundle, (key, value) ->
    when (value) {
        null -> bundle.putString(key, null)
        is Boolean -> bundle.putBoolean(key, value)
        is BooleanArray -> bundle.putBooleanArray(key, value)
        is Bundle -> bundle.putBundle(key, value)
        is Byte -> bundle.putByte(key, value)
        is ByteArray -> bundle.putByteArray(key, value)
        is String -> bundle.putString(key, value)
        is Char -> bundle.putChar(key, value)
        is CharArray -> bundle.putCharArray(key, value)
        is CharSequence -> bundle.putCharSequence(key, value)
        is Double -> bundle.putDouble(key, value)
        is DoubleArray -> bundle.putDoubleArray(key, value)
        is Float -> bundle.putFloat(key, value)
        is FloatArray -> bundle.putFloatArray(key, value)
        is Short -> bundle.putShort(key, value)
        is ShortArray -> bundle.putShortArray(key, value)
        is Int -> bundle.putInt(key, value)
        is IntArray -> bundle.putIntArray(key, value)
        is Long -> bundle.putLong(key, value)
        is LongArray -> bundle.putLongArray(key, value)
        is Parcelable -> bundle.putParcelable(key, value)
        is Array<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArray(key, value.map { it as String }.toTypedArray())
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArray(key, value.map { it as CharSequence }.toTypedArray())
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArray(key, value.map { it as Parcelable }.toTypedArray())
            } else {
                throw IllegalArgumentException("$key の配列における型パラメータが Bundle で扱えない.")
            }
        }
        is ArrayList<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArrayList(key, ArrayList(value.map { it as String }))
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArrayList(key, ArrayList(value.map { it as CharSequence }))
            } else if (value.size > 0 && value.all {it is Int}) {
                bundle.putIntegerArrayList(key, ArrayList(value.map { it as Int }))
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArrayList(key, ArrayList(value.map { it as Parcelable }))
            } else {
                throw IllegalArgumentException("$key の ArrayList における型パラメータが Bundle で扱えない.")
            }
        }
        else -> throw IllegalArgumentException(key + "に対応する値が解釈できない.")
    }
    bundle
})

使用例

以下の様な感じで使える:

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

    // Bundle オブジェクト生成 {hoge: "fuga", hage: [1, 2, 3]}
    val b = bundle("hoge" to "fuga", "hage" to arrayOf(1, 2, 3))

    // Bundle にあたかも put メソッドがあるかのように {hehe: "ouie"} 追加
    b.put("hehe" to "ouie")
    println(b)  // Bundle[{hage=[Ljava.lang.Integer;@a0250eb, hehe=fufu, hoge=fuga, hoho=ouie}]
}

拡張関数

Kotlin だと何が有利かというところで 1 つめ。 拡張関数という仕組みがあり、既存クラスにあたかもそういうメソッドを持っているかのように関数を追加できる。 上記例の fun Bundle.put(vararg args: Pair<String, Any?>): Bundle がそれである。 この仕組のお陰で別途ユーティリティクラスを作成してそれを呼ぶようなことをしなくても直接オブジェクトから呼べて自然な実装となる。

第一級関数

Java だと必ずクラスを作ってその中にメソッドを書かなければならないが Kotlin は一番外側に関数を記述可能。 上記例の fun bundle(vararg args: Pair<String, Any?>): Bundle がそれである。 しかもこの外側の関数をそのまま import できる (Java だとちょっと違うが static インポートに当たるか) のでソースコードがスッキリする。

to は custom operator

"hoge" to "fuga"to って何だろう。言語仕様だろうか。などと思ったかもしれないが、これは custom operator でビルトインのライブラリに以下のように定義されている:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

この infix というのを付けると自由な演算子が追加できるらしい。 この例だと任意のクラス A, B に対し to 演算子を適用すると Pair<A, B> インスタンスを生成して返す、というところか。

以前 Kotlin にはタプルがあったらしいが、これがあるので廃止されたらしい。

Hello World が短い

関係ないが Kotlin の Hello World が短い。以下で良い:

fun main(args: Array<String>) = println("Hello Kotlin!")

Java は以下:

public class Java {
    public static void main(String...args) {
        System.out.println("Hello World");
    }
}