nowで作成したAPIをKotlinとRetrofitとRxJavaとJacksonでGET/POSTする

いつもAndroidでRetrofit+RxJavaを使ってAPI通信する処理を書いてるが、今回はKotlinで挑戦してみる。
ついでに、使ったことのないnowもJacksonも触ってみた。

作成するもの

  • nowを使ったnodejs製のAPIモック(express)
  • Androidアプリ(Kotlin/Retrofit/RxJava/Jackson)

環境

API

準備

APIモックをexpressを使ったnodejsで作成し、nowにデプロイしていく。
nowについてはこの記事を参考にした。
以下、作業ログ

// APIモックのプロジェクト準備
$ mkdir try-now && cd try-now
// package.json作成
$ npm init -y
// expressを追加
$ npm i -S express
// apiの処理を書くjsを作成
$ touch index.js

// nowのインストール
$ npm i -G now
// nowのログイン(メールアドレスを入力すると確認メールが届くのでverifyする)
$ now login

実装

index.jsに以下を貼り付ける。
レスポンス変数は適当に作った。

var express = require('express')
var app = express()

app.get('/', function (req, res) {
  const json = {
    status: 1,
    card_status: 10,
    card_type: 'normal',
    card_id: "ca1111",
  }
  res.json(json)
})

app.post('/', function (req, res) {
  const json = {
    status: 2,
    card_status: 20,
    result_code: 1
  }
  res.json(json)
})

app.listen(3000)

ポートは3000を指定してるけど、なくても良い。

これでAPIモックはできあがった。

$ node index.js

としてローカルでも使えるが、今回はnowにデプロイする

デプロイ

nowでデプロイ、超簡単

$ now

https://try-now-kplvmhijbn.now.shというアドレスが表示されたので、これをAPIモックとして利用できる。

このアドレスに対してgetやpostをしていく。

Androidアプリ

使用するライブラリ

app/build.gradleに以下のライブラリ達を追加する

    // Retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-jackson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

    // RxJava2
    implementation "io.reactivex.rxjava2:rxjava:2.1.9"

    // RxAndroid
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

    // Jackson
    implementation 'com.fasterxml.jackson.core:jackson-core:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.4'
    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.4"

ソース:https://github.com/banbara23/try-now

実装

Retrofitのインタフェースを作成。

戻り値はRxjavaのObservableにする。

interface ApiService {
    @POST("/")
    fun post(): Observable<PostResponse>

    @GET("/")
    fun get(): Observable<GetResponse>
}

APIクライアントを作成していく。

今回、baseUrlにはnowで作成したurlを設定する。

class ApiClient {
    var apiService: ApiService

    init {
        val retrofit: Retrofit = Retrofit.Builder()
                .baseUrl("https://try-now-kplvmhijbn.now.sh")
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(JacksonConverterFactory.create(ObjectMapper()))
                .build()
        apiService = retrofit.create(ApiService::class.java)
    }

    /**
     * GETリクエスト
     */
    fun get(): Observable<GetResponse> =
            apiService.get()

    /**
     * POSTリクエスト
     */
    fun post(): Observable<PostResponse> =
            apiService.post()
}

APIクライアント呼び出しは以下

ラムダ初めて触った、すごい

  // get
        apiClient.post()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(
                        { res -> Log.d(TAG, res.toString()) },
                        { error -> Log.e(TAG, "{$error.message}") },
                        { Log.d(TAG, "post completed") }
                )

  // post
        apiClient.post()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(
                        { res -> Log.d(TAG, res.toString()) },
                        { error -> Log.e(TAG, "{$error.message}") },
                        { Log.d(TAG, "post completed") }
                )

モデルについては、 Response(共通レスポンス)を継承した各モデルが存在するイメージで作成した。

/**
 * 共通レスポンス
 */
open class Response(

        @field:JsonProperty("status") val status: Int = 0,
        @field:JsonProperty("card_status") val cardStatus: Int = 0
)
/**
 * GETレスポンスのモデル
 */
