KotlinでRowmaアプリを作るチュートリアル

**ROSロボットにトピックを送信するAndroidアプリ**を作るチュートリアルです。

通信にはRowmaというシステムを使っています。Rowmaについて詳しくは [Rowma: ROSロボットネットワーク化システム](https://qiita.com/asmsuechan/items/5f0fe8648ddfca1e5934) に書いてます。

完成図はこちら。ロボット一覧を表示してどれかをタップしたらロボットにアクセスして/chatterにトピックを送っています。

![Peek 2020-06-14 19-32.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/770b8e4c-89a6-4628-8eed-206f727f468f.gif)


## 準備
Android Studioのバージョン: 4.0
Kotlinのバージョン: 1.3.72

Rowma ROSパッケージを起動しておきます。

インストールはこちらのコマンドから。

```bash
python <(curl "https://raw.githubusercontent.com/rowma/rowma_ros/master/install.py" -s -N)
```

起動は

```bash
rosrun rowma_ros rowma
```

です。

## 新規プロジェクト作成
まずはプロジェクトを作成します。「File」->「New」-> 「New Project」で新規プロジェクトを作成。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/568cd2cc-04a8-d735-35b9-10f936d6e005.png)

Nameに**RowmaExample**、Package nameに**com.rowma.rowmaexample**と入力してFinishボタンをクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/ca29be0d-8ea8-eaa0-e6f0-09f24f889feb.png)

とりあえず実行します。エミュレーター環境が無い方はAVDマネージャーから作成します。
参考: 公式ドキュメント [仮想デバイスの作成と管理](https://developer.android.com/studio/run/managing-avds?hl=ja)

ウィンドウ右上にある再生ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/a5b56f9d-015c-197f-4e99-626187815004.png)

エミュレーターが表示されてアプリが起動したら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/79fd4b6b-68ce-13ed-f609-ad8a3852aefd.png)

## rowma-kotlinをインストール
左のファイルツリーから`build.gradle(Module: app)`を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/23d9354b-1835-3720-59c6-3e58c6199800.png)

`dependencies{}`に以下を追加します。

```
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation 'com.rowma.rowma-kotlin:rowma-kotlin:+' //追加部分
}
```

ここを変更するとAndroid Studioのコードエディター部分の上に「**Sync Now**」と出るのでこれをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/36fda704-30a1-8904-34d9-9e889791e010.png)

エラー無く成功したらインストール完了です。

## Rowmaサーバーへの接続
「java」->「com.rowma.rowmaexample」->「MainActivity」を開いて編集します。

rowma変数を追加して初期化します。

```kotlin:MainActivity.kt
package com.rowma.rowmaexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.rowma.rowma_kotlin.Rowma

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com") // 追加

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

「manifests」->「AndroidManifest.xml」を開いてインターネット接続の許可を与えます。


```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.rowma.rowmaexample">
    <uses-permission android:name="android.permission.INTERNET"/> // ここを追加

    <application>
      (中略)
    </application>

</manifest>
```

## `rowma.currentConnectionList()`を使ってロボット一覧を取得する

サーバーとの接続には**コルーチン**というものを使います。コルーチンを使うためには新しくライブラリをbuild.gradleに追加します。`build.gralde`のdependencies{}に`implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'`を追加します。

```gradle:build.gradle
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    (中略)

    // ここから3行を追加
    implementation 'com.rowma.rowma-kotlin:rowma-kotlin:+'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
}
```

追加したら**Sync Now**をクリックします。

次に`getCurrentRobots()`を追加します。

```kotlin:MainActivity.kt
package com.rowma.rowmaexample

import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import com.rowma.rowma_kotlin.Rowma
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.json.JSONArray

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")

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

    private fun getCurrentRobots() = GlobalScope.launch(Dispatchers.Main) {
        async(Dispatchers.Default) { rowma.currentConnectionList() }.await().let {
            val res = JSONArray(it.toString())
            println(res)
        }
    }
}
```

