Kotlin歴が1ヶ月で、「あ、これは便利かも」と思えたKotlinの用法

まえおき

Kotlin歴が一週間の自分が、Kotlinのコードをレビューするときに指摘していること - YusukeIwakiのブログ を書いたあと、 id:suzan2go からコメントをいただいて、

Idiomatic Kotlin. Best Practices.

のページの存在を知ることになった。

業務でも「新しく作っていくものはKotlinで!」ってなったので、本格的に触り始めた。実際さわりはじめて「お!これはKotlinがいいかも」って思えてきたものを幾つか紹介してみる。

ちなみになんだかんだでJavaのほうがIDEが(速度的に)優秀なので、わりとJavaで書いてKotlinに変換してるんだけど、それは内緒。

SharedPreferenceをDelegated Propertyでread/writeする

「×ボタンを押したら二度と出さないようにする」みたいな制御をやるときには、かんたんなキャッシュとしてSharedPreferenceをささっと使いたい。ただ、経験上、SharedPreferenceは「ラッパークラスを作れば作るほど複雑化して、結局SharedPreferenceを生で使うのが一番いい」ということになりがちだった。

けど、KotlinのDelegated Propertyで、BooleanPref, StringPref, IntPref, LongPref, ... などなどうっっっっすーーーーーーいラッパークラスを用意してやることで

class HogeCache(val context: Context) {
  private val prefs get() = context.getSharedPreferences("hoge", Context.MODE_PRIVATE)

  var dontShowBanner: Boolean by BooleanPref(prefs)
}

みたいなクラスを作って