data class GetResponse(
        @field:JsonProperty("card_type") val cardType: String = "",
        @field:JsonProperty("card_id") val cardId: String = ""
) : Response() {
    override fun toString(): String =
            "GetResponse(status='$status' cardStatus='$cardStatus' cardType='$cardType', cardId='$cardId')"
}
/**
 * POSTレスポンスのモデル
 */
data class PostResponse(
        @field:JsonProperty("card_type") val cardType: String = "",
        @field:JsonProperty("card_id") val cardId: String = "",
        @field:JsonProperty("result_code") val resultCode: Int = 0
) : Response() {
    override fun toString(): String =
            "PostResponse(status='$status' cardStatus='$cardStatus' resultCode='$resultCode')"
}

この継承してるあたり、もうちょいうまくできないだろうか。

ビルドして実際に動作させると、Logcatに以下のログが表示されていればGET/POSTが成功している

getのログ
D/AppCompatActivity: GetResponse(status='1' cardStatus='10' cardType='normal', cardId='ca1111')
D/AppCompatActivity: get completed

postのログ
D/AppCompatActivity: PostResponse(status='2' cardStatus='20' resultCode='1')
D/AppCompatActivity: post completed

ソース:https://github.com/banbara23/Kotlin-Jackson-Sample

ハマったところ

Jacksonが@JsonPropertyを認識してくれない

APIレスポンスをJacksonでシリアライズする際、共通レスポンスモデルであるResponse.ktがKotlin用のアノテーションでないとエラーになる。

Jacksonが継承元の変数を@JsonPropertyだと認識してくれなかった。
継承先は@JsonPropertyでも大丈夫なのに…

NG : @JsonProperty
OK : @field:JsonProperty

実際のコード

/**
 * 共通レスポンス
 */
open class Response(
        // NG
        //@JsonProperty("status") val status: Int = 0,
        //@JsonProperty("card_status") val cardStatus: Int = 0

        // OK
        @field:JsonProperty("status") val status: Int = 0,
        @field:JsonProperty("card_status") val cardStatus: Int = 0
)

同様の問題が起きてないか調べたところ、以下のリンクに辿り着き、kotlinアノテーションを使うことで解決できた。 - https://github.com/FasterXML/jackson-module-kotlin/issues/56#issuecomment-321932309 - https://github.com/FasterXML/jackson-module-kotlin/issues/98#issuecomment-352016501

感想

nowが楽すぎてすごい

kotlinだとJavaよりコード量少なくて楽で良い

IllegalArgumentException: *** is not part of the schema for this Realm

環境

現象

既存のJavaAndroid StudioプロジェクトにKotlinを導入してビルドしたところ、起動時以下のエラーが必ず発生してお手上げとなった。

java.lang.RuntimeException: Unable to resume activity {my.package.MainActivity}: java.lang.IllegalArgumentException: MyMessageData is not part of the schema for this Realm
      at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3429)
      at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3469)
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2732)
      at android.app.ActivityThread.-wrap12(ActivityThread.java)
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6119)
      at java.lang.reflect.Method.invoke(Native Method)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
   Caused by: java.lang.IllegalArgumentException: MessageData is not part of the schema for this Realm
      at io.realm.internal.modules.CompositeMediator.getMediator(CompositeMediator.java:169)
      at io.realm.internal.modules.CompositeMediator.getTableName(CompositeMediator.java:87)
      at io.realm.RealmSchema.getTable(RealmSchema.java:218)
      at io.realm.RealmSchema.getSchemaForClass(RealmSchema.java:239)
      at io.realm.RealmQuery.<init>(RealmQuery.java:122)
      at io.realm.RealmQuery.createQuery(RealmQuery.java:76)
      at io.realm.Realm.where(Realm.java:1342)

MyMessageData is not part of the schema for this Realm
日本語訳:MyMessageDataは、このレルムのスキーマの一部ではありません

おかしいな、Kotlin追加する前までビルド通ってたんだけどなー

解決方法

なんと、app/build.gradleの宣言順を入れ替えるだけで解決した。

Before

apply plugin: 'com.android.application'
apply plugin: 'realm-android'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
...

After

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' // <= こいつを一番最後にする

