読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】和歌山トイレマップで遊んでみる 2

Android Kotlin

はじめに

ずいぶんほったらかしてしまいましたが、和歌山県が公開しているトイレマップを使ったアプリを更新しました。

今週末の12/12に、和歌山大学にてわかやまITカーニバルというイベントが行われるそうで、私も行ってみることにしました。
このイベント、Githubがスポンサーになっているということで、上記のアプリを思い出したわけです。

今回はAndroid6.0に対応するのと合わせて、前から気になっていたKotlinを使ってみることにしたのでそれについてまとめます。

なお、今回以下の環境で実行しています。

SearchWakayamaToilet - GitHub

Kotlinを使う

Android StudioでKotlinを使うには、プラグインをインストールします。

  1. File > Settings > Plugins > Install JeBrains plugin... で「Kotlin」をインストールします。
  2. Android Studioを再起動します。
  3. Toolsに「Kotlin」が追加されるので、Tools > Kotlin > Configure Kotlin in Project をクリックします。
  4. 対象をAll modulesにし、Kolinのバージョンは「1.0.0-beta-3595-IJ141-11」にしました
    (最初0.14にしていましたが、String.splitが見つからないとエラーになったため、最新バージョンにしました)。

Kotlinのすごいところは、既存のJavaファイルを選択して、Code > Convert Java File to Kotlin File をクリックすると、自動でKotlinの記法に置き換えてくれることです。

1箇所うまく変換ができない部分があったため、そこだけ少しコード内容を変更しました。

before

~省略~
// 1行ずつ読み込む.
while ((strLine = bufferReader.readLine()) != null) {
~省略~

after

~省略~
// 1行ずつ読み込む.
while (true) {
    strLine = bufferReader.readLine()
    if(strLine == null){
        break
    }
~省略~

Kotlinでは変数がNullを持つことができるかどうかが重要となるため、beforeの形でもKotlinで使う方法はあるかと思いますが...。

Map

GoogleMapを使う方法は基本的には前回と同じです。
ただし、GoogleMapのインスタンスを取得するSupportMapFragment.getMap()がdeprecatedになっているため、SupportMapFragment.getMapAsync(OnMapReadyCallback callback)を使います。

このCallbackですが、Kotlinでは省略して書くことができます。
また、引数は「it」で受け取ることができます。

以上から、以下のように書くことができるわけです。

// マップの表示.
(supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment).getMapAsync({
        mMap = it
        mMap!!.isMyLocationEnabled = true})

シンプルに書ける反面、気を付けないと何が呼ばれ、何が引数として返ってくるのか分かりづらくなりそうですね...。

アクセス権

BLEのときと同じくAndroid6.0ではアクセス権があるかを確認し、ない場合はリクエストが必要となります。

MainActivity.kt

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

    mMain = this

    // Android6.0以降なら権限確認.
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        this.requestPermissions()
    }
~省略~
private fun requestPermissions(){
    
    if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
        this.getNewMap()
        this.loadCsv()
    }else{
        // 権限が許可されていない場合はリクエスト.
        requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_PERMISSIONS)
    }
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    // 権限リクエストの結果を取得する.
    if (requestCode == REQUEST_PERMISSIONS) {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // after being allowed permissions, get GoogleMap and start loading CSV.
            this.getNewMap()
            this.loadCsv()
        }
    }else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}
~省略~

基本的に前回と同じ内容ですが、配列がない分「onRequestPermissionsResult」などの引数の型が変更となっています。

実行順

現在のコードでは、以下の4つの処理を行っています。

  1. 位置情報へのアクセス権の確認
  2. GoogleMapのインスタンスを取得
  3. CSVからデータを取得
  4. 3のデータを元に地図上にマーカーを置く

この内1、2が非同期で実行されるため、1や2が完了していないのに4を実行したりするとクラッシュする、という問題が発生します。
そのため、確実に1、2が完了したことを確認した後3、4を実行するよう変更しました。

コード

MainActivity.kt

package jp.searchwakayamatoilet

import android.Manifest
import android.content.pm.PackageManager
import android.location.Geocoder
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
import android.util.Log
import android.view.Menu
import android.view.MenuItem