## ロボットリスト(ListView)の追加
右のファイルリストから「res」->「layout」->「activity_main.xml」を開きます。開いたら右上にある「**Design**」タブをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/f8959f83-4432-3479-8535-397e5290d2f8.png)

スクリーンが表示されたらデフォルトで配置されているコンポーネントを全て**削除**します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/e33c34b7-377c-6d85-8a22-c30d21c6d1f3.png)

「Palette」->「Layouts」->「LinearLayout(horizontal)」をその下の「Component Tree」にドラッグ&ドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/de49c31d-415b-4401-b075-28515acdfa02.png)

Paletteの虫眼鏡を押して**ListView**を検索します。

最近は[RecyclerView](https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ja)を使うらしいのですがめんどくさいのでListViewでお茶を濁します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/517c1487-1ac9-3572-4c02-4b8ea05309aa.png)

出てきた「ListView」を同じようにドラッグ&ドロップでComponent TreeのLinearLayoutの中に落とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/54a8dd43-390a-b8eb-845b-647cd27ea5cb.png)

Component Treeの中のRecyclerViewをクリックして右側のAttributes内の**idを入力**します。
idを設定します。ここでは`robot-list`とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/118de8e8-d0aa-b9b0-ee47-d42ff33dccbd.png)

追加できたら**再ビルド**します。

## ListViewにデータを表示する
上で作ったListViewの中身にデータを入れます。

```kotlin:MainActivity.kt
package com.rowma.rowmaexample

import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import com.rowma.rowma_kotlin.Rowma
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.json.JSONArray

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")
    private var robots : JSONArray = JSONArray() // 追加
    private val robotUuids : ArrayList<String> = ArrayList() // 追加

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getCurrentRobots()  // 追加
    }

    // 追加
    private fun getCurrentRobots() = GlobalScope.launch(Dispatchers.Main) {
        async(Dispatchers.Default) { rowma.currentConnectionList() }.await().let {
            robots = JSONArray(it.toString())
            for (i in 0 until robots.length()) {
                val item = robots.getJSONObject(i)
                robotUuids.add(item.getString("uuid"))
            }
            val robotListView : ListView = findViewById(R.id.robot_list)
            val arrayAdapter : ArrayAdapter<String> = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1, robotUuids)
            robotListView.adapter = arrayAdapter
        }
    }
}
```

再ビルドしてリストが表示されていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/34bd0085-f86e-e4fa-cf66-7d5fad3089ba.png)

## RobotActivityを追加
上でロボット一覧を取得・表示する画面を作りました。今度はロボット一覧からロボットを選択して操作を行う画面を追加します。

MainActivityを右クリックして「New」->「Activity」->「Empty Activity」をクリックします。

出てきた入力欄の「Activity Name」にRobotActivityと入力してFinishボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/f2a41668-d4f4-25be-25bb-c1d0b302801c.png)

次にレイアウトを変更します。「res」->「layout」->「activity_robot.xml」を開きます。

開いたら既にあるコードを消して中身を以下に変更します。

```xml:activity_robot.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".RobotActivity">

</LinearLayout>
```

ひとまず分かりやすくテキストを置きます。Paletteペーンから**TextView**を選択して適当な場所にドラッグ&ドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/39e45526-b71a-36df-3711-04dc89ff3652.png)

idには`robot-uuid`を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/8f3ae5d8-374e-4e2a-4395-5e16334c1322.png)

テキストはセンタリングしておきます。`android:textAlignment="center"`を`<TextView>`タグに追加します。

```xml:activity_robot.xml
    <TextView
        android:id="@+id/robotUuid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textAlignment="center" />
```

Warningが出ているので修正します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/eab09075-3a7c-5251-80b7-e2768f2bd17e.png)

Codeに移動して`android:text="TextView"`をクリックし**Alt + Enter**を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/b2c995a9-4b4b-dff1-3c66-050c38ca5657.png)