android {
...

apply plugin: 'realm-android'よ、お前は一番最後だったのか…
これだけでビルドが通った。

エラーでググるとStackOverFlowにも回答がすぐに見つかったため、割と同様にハマっている人が多いのかもしれない。

参考リンク

KotlinでDatabaseException: mypackage.Item does not define a no-argument constructor.

状況

Android Studio: 3.0.1
Kotlin: 1.2.10
firebase-database: 11.8.0

現象

Kotlinの勉強がてらFirebaseのRealTime Databaseでデータを取得しようとしたらこんなエラーが出た。

com.google.firebase.database.DatabaseException: Class my.com.package.Item does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped.
    at com.google.android.gms.internal.zzelx.zze(Unknown Source)
    at com.google.android.gms.internal.zzelw.zzb(Unknown Source)
    at com.google.android.gms.internal.zzelw.zza(Unknown Source)
    at com.google.android.gms.internal.zzelw.zza(Unknown Source)
    at com.google.android.gms.internal.zzelw.zza(Unknown Source)
    at com.google.firebase.database.DataSnapshot.getValue(Unknown Source)
    at my.package.MyListFragment$fetchData$1.onDataChange(MyListFragment.kt:83)
    at com.google.android.gms.internal.zzegf.zza(Unknown Source)
    at com.google.android.gms.internal.zzeia.zzbyc(Unknown Source)
    at com.google.android.gms.internal.zzeig.run(Unknown Source)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6119)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

※パッケージやクラス名は変更している

firebaseからの取得コードはこれ

    /**
     * 一覧に表示するデータを取得
     */
    private fun fetchData() {
        // Write a message to the database
        val database = FirebaseDatabase.getInstance()
        val myRef = database.getReference(mPath)

        // Read from the database
        myRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                // This method is called once with the initial value and again
                // whenever data at this location is updated.
                val items: List<Item>? = dataSnapshot.getValue(object : GenericTypeIndicator<ArrayList<Item>>() {})
                Log.d(TAG, "Value is: " + items)
                recyclerView!!.adapter = MyRecyclerViewAdapter(items!!, mListener)
            }

            override fun onCancelled(error: DatabaseError) {
                // Failed to read value
                Log.w(TAG, "Failed to read value.", error.toException())
            }
        })
    }

エラーメッセージのみ抜き出すと
Class my.com.package.Item does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped.

日本語に訳すと
クラスmy.com.package.Itemは、引数のないコンストラクタを定義しません。 ProGuardを使用している場合は、これらのコンストラクタが削除されていないことを確認してください。

解決方法

引数のないコンストラクタ…
もしかしてモデルとして作ったItemクラスがおかしいのか?
と思い見てみると、ああ、Boolean型のisVideoに初期値を定義していなかった。

// 修正前
data class Item(
        val caption: String = "",
        val code: String = "",
        val data: String = "",
        val isVideo: Boolean,  // ←こいつの初期値がないのが原因
        val thumbnail: String = ""
)

とりあえずfalseでも突っ込んでおく

// 修正前
data class Item(
        val caption: String = "",
        val code: String = "",
        val data: String = "",
        val isVideo: Boolean = false,  //初期値falseを追加
        val thumbnail: String = ""
)

ビルドして動作させると...動いた!

モデルのコンストラクタに初期値を忘れるとこうなるんだな、勉強になった。

Android Studioで「Failed to resolve: com.android.support:appcompat-v7:27.+」

現象

久しぶりにAndroid Studio2.3.3を起動し、New Projectを作って初回Gradle Syncが実行されると、以下のメッセージが表示された。

Error:(26, 13) Failed to resolve: com.android.support:appcompat-v7:27.+
Install Repository and sync project
Show in File
Show in Project Structure dialog

ああ、sdkマネージャーに27が入ってないのね、と思いインストールするがエラーは直らず。

解決方法