import android.support.v4.app.FragmentActivity
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.Locale
import java.util.regex.Pattern

class MainActivity : FragmentActivity() {

    private var mMap: GoogleMap? = null
    private var mMain: MainActivity? = null
    private var mStrToiletName: String? = null
    private var mDblLatitude: Double = 0.toDouble()
    private var mDblLongitude: Double = 0.toDouble()
    private final val REQUEST_PERMISSIONS: Int = 1

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

        mMain = this

        // Android6.0以降なら権限確認.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            this.requestPermissions()
        }
        else{
            this.getNewMap()
            this.loadCsv()
        }
    }
    private fun requestPermissions(){
        // 権限が許可されていない場合はリクエスト.
        if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            this.getNewMap()
            this.loadCsv()
        }else{
            requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_PERMISSIONS)
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
        // 権限リクエストの結果を取得する.
        if (requestCode == REQUEST_PERMISSIONS) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // after being allowed permissions, get GoogleMap and start loading CSV.
                this.getNewMap()
                this.loadCsv()
            }
        }else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
    private fun getNewMap(){
        // get GoogleMap instance.
        if (mMap != null) {
            return
        }
        // マップの表示.
        (supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment).getMapAsync({
                    mMap = it
                    mMap!!.isMyLocationEnabled = true})
    }
    private fun loadCsv(){
        val handlerThread = HandlerThread("AddMarker")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            val geocoder = Geocoder(mMain, Locale.getDefault())
            val asmAsset = mMain!!.resources.assets
            try {
                val ipsInput = asmAsset.open("toilet-map.csv")
                val inputStreamReader = InputStreamReader(ipsInput)
                val bufferReader = BufferedReader(inputStreamReader)
                var strLine: String?
                var strSplited: Array
                val ptnNumPattern = Pattern.compile("^[0-9]+")

                while (true) {
                    strLine = bufferReader.readLine()
                    if(strLine == null){
                        break
                    }

                    // とりあえず数値から始まっている行のみ
                    if (ptnNumPattern.matcher(strLine).find()) {
                        // とりあえずSplit後に4件以上データがある行のみ.
                        strSplited = strLine.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                        if (strSplited.size >= 4) {
                            // とりあえず名称と住所のみ.
                            val addrList = geocoder.getFromLocationName(strSplited[3], 1)
                            if (addrList.isEmpty()) {
                                Log.d("swtSearch", "list is empty")
                            } else {
                                val address = addrList[0]

                                mMain!!.mStrToiletName = strSplited[1]
                                mMain!!.mDblLatitude = address.latitude
                                mMain!!.mDblLongitude = address.longitude

                                getCsvHandler.sendEmptyMessage(1)
                            }
                        }
                    }
                }
                bufferReader.close()
                ipsInput.close()

            } catch (e: IOException) {
                Log.d("swtSearch", "IOException 発生")
            }
        }
    }
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val id = item.itemId

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true
        }

        return super.onOptionsItemSelected(item)
    }

    private val getCsvHandler = object : Handler() {
        override fun handleMessage(msg: Message) {
            addMarker()
        }
    }
    private fun addMarker() {
        if (mMap != null) {
            // 表示したマップにマーカーを追加する.
            mMap!!.addMarker(MarkerOptions().position(
                    LatLng(mMain!!.mDblLatitude, mMain!!.mDblLongitude)).title(mMain!!.mStrToiletName))
        }
    }
}

終わりに

とりあえずクリック一回でほぼKotlinに置き換えられるのはすごいですね。
プラグインもアップデートの確認も自動でやってくれるなど、とても便利です。

ただKotlinの書き方自体については、今のところ戸惑うことが多いので、もう少し続けて使ってみることでその便利さが分かるようになるかと思っています。

次はCSVから読み込んだデータをDBに保存するのと、マーカーをすべて置くのではなく現在位置から一定距離内にあるトイレのみにマーカーを置く、といったところに挑戦しようかと思います。

...12/12に間に合うかはわかりませんが苦笑。

参考

Kotlin

Map

Permission

Keystore