nowで作成したAPIをKotlinとRetrofitとRxJavaとJacksonでGET/POSTする
いつもAndroidでRetrofit+RxJavaを使ってAPI通信する処理を書いてるが、今回はKotlinで挑戦してみる。
ついでに、使ったことのないnowもJacksonも触ってみた。
作成するもの
環境
- macOS Sierra
- kotlin_version = '1.2.21'
- Android Studio 3.0.1
- node v9.4.0
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
環境
- Android Studio 2.3.3
- Java8
- Kotlin 1.1.51
- realm 3.5.0
現象
既存のJavaのAndroid 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+Realmで*** is not part of the schema for this Realm問題にハマった - Qiita https://qiita.com/u_nation/items/d401a2b589d3c101ce19
- java - Object is not part of the schema for this Realm - Stack Overflow https://stackoverflow.com/questions/37887303/object-is-not-part-of-the-schema-for-this-realm
- realm-java/build.gradle at master · realm/realm-java https://github.com/realm/realm-java/blob/master/examples/kotlinExample/build.gradle
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歳)にプログラミングをマンツーマンで教えてみた。
使ったもの
生徒(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が出とる。
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
MacをSierraにアップグレードした後、ラズパイに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に同様の現象を質問してる人を発見。
MacOSがSierraになってからxauthの場所が変更されたらしく、
どうやら ssh -XでXQuartzが起動してないようだ。
対処法
- termnalで
/etc/ssh/ssh_config
をひらく
sudo vi /etc/ssh/ssh_config
- 最後の行に以下を追加
XAuthLocation /usr/X11/bin/xauth
- Mac再起動
これで ssh -XでXQuartzが起動するようになった。
解決!