プロジェクトルートのbuild.gradleを開いて+の行を追加する。

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
+        maven {
+            url 'https://maven.google.com/'
+            name 'Google'
+        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

再度syncするとエラーが消えた、解決!

前からNew Projectでこんなの手で入門する必要あったっけ?

6歳にプログラミングを教えて失敗した話

microbitをなんとか有効的に使いたい & 前からプログラミング教育に興味があったので、隣家の家族に協力してもらい、男の子(6歳)にプログラミングをマンツーマンで教えてみた。

使ったもの

  • Mac Book Air
  • USBマウス
  • microbit
  • USBケーブル(タイプB)

生徒(1人)

  • 隣人の6歳男子
  • ゲーム大好き
  • プログラミングに興味があるかは不明(多分ない)
  • 母親はプログラミング教育にかなり興味あり

授業について

  • 授業会場は筆者の家のリビング
  • 授業時間は30分
  • microbitサイトのビジュアルプログラミングで動作を組み立てるやり方
  • 筆者の家なので妻と娘(1歳)がリビングで自由にしてる
  • 生徒の親は不参加

やったこと

  • プログラムとはなにか?を軽く説明
  • Microbitで起動時にLEDで好きなマークを表示
  • microbitのAボタン押下でLEDにお化けを表示(if的な処理)
  • microbitのBボタン押下でLEDにウンチを表示(if的な処理)

結果

  • 「ゲームはプログラムで作られている」と説明すると「え?ほんと?」と興味を示す
  • 最初にmicrobitでLEDを表示させた後、飽きたのか席から逃亡して娘のオモチャで遊び始める
  • なんとか席に戻してもすぐ、逃亡して授業がなかなか進まなかった
  • なんとか授業は完遂したがプログラミングとは何か的なことは10%も伝わってない
  • 俺「なんとか終わったね、お疲れ様!」 男の子「そんな事より外で遊ぼうよー」

反省点

マウスが操作できる前提だった

  • まずこれが間違い
  • マウスに慣れてない子だった
  • クリックが狙ったところにできず苦労していた
  • 今時の若い子はPCを使えると思ったが、時代はスマフォが主流らしい

microbitに興味を示さなかった

  • 6歳の男の子なら光る機械に興味を持つだろうと思ったが、これが間違い
  • LEDを1回表示させたら飽きた
  • Scratchのようにキャラクターを動かすやり方の方がいい
  • microbitを使うならもう少し年齢が上じゃないとダメ

授業机にコタツはNG

  • コタツだと簡単に立ち上がって席から離脱できてしまう
  • 机+椅子で多少逃げにくくすべき

授業部屋はテレビがON & オモチャがあってはならない

  • 娘がテレビを見ていたので男の子もついつい見てしまう
  • 娘のオモチャが気になってPC画面を見てくれない
  • むしろ授業PCよりもテレビとオモチャを多く見ていた
  • 授業するなら誘惑するモノを同じ部屋に置いてはならない(当たり前)

男の子の母には成果物としてmicrobitの完成物を見せ、「え?これを作ったんですか?」的な驚きコメントを頂いた。
が、結果としてプログラミング学習できたかは怪しく、「子供にモノを教えて静かに聞いてもらえるって難しいですね」と伝えると笑ってくれた。

今回の失敗で得たもの

環境作りは超大事!

  • 家でやるならPC以外に何もない部屋を用意しなければならない

絵で説明すること

  • 口だけで説明しても男の子は全くイメージできてなかった
  • 下手な絵でもいいから紙にでも書いて説明するべき

年齢に合わせた学習方法を選ぶこと

  • 光るデバイスに飛びつくのは少数派らしい
  • 子供が興味をもつキャラクターものにするべき

次回に盛り込みたいこと

  • 生徒数を複数にしたい(競争させて集中させよう作戦)
  • ラズパイ3でScratchでの授業を試したい
  • microbitを使うなら音を出す授業にしたい(音が出せるの?と興味を持っていた)
  • バイスを使わずCode Monkeyのようなサイトでの授業もやってみたい

The following classes could not be instantiated:- android.support.v7.widget.AppCompatTextView

現象

Android Studio 2.3.3でPreview画面に何やらerrorが出とる。

f:id:banbara:20171221185836p:plain

The following classes could not be instantiated:- android.support.v7.widget.AppCompatTextView

java.lang.NullPointerException
    at android.content.res.Resources_Delegate.getValue(Resources_Delegate.java:788)
    at android.content.res.Resources.getValue(Resources.java:1286)
    at android.support.v4.content.res.ResourcesCompat.loadFont(ResourcesCompat.java:212)
    at android.support.v4.content.res.ResourcesCompat.getFont(ResourcesCompat.java:206)
    at android.support.v7.widget.TintTypedArray.getFont(TintTypedArray.java:119)
    at android.support.v7.widget.AppCompatTextHelper.updateTypefaceAndStyle(AppCompatTextHelper.java:208)
    at android.support.v7.widget.AppCompatTextHelper.loadFromAttributes(AppCompatTextHelper.java:152)
    at android.support.v7.widget.AppCompatTextHelperV17.loadFromAttributes(AppCompatTextHelperV17.java:38)
    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:81)
    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:71)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.jetbrains.android.uipreview.ViewLoader.createNewInstance(ViewLoader.java:475)
    at org.jetbrains.android.uipreview.ViewLoader.loadClass(ViewLoader.java:250)
    at org.jetbrains.android.uipreview.ViewLoader.loadClass(ViewLoader.java:213)
    at com.android.tools.idea.rendering.LayoutlibCallbackImpl.loadClass(LayoutlibCallbackImpl.java:193)
    at android.view.BridgeInflater.loadCustomView(BridgeInflater.java:333)
    at android.view.BridgeInflater.onCreateView(BridgeInflater.java:152)
    at android.view.LayoutInflater.onCreateView(LayoutInflater.java:717)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:785)
    at android.view.BridgeInflater.createViewFromTag(BridgeInflater.java:222)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:727)
    at android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:858)
    at android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:70)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:834)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:492)
    at com.android.layoutlib.bridge.bars.CustomBar.<init>(CustomBar.java:95)
    at com.android.layoutlib.bridge.bars.StatusBar.<init>(StatusBar.java:67)
    at com.android.layoutlib.bridge.impl.Layout.createStatusBar(Layout.java:224)
    at com.android.layoutlib.bridge.impl.Layout.<init>(Layout.java:146)
    at com.android.layoutlib.bridge.impl.RenderSessionImpl.inflate(RenderSessionImpl.java:301)
    at com.android.layoutlib.bridge.Bridge.createSession(Bridge.java:429)
    at com.android.ide.common.rendering.LayoutLibrary.createSession(LayoutLibrary.java:368)
    at com.android.tools.idea.rendering.RenderTask$2.compute(RenderTask.java:567)
    at com.android.tools.idea.rendering.RenderTask$2.compute(RenderTask.java:549)
    at com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:863)
    at com.android.tools.idea.rendering.RenderTask.createRenderSession(RenderTask.java:549)
    at com.android.tools.idea.rendering.RenderTask.lambda$inflate$1(RenderTask.java:680)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