class HogeActivity: Activity {
  override fun onCreate(savedInstanceState: Bundle?) {

    ...

    buttonCloseBanner.setOnClickListener { _ ->
      hogeCache(this).dontShowBanner = true
    }

    if (hogeCache(this).dontShowBanner) {
      buttonCloseBanner.visibility = View.GONE
    }

みたいに書けるようになった。

github.com

可読性が上がって良いと思うので、適当にライブラリ化もしてみた。

(世の中には、↑をもっと汎用的にした chibatching/KotPref というライブラリもあったりする)

JSONArray#forEachを拡張関数で定義する

for (int i = 0; i < jsonArray.length(); i++) {
  JSONObject jsonObject = jsonArray.getJSONObject(i);

    ...
}

をKotlinでバカ正直に書くと

for (i in 0 until jsonArray.length()) {
  JSONObject jsonObject = jsonArray.getJSONObject(i);

    ...
}

みたいになる。

悪いコードじゃないんだけど、これでは可読性がやや低いと思う。

fun JSONArray.forEach(action: (JSONObject) -> Unit) {
  for (i in 0 until length()) {
    action(jsonArray.getJSONObject(i));
  }
}

という拡張関数を定義しておけば

jsonArray.forEach { jsonObject ->
  ...
}

のようにスッキリ書けて、可読性が上がる。

まだ試したことはないけど、ViewGroupとかでも同じことが言えると思う。

 

 

まとめ

Kotlinは「○○みたいなときには便利」みたいなしくみがアホみたいにたくさんある。

相変わらずKotlinは嫌いだけど、少しずつ勉強していくと可読性の高いコードが書けるようになっていきそう。

Kotlin歴が一週間の自分が、Kotlinのコードをレビューするときに指摘していること

Kotlin歴1年くらいになったら考えは変わるかもしれないので、Kotlin歴1週間での脳のスナップショットを書き留めておきます。Qiitaとかに書くと「いや、そうじゃないだろ」みたいなのが多数派だとおもうので、とりあえず自分のブログに書いておこう。

?. は3つ以上連続使用しないほうがいいんじゃない?

fragment.arguments?.getString("title")?.toUpperCase()

気持ちはわかるけど、それだったら「nullかもしれない」を排除する方向のコードにしたほうがいい気がする。 なんとなく、本当になんとなくだけど・・・。

スコープ関数は letalso だけ使うようにしましょう & apply は使わないで下さい

HogeFragment().apply {
  argument = Bundle().apply {
    putLong("id", id)
    putString("title", title)
  }
}

よりも

HogeFragment().also { fragment ->
  fragment.argument = Bundle().also { bundle ->
    bundle.putLong("id", id)
    bundle.putString("title", title)
  }
}

のほうが可読性高い気がする。 apply は、IDEの補助がないと、どのクラスのメソッドを読んでいるかを見失う。つらい。

Rubytrytap だけで概ね行けてるんだし、Kotlinだって letalso があれば行けるでしょ?とおもっているけど、違うのかな・・・。

スコープ関数とかブロックをネストするときは it は使わない

Realm.getDefaultInstance().use {
  it.executeTransaction {
    it.where(User::class.java).equalsTo("id", userId).findFirst()?.let {
      it.name = username;
    }
  }
}

だと、it が何を指してるのかパット見でわかりにくいので、

Realm.getDefaultInstance().use { realm ->
  realm.executeTransaction { realm ->
    realm.where(User::class.java).equalsTo("id", userId).findFirst()?.let { user ->
      user.name = username;
    }
  }
}

のようにレシーバーを明記しよう。 主語のない文が連続してる文章が読みづらいのと同じで、引数がないブロックが連続してるのもつらい。

スコープ関数とかブロックは3段以上ネストしない

fun saveUserName(username: String) {
  Realm.getDefaultInstance().use { realm ->
    realm.executeTransaction { realm ->
      realm.where(User::class.java).equalsTo("id", userId).findFirst()?.let { user ->
        user.name = username;
      }
    }
  }
}

でもパット見の印象は "複雑" だ。

fun inRealmTransaction(operation: (Realm) -> Unit) {
  Realm.getDefaultInstance().use {
    it.executeTransaction(operation)
  }
}


fun saveUserName(username: String) {
  inRealmTransaction { realm ->
    realm.where(User::class.java).equalsTo("id", userId).findFirst()?.let { user ->
      user.name = username
    }
  }
}

ネストの深さはせいぜいこのくらいであって欲しい。

その拡張関数、本当に拡張関数じゃないとダメなの?

IntentとかBundleとかに独自メソッドを生やすのは、まぁなんとなくはわかる。

一方で、FragmentとかActivityにメソッドを生やすのは「それ、ベースクラスつくって継承させるのじゃだめなの?」と疑いたくなる。 本当に継承じゃだめな理由があるなら、メソッドを生やせばいいけど、その場合には↓に紹介されてるように、Interfaceでスコープを明示的に示すように実装したほうが事故るリスクは少なそう。

dev.classmethod.jp

まとめ

よくわからないなりにも、"直感的にロジックが読めない"コードはどんなにきれいなコードでも排除しようとレビューをしている。

 

ところで、Googleが絶賛布教中?のDartっていう言語だと

Effective Dart: Usage | Dart

Effective Dart: Design | Dart

みたいなベストプラクティスが公式に示されている。それよりも後発のKotlinにはこういうコンテンツってないのだろうか?

Heroku+SendGridとfreenomで空メール送信を受け付ける何か(使い捨て)をつくる

空メール送信をしたら会員登録URLが送られてきて、それをタップするとアプリが開いて、Facebookで会員登録! みたいな流れを作りたかった。

実装にあたり、ネックとなるのは、空メールを受け付けるところ。受信可能なメールアドレスをなんとかしてお金かけずに作りたい。

ということで、なんとなくやってみた。

ドメインを無料でとる

freenom というところで12ヶ月までなら無料で使えるドメインを取ることができる。 (無料とはいえ、ドメインを持つ以上は住所とかいろいろ入れないといけないので、そのあたりは自己責任で...)

今回は、とりあえず kodama.tk という適当なドメインをとった。ネーミングに特に理由はない。(なんとなくechoを日本語にしただけ)

とりあえずHerokuで適当なプロジェクトをつくる

アプリ名は kodamatk にして、

bundle exec rails new . --api --skip-bundle --skip-test --database=postgresql

したのをGitHubに置いて、pipeline設定すると

https://kodamatk.herokuapp.com/

が使えるようになる。

f:id:YusukeIwaki:20171004111006p:plain

ただ、何もコンテンツがないRailsアプリは404しか返してくれず、ちゃんと動いてるか判別がつきにくいので、

github.com

こんな感じで https://kodamatk.herokuapp.com/inspect/ip というエンドポイントをなんとなく作った。

Herokuアプリにカスタムドメインを設定する

devcenter.heroku.com

に従う。

$ heroku domains:add www.kodama.tk
Adding www.kodama.tk to ⬢ kodamatk... done
 ▸    Configure your app's DNS provider to point to the DNS Target www.kodama.tk.herokudns.com.
 ▸    For help, see https://devcenter.heroku.com/articles/custom-domains

The domain www.kodama.tk has been enqueued for addition
 ▸    Run heroku domains:wait 'www.kodama.tk' to wait for completion
$ heroku domains
=== kodamatk Heroku Domain
kodamatk.herokuapp.com

=== kodamatk Custom Domains
Domain Name    DNS Record Type  DNS Target
─────────────  ───────────────  ───────────────────────────
www.kodama.tk  CNAME            www.kodama.tk.herokudns.com

この最後の1行がポイント。 www.kodama.tk CNAME www.kodama.tk.herokudns.comドメイン設定側(今回の場合はfreenom)で指定する必要がある。

f:id:YusukeIwaki:20171004111708p:plain

これだけ!

めっちゃかんたん。

暫く待つと、 http://www.kodama.tk/inspect/ip でさっきのIPアドレス表示ページにアクセスできるようになる。

SendGridにカスタムドメインを設定する

こっちはやや面倒。2つやることがある。

Whitelabelの設定

f:id:YusukeIwaki:20171004112049p:plain

ここにある。

[Add whitelabel]をおすと、入力フォームがでてくるので

f:id:YusukeIwaki:20171004112206p:plain

こんなかんじで。subdomainのところはなんでもいいらしい。とりあえず適当に mail にした。

Saveすると

f:id:YusukeIwaki:20171004112442p:plain

こんなかんじで、CNAMEを3つ設定してくれ、っぽいことが出てくるので、あらためてドメイン設定側(今回の場合はfreenom)で指定する。

f:id:YusukeIwaki:20171004112703p:plain

しばらく待ってから、SendGrid側で「Validate」をすると、 ✘ だったのが ✔ になる。

これで、カスタムドメインでのメールが使えるようになる。

Inbound Parseを設定する

先の手順まででカスタムドメインでメールの送信はできるが、受信にはもう一手間必要。

SendGridには、Inbound Parseという、受信したメールを特定のURLにWebHookで通知してくれる超便利機能があるので、それを使う。

設定手順はわりとかんたん。 (詳しくは→ Inbound Email Parse Webhook - ドキュメント | SendGrid )

まずは、ドメイン設定側(今回の場合はfreenom)でMXレコードを追加する。

f:id:YusukeIwaki:20171004113302p:plain

これで、 xxxxxxxx@kodama.tk というメールアドレスにメールを送ると、SendGridの方に届くようになる。

次に、SendGridでWebhookの設定

f:id:YusukeIwaki:20171004113507p:plain

ここから、

f:id:YusukeIwaki:20171004113453p:plain

こんな感じで、適当にWebHookのURLを指定する。( www.kodama.tk は証明書を取っていないのでhttps通信では使えないので、 kodamatk.herokuapp.com のほうのURLを指定している)

あとは、Railsアプリ側でWebHookをさばく処理を書くだけ!

namespace :webhook do
  post :email, to: 'email#incoming'
end
class Webhook::EmailController < ::ApplicationController
  def incoming
    mail_from = params[:from]  # 空メールを送った人のアドレス
    mail_to = params[:to] # xxxx@kodama.tk
    mail_title = params[:subject]
    mail_body = params[:text]

    # 空メールの内容が正しければ、会員登録URLをmail_fromに送る
    handle_mail(mail_from, mail_to, mail_title, mail_body)

    render plain: "ok"
  end
end

というかんじで、使い捨てと割り切れば無料でおためしができた。