**Extract string resource**をクリックします。出てきた入力欄にResource name: robotUuid、Resource value: Robot UUIDと入力してOKボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/48fc72f4-bc2e-7022-c201-c19e0d1245bf.png)

これでWarningが消えました。

## MainActivityからRobotActivityに遷移する
以下のキャプチャのようにリストの要素を押すとMainActivityからRobotActivityに遷移するように実装します。

![Peek 2020-06-14 17-49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/3b405e87-80de-ced1-a96d-88bdaeffdccb.gif)

遷移には**Intent**というものを使います。MainActivityのonCreate()にIntentの処理を書きます。

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

        getCurrentRobots()

        // ここから追加
        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent : Intent = Intent(this, RobotActivity::class.java)
            this.startActivity(intent)
        }
    }
```

追加して再ビルドできたら完了です。

## RobotActivityにUUIDを渡す
MainActivityのリストにあるUUIDをクリックするとそのUUIDをRobotActivityに渡したいです。

`intent.putExtra()`を使うとデータを遷移先のActivityに渡すことができます。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")
    private var robots : JSONArray = JSONArray()
    private val robotUuids : ArrayList<String> = ArrayList()

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

        getCurrentRobots()

        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent = Intent(this, RobotActivity::class.java)
            intent.putExtra("ROBOT_UUID", robotUuids[position]) // 追加
            this.startActivity(intent)
        }
    }
    (中略)
}
```

RobotActivityでIntentのデータを受け取ってTextViewにappendします。

```kotlin:RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_robot)

        // ここから追加
        val robotUuid = intent.getStringExtra("ROBOT_UUID")
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robotUuid)
    }
}
```

ビルドしてロボットのUUIDが表示されれば成功です。

![Peek 2020-06-14 18-27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/96fb224b-8fcc-5e0b-3560-423246dace28.gif)

## ロボットに接続する
ようやくロボットに対してアクションを起こします。

まずはMainActivityからRobotActivityに送信するデータをUUIDから`robot`変数の中身をStringにしたJSONObjectへと置き換えます。

元々のUUIDのデータ↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/14d42fc6-4327-2d0a-4cc5-58f8bd09d842.png)

変更後のrobot変数の中身、UUIDも含んでいる↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/61d05723-ec58-b944-3e62-38e401cc9979.png)


まずMainActivity.ktのonCreate()内を変更してタップされたロボットの情報を全部渡すようにします。

```kotlin:MainActivity.kt
        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent = Intent(this, RobotActivity::class.java)
            intent.putExtra("ROBOT", robots[position].toString()) // ここ変更
            this.startActivity(intent)
        }
```

RobotActivityも変更します。

```kotlin:RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    private var robot : JSONObject = JSONObject()  // ここ変更

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

        // ここ変更
        val robotString = intent.getStringExtra("ROBOT")
        robot = JSONObject(robotString)
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robot.getString("uuid"))
    }
}
```

そして最後にロボットに接続してトピックをpublishします。

```kotlin:RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    private var robot : JSONObject = JSONObject()
    val rowma = Rowma("https://rowma.moriokalab.com") // 追加

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

        val robotString = intent.getStringExtra("ROBOT")
        robot = JSONObject(robotString)
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robot.getString("uuid"))

        rowma.connect() // 追加
        val msg = JSONObject() // 追加
        msg.put("data", "test message") // 追加
        rowma.publish(robot.getString("uuid"), "/chatter", msg) // 追加
    }
}
```

これでタップしたROSロボットに対して/chatterトピックが送信されます。RobotActivityが表示されるたびにROSにトピックが送信されています。

![Peek 2020-06-14 19-13.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/5d3965b9-ad0f-d30a-0e94-1ff4a4f9848a.gif)


ひとまずこれで終わり。

## その他
### `socket failed: EPERM (Operation not permitted)`でアプリが落ちる
以下のエラーが出る場合は端末を操作してアプリを一度消してから再インストールしましょう。

```
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.rowma.rowmaexample, PID: 15463
    java.net.SocketException: socket failed: EPERM (Operation not permitted)
```