一体なんだこのエラーは...

解決方法

values/styles.xmlのベーステーマのを修正する。

Theme.AppCompat.Light.DarkActionBar
から Base.Theme.AppCompat.Light.DarkActionBar
にすると直った。

 <resources>
 
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+    <style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
         <!-- Customize your theme here. -->
         <item name="colorPrimary">@color/colorPrimary</item>
         <item name="colorPrimaryDark">@color/colorPrimaryDark</item>

MacOS Sierraでラズパイssh接続時にWarning: untrusted X11 forwarding setup failed: xauth key data not generated

MacSierraにアップグレードした後、ラズパイにssh -Xで接続すると以下のエラーが出ていた。

Warning: untrusted X11 forwarding setup failed: xauth key data not generated

はじめは気付かず、ssh越しにラズパイの画像をCLIで表示しようとすると動かず

$ feh Pictures/image.jpg
feh ERROR: Can't open X display. It *is* running, yeah?

一体なにがあった...

原因

エラーをググるとStackOverFlowに同様の現象を質問してる人を発見。

stackoverflow.com

MacOSSierraになってからxauthの場所が変更されたらしく、
どうやら ssh -XでXQuartzが起動してないようだ。

対処法

  1. termnalで/etc/ssh/ssh_configをひらく
sudo vi /etc/ssh/ssh_config
  1. 最後の行に以下を追加
XAuthLocation /usr/X11/bin/xauth
  1. Mac再起動

これで ssh -XでXQuartzが起動するようになった。

解決!