- 投稿日:2019-12-22T23:57:35+09:00
【Android】Github Actions と Firebase App Distribution で特定のブランチ毎にベータ配布する
GitHub Actions
Firebase App Distribution
環境構築
事前準備
FIREBASE_APP_ID
とFIREBASE_TOKEN
をGithub ActionsのSecret Tokenで使用するので準備しておく。
FIREBASE_APP_ID
FIREBASE_TOKEN
firebase-toolsをインストールして以下のコマンドを実施、対象のGoogleアカウトで認証するとtokenが取得できる。
$ firebase login:ci
実装
ベータ配布用の
deploy.yml
を作成まずは、debugビルドでベータ配布するworkflowを作成します。
事前に準備したFIREBASE_APP_ID
とは別に debug用のFIREBASE_DEV_APP_ID
(application idが異なる分) を用意して使用してます。name: deploy on: push: branches: - feature/test-deploy jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup JDK uses: actions/setup-java@v1 with: java-version: 1.8 - name: Debug build run: ./gradlew assembleDebug - name: Deploy Firebase App Distribution [DEV] uses: wzieba/Firebase-Distribution-Github-Action@v1.1.1 with: appId: ${{secrets.FIREBASE_DEV_APP_ID}} token: ${{secrets.FIREBASE_TOKEN}} groups: developer file: app/build/outputs/apk/debug/app-debug.apk無事配布されるとFirebaseのコンソール上では以下の様に表示されます。
また、招待を受けたユーザーには以下の様なメールが配信されるはずです。
特定のブランチ毎に特定の処理を実施させたい場合
Github Actionsでは
if
で条件分岐できるので、現在のブランチを判定し処理を分岐させます。
https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions例) masterブランチのみ何か処理を行う場合
name: release build if: github.ref == 'refs/heads/master' run: ./gradlew assembleReleasedebugビルドとreleaseビルドをそれぞれ
develop
ブランチ,master
ブランチで行う場合の設定ファイルは以下になります。name: deploy on: push: branches: - develop - master jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup JDK uses: actions/setup-java@v1 with: java-version: 1.8 - name: Debug build if: github.ref == 'refs/heads/develop' run: ./gradlew assembleDebug - name: Release build if: github.ref == 'refs/heads/master' run: ./gradlew assembleRelease - name: Deploy Firebase App Distribution [DEV] if: github.ref == 'refs/heads/develop' uses: wzieba/Firebase-Distribution-Github-Action@v1.1.1 with: appId: ${{secrets.FIREBASE_DEV_APP_ID}} token: ${{secrets.FIREBASE_TOKEN}} groups: developer file: app/build/outputs/apk/debug/app-debug.apk - name: Deploy Firebase App Distribution [PRO] if: github.ref == 'refs/heads/master' uses: wzieba/Firebase-Distribution-Github-Action@v1.1.1 with: appId: ${{secrets.FIREBASE_APP_ID}} token: ${{secrets.FIREBASE_TOKEN}} groups: developer file: app/build/outputs/apk/release/app-release.apk※ 他に良いやり方がある場合ご指摘頂けると助かります
参考になったURL
- 投稿日:2019-12-22T22:51:34+09:00
Android端末に外部センサーをつけてみた
ひとり開発 Advent Calendar 2019、24日目の記事です。
少し前にAndroidに外部センサーをどうにかしてつけられないか?と思い、色々試行錯誤したらできちゃいました。
せっかくQiitaのアカウントもあるので今回はそれについて書きます。ざっくりと作ったものを紹介
中華Androidカーナビにセンサーが無かったので取り付けた pic.twitter.com/Ja0VMIAEK7
— 辰零 (@Ryo_TATSUREI) September 24, 2019動画のようにセンサーを接続したArduinoからAndroidへUSBシリアルでデータを渡し、XposedモジュールでSensorManagerに割り込ませる処理を行っています。
センサーはBNO055を使用。動画ではmicro:bitを使っていますが後でadafruit Trinket M0に変えてます。
作り方
大まかな作り方は
1.センサーとArduinoを繋ぐ!
2.Arduinoにプログラムを流し込む!(キャリブレーション値も取る!)
3.XposedモジュールをAndroidにインストール!
4.ArduinoとAndroidを繋ぐ!
5.動く!!!以下詳細です。
必要なもの
・adafruit Trinket M0
・BNO055(9軸センサー)
・Xposed導入済みのAndroid
・その他ハンダゴテとかそういう道具接続
上の画像のようにちゃちゃっと配線!
プルアップ抵抗は必要に応じて付けてください。コード(Arduino)
まずはキャリブレーション値を取らないといけないので下記のコードを書き込んでください。
※BNO055のライブラリが必要です。先に入れてください。リンク
キャリブレーション用コード
#include <BNO055_support.h> #include <Wire.h> struct bno055_t myBNO; unsigned char accelCalibStatus = 0; unsigned char magCalibStatus = 0; unsigned char gyroCalibStatus = 0; unsigned char sysCalibStatus = 0; unsigned long lastTime = 0; BNO055_S16 accOffsetX = 0; BNO055_S16 accOffsetY = 0; BNO055_S16 accOffsetZ = 0; BNO055_S16 gyroOffsetX = 0; BNO055_S16 gyroOffsetY = 0; BNO055_S16 gyroOffsetZ = 0; BNO055_S16 magOffsetX = 0; BNO055_S16 magOffsetY = 0; BNO055_S16 magOffsetZ = 0; boolean flgCalib = false; int calCount = 0; void setup() { lastTime = millis() + 1000; //Initialize I2C communication Wire.begin(); delay(500); BNO_Init(&myBNO); bno055_set_operation_mode(OPERATION_MODE_NDOF); delay(1); Serial.begin(115200); Serial.println(); Serial.println(); Serial.println(); Serial.println(); } void loop() //This code is looped forever { if ((millis() - lastTime) >= 500) { lastTime = millis(); if (!flgCalib) { Serial.print("Time Stamp: "); Serial.println(lastTime); bno055_get_accelcalib_status(&accelCalibStatus); Serial.print("Accelerometer Calibration Status: "); Serial.println(accelCalibStatus); bno055_get_magcalib_status(&magCalibStatus); Serial.print("Magnetometer Calibration Status: "); Serial.println(magCalibStatus); bno055_get_gyrocalib_status(&gyroCalibStatus); Serial.print("Gyroscope Calibration Status: "); Serial.println(gyroCalibStatus); bno055_get_syscalib_status(&sysCalibStatus); Serial.print("System Calibration Status: "); Serial.println(sysCalibStatus); Serial.println(); //To separate between packets calCount++; if (accelCalibStatus == 3 && magCalibStatus == 3 && gyroCalibStatus == 3) { flgCalib = true; //delay(1000); if (calCount > 10) { software_reset(); } } }else{ showCalibData(); } } } void showCalibData() { bno055_read_accel_offset_x_axis(&accOffsetX); bno055_read_accel_offset_y_axis(&accOffsetY); bno055_read_accel_offset_z_axis(&accOffsetZ); bno055_read_gyro_offset_x_axis(&gyroOffsetX); bno055_read_gyro_offset_y_axis(&gyroOffsetY); bno055_read_gyro_offset_z_axis(&gyroOffsetZ); bno055_read_mag_offset_x_axis(&magOffsetX); bno055_read_mag_offset_y_axis(&magOffsetY); bno055_read_mag_offset_z_axis(&magOffsetZ); Serial.print("AccCalibration"); Serial.println(); Serial.print("X:"); Serial.println(accOffsetX); Serial.print("Y:"); Serial.println(accOffsetY); Serial.print("Z:"); Serial.println(accOffsetZ); Serial.print("GyroCalibration"); Serial.println(); Serial.print("X:"); Serial.println(gyroOffsetX); Serial.print("Y:"); Serial.println(gyroOffsetY); Serial.print("Z:"); Serial.println(gyroOffsetZ); Serial.print("MagCalibration"); Serial.println(); Serial.print("X:"); Serial.println(magOffsetX); Serial.print("Y:"); Serial.println(magOffsetY); Serial.print("Z:"); Serial.println(magOffsetZ); } void software_reset() { SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk); }Tera Termなんかで接続するとキャリブレーション状況が出力されます。
キャリブレーションのやり方は公式が動画を出してるのでこちらを参考にしてください。
キャリブレーション後にArduinoを再起動している理由は、
私が購入したgy-bno055だとキャリブレーション値がうまく読み込めないためです。
原因不明ですが再起動させるとうまくいくのでしています。次にXposedモジュールと通信する用のコードを書き込みます。
※76~87行でオフセット値をセットしてます。
ここの部分はさっき取得したキャリブレーション値に書き換えてください。
Xposedモジュールと通信する用のコード
#include "BNO055_support.h" #include <Wire.h> struct bno055_t myBNO; struct bno055_accel accData; struct bno055_gyro gyroData; struct bno055_mag magData; struct bno055_euler eulerData; struct bno055_quaternion quatData; struct bno055_linear_accel linearAccData; struct bno055_gravity gravityData; BNO055_S16 magOffsetX = 0; BNO055_S16 magOffsetY = 0; BNO055_S16 magOffsetZ = 0; unsigned long lastTime = 0; int ledStatus = 0; int ledCount = 0; void setup() { Serial.begin(115200); lastTime = millis() + 1000; pinMode(PIN_LED, OUTPUT); Wire.begin(); delay(500); BNO_Init(&myBNO); bno055_write_accel_offset_x_axis(10); bno055_write_accel_offset_y_axis(-51); bno055_write_accel_offset_z_axis(8); bno055_write_gyro_offset_x_axis(-1); bno055_write_gyro_offset_y_axis(-3); bno055_write_gyro_offset_z_axis(1); bno055_write_mag_offset_x_axis(114); bno055_write_mag_offset_y_axis(598); bno055_write_mag_offset_z_axis(57); bno055_set_operation_mode(OPERATION_MODE_NDOF); bno055_set_accel_range(ACCEL_RANGE_8G); bno055_set_gyro_range(GYRO_RANGE_500rps); delay(1); } void loop() { if ((millis() - lastTime) >= 10) { lastTime = millis(); byte sendData[39]; bno055_read_accel_xyz(&accData); bno055_read_gyro_xyz(&gyroData); bno055_read_mag_xyz(&magData); bno055_read_linear_accel_xyz(&linearAccData); bno055_read_gravity_xyz(&gravityData); bno055_read_quaternion_wxyz(&quatData); byte d[2]; convertBytes(accData.x, d); sendData[0] = d[0]; sendData[1] = d[1]; convertBytes(accData.y, d); sendData[2] = d[0]; sendData[3] = d[1]; convertBytes(accData.z, d); sendData[4] = d[0]; sendData[5] = d[1]; convertBytes(gyroData.x, d); sendData[6] = d[0]; sendData[7] = d[1]; convertBytes(gyroData.y, d); sendData[8] = d[0]; sendData[9] = d[1]; convertBytes(gyroData.z, d); sendData[10] = d[0]; sendData[11] = d[1]; convertBytes(magData.x, d); sendData[12] = d[0]; sendData[13] = d[1]; convertBytes(magData.y, d); sendData[14] = d[0]; sendData[15] = d[1]; convertBytes(magData.z, d); sendData[16] = d[0]; sendData[17] = d[1]; convertBytes(linearAccData.x, d); sendData[18] = d[0]; sendData[19] = d[1]; convertBytes(linearAccData.y, d); sendData[20] = d[0]; sendData[21] = d[1]; convertBytes(linearAccData.z, d); sendData[22] = d[0]; sendData[23] = d[1]; convertBytes(gravityData.x, d); sendData[24] = d[0]; sendData[25] = d[1]; convertBytes(gravityData.y, d); sendData[26] = d[0]; sendData[27] = d[1]; convertBytes(gravityData.z, d); sendData[28] = d[0]; sendData[29] = d[1]; convertBytes(quatData.x, d); sendData[30] = d[0]; sendData[31] = d[1]; convertBytes(quatData.y, d); sendData[32] = d[0]; sendData[33] = d[1]; convertBytes(quatData.z, d); sendData[34] = d[0]; sendData[35] = d[1]; convertBytes(quatData.w, d); sendData[36] = d[0]; sendData[37] = d[1]; uint32_t chksum = 0; for (int i = 0; i < 38; i++) { chksum += sendData[i]; } sendData[38] = chksum & 0xFF; Serial.write(sendData, 39); ledCount++; if (ledCount > 2) { ledStatus = !ledStatus; digitalWrite(PIN_LED, ledStatus); ledCount = 0; } } } void convertBytes(int num, byte data[2]) { data[0] = (num & 0xFF00) >> 8; data[1] = num & 0xFF; } void software_reset() { SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk); }Arduino側はこれでOK
次はAndroid側になります。
コード(Android)
githubにプロジェクトごと上げたのでこれをビルドしてインストールすればOK(なはず)
XposedモジュールあとはXposedでモジュールを有効化し、ArduinoとAndroidをUSBで接続してください。
作ったきっかけ
使っているAndroidナビにセンサー類が一切入っておらず、Y!カーナビで「運転力診断試」やトンネル内の自車位置特定ができませんでした。
Xposedならどうにかして外部センサーの情報をアプリに渡せないか?と思い検索したところGyroEmuなるものを発見。
このモジュールはジャイロセンサーは無いけど加速度センサー、地磁気センサーはある端末でジャイロをソフトウェア的に再現し、SensorManagerに割り込ませるものです。
この仕組みを上手く使えば作れそう・・・と思い今回作りました。
(なので今回作ったXposedモジュールはGyroEmuのフォークとして上げてます)それでちゃんと動くの・・・?
Y!カーナビで試した感じではそれなりに動いているっぽいです(?)
Yカーナビで何度か運転力診断試したけどそれっぽい評価が出てるから多分大丈夫 pic.twitter.com/MLhjhPxq2g
— 辰零 (@Ryo_TATSUREI) September 24, 2019トンネル内でも自車位置特定がそれなりにできてるっぽいので多分大丈夫
他のアプリではわかりません(笑
おわりに
Qiitaアカウントを取ってからいつか自分も記事を書こうと思ってかなり月日が立ってしまいました・・・。
せっかくだしAdvent Calendarに登録しよう!と思って枠を取ったものの、全然書いておらず二日前に急いで書いた次第です。
内容も結構ニッチな感じではありますが普段からこういった自分がほしいアプリなどちまちま作っています。
またそのうち新しいアプリを作ったら記事を書きたいと思います。
- 投稿日:2019-12-22T22:23:13+09:00
Hack Day 2019に参加したレポ
はじめに
2019/12/14-15に開催された、ヤフー株式会社様主催のハッカソン「Hack Day 2019」に3人(@iaoiui、もう一人)で参加しました。エンジニアとして非常に刺激になったので、是非この時の記憶を残しておきたいと思い、記事を書かせていただきます。この記事を見て、少しでもハッカソンの
辛さ楽しさが伝われば幸いです。成果物
作成したものは「ARであそぼ」というAR×工作×パズルのアプリです。
文字の書かれた「パーツ」を組み合わせて、実際に存在する単語を作ると、それをマーカとして認識し、AR空間に「ほんもの」として出てくるアプリです。ただし、時間の都合上、実際には4文字を全て別個で認識したらオブジェクトが出てしまう感じになりました。
ワクワクさんリスペクトで、↓のように紙皿で「パーツ」を作っていきました。
「ロ」「ボ」「ッ」「ト」のパーツを集めると、
こんな感じでロボットのARオブジェクトが出ます。ロボット自体は、Polyで見つけた物です。
発表はネタに全振りしましたが笑、実際は子供の知育玩具としてのビジネス価値もあるのではないかと思っています。今回はそこまでできていませんが、
- 写真をアップロードしてマーカーとして登録する機能
- 任意の文字列(例:ロボット)に対して表示するオブジェクトを登録する機能
まで合わせて作成し、子供が自分で作った文字パズルでARが出せるよ!という製品を想定しています。
使用技術
キー技術としては、ARCoreを使用しています。元々は、メンバーの技術スタックを考えてAR.jsを使用しようと思っていましたが、できることが少なかったため、ARCoreもしくはARKitが選択肢となりました。しかし、ARKitのビルドに必要なMacを一人しか持っていなかったため、ARCoreを採用しました。
ARCoreのAugmentedImageで、予め登録しておいた「パーツ」を認識してARオブジェクトを表示させています。
途中、three.ar.js、WebARonARkit、WebARonARCore、ARToolkit、ARToolkitXも検討しましたが、ドキュメントが少なかったり、メンテがされていなかったりしたため不採用となりました。
「ARであそぼ」に至るまでに出たアイディア
ハッカソン参加に当たって、一番時間を使うのは「何を作るか」というところだと思います。最初は得意領域のAIを活用しようと考えていましたが、最終的に「映えるんじゃね?」ってことでARを使うことにしました。最後の方は時間がなくなってきたため、如何に簡単な技術で見栄えの良い物を作るか、という方向性にシフトしていきました。
3人でざっと100程度はアイディアを出しました。以下はその一部です。
- 水槽に水を入れたらセンサーが反応して画面の中の木が育つアプリ
- VRでアクアリウムができるアプリ
- 頭の形と髪質から実現可能な髪型をレコメンドしてくれる
- Adversarial Examplesで何か面白いことやる
- ARリアル脱出ゲーム
もうあった:https://nazoxnazo.com/shibuya_pp/- 次世代広告システムとしてのAR。広告を載せたい人は、座標あるいはマーカーをマーカーの場所を申請。ユーザがその場所に行ったり、マーカーにかざしたりすると広告が表示される。設置コストがほぼ0なところがポイント。
- 次世代SNSとしてのAR。自分の今いる場所に時間付きでメッセージを残せる。この店美味しいよ!とか、の情報共有もありだし絵馬に書くノリで何か書いてもよし。サイバー空間上だから誰かに迷惑をかけることもなし
もうあった:https://jp.techcrunch.com/2017/11/03/graffity-releases-their-first-app-formally-and-raised-30-mil-yen/- 三次元ウォーリーをさがせ
- Just a Line✕ピクトセンス。その場にいてマルチプレイができる。
- ARヘンゼルとグレーテル。パンをちぎってAR空間に置いておけるだけのアプリ。
- トランプウノ。トランプカード一枚一枚がマーカになっておりAR上ではウノとして遊べる。重ねると別のマーカとして認識したり。
- ARクロスワードパズル
→技術的にできそうということで、最終的にこれをベースにアイディアが固まっていった。レポ
2日間、ほぼ寝ずに作業し続けていました。以下は覚えている限りの二日間の軌跡です。
- [10:45] 会場につく。一人寝坊してまだこない。
- [11:30] 3人集まる。
- [11:50] 参加者に配布されたレストラン1000円クーポンを握り締めて、早速昼ご飯(海鮮丼)を食べに行く。
- [12:40] 食べ終わった。
- [12:50] ARCoreでできることについて調べる。Andorid Studioを使おうと思っていたが、Unityの方がいいんじゃね?となる。
- [13:30] ARCoreのサンプルがAndroid Studioで動いた。Unityはやっぱりやめることにする。
- [14:00] それぞれAndroid Studioで分担して開発する。が、エミュレータがうまく動かない。しかもARの動く実機端末は一個しかない。PCと接続するケーブル(USB3.0 to USB Type-C)もない。
- [14:30] ARで巨人を置いて遊ぶ。
![]()
- [14:50] ARのオブジェクトを作成できるSmoothie-3Dというサイトを見つける。試しにモナリザをオブジェクト化したところ、化物が出来上がる。
![]()
- [15:30] プレゼン方法を考え始める。Pixel3の画面をPCに写す方法がないことに気が付く。
- [15:50] Vysorというアプリを見つける。これならいけそうだ!有料なので仕方なく購入する。
- [16:40] 近くのビックカメラで発表用PCとPixel3を接続するコードを買ってくる。98円。
- [16:50] なぜかUnityに戻ってくる。
- [17:00] Unityだとエミュが使えないことが発覚。やっぱりAndroid Studioに戻る。どのみちエミュレータが動かないから一人しか開発できないことに気づいていない。
- [17:30] ARで遊び始める。ArgumentedImageで体をマーカーにして認識させようと試行錯誤する。
- [18:30] ArgumentedImageで体をマーカーにするのは精度的に厳しい(認識率が低い)と判明。簡単な(認識率の高い)マーカーでできることを考える。
- [19:30] 晩ご飯(うどん)を食べに行く。ご飯中もアイディアを考える(この時点でまだ決まっていない!?)。
- [20:30] 食べ終わった。
- [21:30] ようやくアイディアが固まる。「ARであそぼ」を作ることに決定。
- [22:00] 紙皿等々を買いに行く。工作をやろうとなったくせに、誰もペンすら持っていない。もはやホームセンターも空いていないので、コンビニで買える物で済ます。
- [23:00] 発表資料を作り始める。
- [0:00] ひたすら工作をする。認識率の高いマーカー作成に余念がない。
![]()
- [1:00] 眠い。
- [3:00] 何をやっていたか記憶がない。
- [5:00] 足りない紙コップとプリンを買いに行く。プリンはARオブジェクトにする予定だった。いいカメラで撮ったので画質が良い。
![]()
- [6:00] 仮眠をとる。近くの快活クラブがちょうど改装中だったため部屋で寝る。寒い。
- [8:00] ぼちぼち起きる。
- [9:00] 近くの銭湯へ。1カ所目、空きがない。仕方なく別のところへ行く(多分30分以上歩いた)。
- [10:20] 発表用に長いケーブルを買いに行く。
- [10:30] 朝ご飯を参加者に配布されたCOMPで済ます。
- [11:10] 発表最終準備。この時点でまだデモすら動かない。
- [11:50] なんとかデモはできそうか!?
- [12:00] 開発タイム終了。そして発表へ———
反省点
準備不足、それにつきます。事前に環境構築と技術確認(ARCoreで実際どこまでできるか?)は最低限済ませておくべきでした。エミュレータがうまく動かず、ARCore対応の実機が一台しかないため、ハッカソン通して@iaoiuiが全てのコーディングを担当することになってしまいました。
その上、全員ARもネイティブアプリ開発もほぼ未経験のため、開発現場は凄惨を極めました。
また、これまで同じメンバーでハッカソンは何回か参加しているので、「まあなんとかなるんじゃね?」という気持ちがどこかにあったせいもあると思います。
最終的に、発表では見せたい物を100%見せることはできませんでしたが、本物のワクワクさんにコメントを頂けたのでよしとします笑
おわりに
楽しかった!!それにつきます。若くて優秀なエンジニアや百戦錬磨のシニアエンジニアがこれほど集まる機会は他にはないのではないでしょうか。
来年も機会があれば是非参加させていただこうと思います。次こそは入賞を・・・!
- 投稿日:2019-12-22T20:55:58+09:00
Huggingface の DistilBERT を使って、Android で NLP を!
こんにちは。@rheza_h です。
この記事はACCESS Advent Calendar 2019 22日目の記事です。NLP は初めてですが、今回の記事は Huggingface が提供している DistilBERT を紹介し、それを使って、Android 上で動かしてみた記事です。
この記事で BERT については書かれていません。
BERT については論文 または @shotasakamoto のプレゼン資料が参考になれると思います。
社内リンクHuggingface
とは?
Natural Language Processing (NLP - 自然言語処理) を中心に研究開発をやっています。
Huggingface は 2016 に Brooklyn, New York で始まりました。
2017 にチャットボットをリリースしました。
Huggingface は自社の NLP モデルを開発して、Hierarchical Multi-Task Learning (HTML) と呼ばれています。
Chatty, Talking Dog, Talking Egg, Boloss と言う iOS アプリを開発しています。
DistilBERT と言うモデルを NeurIPS 2019 に公開されました。DistilBERTの話
小さく、早く、安く、軽く
state-of-the-arts の NLP モデルはほとんど large-scale language model を使われています。Transformer (Vaswani et al.,) のベースで研究されていて、最近の state-of-the-art モデルのパラメーターのサイズが大きくなっています。例えば NVIDIA が作った、MegatronLM と言うモデルは 8.3億パラメーターがあります。それは約160GBのテキストデータで学習されているそうです。
DistilBERT は名前の通り、"Distil"、必要・大事な部分だけを使用して、モデルが小さくして、精度は耐えられる程度で研究しています。
Knowledge Distillation [Bucila et al., 2006, Hinton et al., 2015] と言う方法をやっています。"Teacher-Student" 学習でもよく言われています。
モデルは2つあります。
- "学生"モデルと呼ばれています。
- 小さなモデル
- このモデルが"先生"モデルに似たような結果を出せるよう期待しています
- "先生"モデル
- ベースモデルは"学生"モデルと同じ
Knowledge Distillation の流れはいろんな流れがありますが、一番大切なのは Loss Function を組み合わせところだと思います。
pre-trainedを使う場合、
図2. Knowledge DistillationDistilBERT は名前の通り、BERT がベースになりますが、小さいバージョンが作られました。
ほとんどのアーキテクチャはそのままにさせましたが、token-type embeddings と pooler だけを削除し、レイヤーの数を2の因数に減らします。評価 (モデルパフォーマンス)
DistillBERT は学習のため、BERT と同じコルパスを使って、英語の Wikipedia と Toronto Book Corpus [Zhu et al., 2015]です。
8つの 16GB V100 GPUs を使って 90時間ぐらいかかったそうです。BERT と比べたら、パラメーターの数は 40% 少ないし、パフォーマンスは約2~3%しか減っていません。
Android の実装
Huggingface は以下のレポジトリでサンプルアプリを公開します。
https://github.com/huggingface/tflite-android-transformersDistilBERTをスクラッチから学習すると時間がかかりすぎるので、huggingface は tflite版のモデルを公開されています。
(コードは Huggingface の github で公開されています)
その tflite モデルをロードして、inference に使えばうまく動いています。モデルのロード
private static final int NUM_LITE_THREADS = 4; public synchronized void loadModel() { try { ByteBuffer buffer = loadModelFile(this.context.getAssets()); Interpreter.Options opt = new Interpreter.Options(); opt.setNumThreads(NUM_LITE_THREADS); tflite = new Interpreter(buffer, opt); Log.v(TAG, "TFLite model loaded."); } catch (IOException ex) { Log.e(TAG, ex.getMessage()); } } public MappedByteBuffer loadModelFile(AssetManager assetManager) throws IOException { // MODEL_PATH は assets にあります try (AssetFileDescriptor fileDescriptor = assetManager.openFd(MODEL_PATH); FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor())) { FileChannel fileChannel = inputStream.getChannel(); long startOffset = fileDescriptor.getStartOffset(); long declaredLength = fileDescriptor.getDeclaredLength(); return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength); } }predict をする前に、query の feature extraction をやることが必要です。
空白とか、query に要らない部分を抜いて、必要な部分だけを predict に渡します。Tokenization と関係するコードは以下のところで
https://github.com/huggingface/tflite-android-transformers/tree/master/bert/src/main/java/co/huggingface/android_transformers/bertqa/tokenization感想
- DistilBERT のおかげで BERT でも Android 端末で実行することができます。
- tflite のフォマットでpre-trained モデルが公開されているので、すぐ使えます。
- 答えの検索処理はコンテンツ内容によります。公開されたデータセットを使ってみるところ、Nexus 6P 端末で速度は 1~ 3秒ぐらいかかります。
- モデルのサイズは結構大きい (250MB) です。
- 次は日本語のモデルを試してみたいです。以下には CL-Tohoku が公開された 日本語の BERT です。 https://github.com/cl-tohoku/bert-japanese
最後に
明日は @hey3 の初めての記事です。お楽しみに!
参考
https://huggingface.co/
https://golden.com/wiki/Hugging_Face
https://techcrunch.com/2019/12/17/hugging-face-raises-15-million-to-build-the-definitive-natural-language-processing-library/
DistilBERT Paper
BERT Paper
Knowledge Distillation Paper
https://medium.com/huggingface/distilbert-8cf3380435b5
https://github.com/huggingface/tflite-android-transformers
https://nervanasystems.github.io/distiller/knowledge_distillation.html
https://towardsdatascience.com/model-distillation-and-compression-for-recommender-systems-in-pytorch-5d81c0f2c0ec
https://qiita.com/nekoumei/items/7b911c61324f16c43e7e
- 投稿日:2019-12-22T20:32:39+09:00
android javaでユーザからカメラ、写真フォルダへの許可を取る方法
android javaでユーザからカメラ、写真フォルダへの許可を取る方法
import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.support.v4.app.ActivityCompat; import android.support.v4.content.PermissionChecker; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private static final int REQUEST_CODE_CAMERA_PERMISSION = 1000; private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION = 2000; String title = "権限チェック"; String message = "この機能を使用する際には権限を許可する必要があります。"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button requestButtonCamera = (Button) findViewById(R.id.requestButton1); requestButtonCamera.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // カメラの権限状態取得 int permissionCamera = PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA); // カメラの権限確認 if (permissionCamera != PackageManager.PERMISSION_GRANTED) { requestPermission(REQUEST_CODE_CAMERA_PERMISSION); } // TODO : アクセス後の処理 } }); Button requestButtonWriteExternalStoragePermission = (Button) findViewById(R.id.requestButton2); requestButtonWriteExternalStoragePermission.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // カメラロールの権限状態取得 int permissionWriteExternalStorage = PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE); // カメラロールの権限確認 if (permissionWriteExternalStorage != PackageManager.PERMISSION_GRANTED) { requestPermission(REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION); } // TODO : アクセス後の処理 } }); } // Permission handling for Android 6.0 private void requestPermission(int requestCode) { // 権限チェックした結果、持っていない場合はダイアログを出す switch (requestCode) { case REQUEST_CODE_CAMERA_PERMISSION: alert(requestCode,new String[]{Manifest.permission.CAMERA},title, message); break; case REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION: alert(requestCode,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},title, message); break; } } private void alert(final int requestCode,final String[] manifestPermission, String title,String message){ DialogInterface.OnClickListener dialog = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityCompat.requestPermissions(MainActivity.this, manifestPermission, requestCode); } }; new AlertDialog.Builder(this) .setTitle(title).setMessage(message) .setPositiveButton(android.R.string.ok,dialog).create().show(); } }
- 投稿日:2019-12-22T19:57:36+09:00
利き酒アプリ開発(OSS公開)
はじめに
どうも,日本酒好きなエンジニアです.
日本酒とKotlinによるAndroidアプリ開発の勉強のために,利き酒アプリを作りましたので,ソースコードを共有いたします.
課題は残るものの一通り動くようになっていますので,アプリ開発の参考にしていただいたり,改造して使っていただければ幸いです.アプリ画面
公開
紹介動画:
https://youtu.be/l4n4CSIhghMコード:
https://github.com/nashimo/TastingGenie/tree/masterアプリの使い方や開発メモなどはGitHub上のPDFに記載してあります.
開発環境
- Android Studio 3.5.1
minSdkVersion 21, targetSdkVersion 28- Kotlin 1.3.11
- Anko 0.10.8
技術的な内容
このアプリで得られる情報と,残る課題について示します.
開発の参考になりそうな情報
私がKotlinを触るうえで苦戦したポイントと,解決した課題は以下のとおりです.ソースコードを見ると,どのように実装するかの参考になるかと思います.
- Activity-Fragment連携による状態遷移
- FragmentのListenerをActivityで処理する方法
- カメラ・ドキュメントから画像の登録 (図1)
- RecyclerView + CardViewによるデータのリスト表示 (図1)
- 1要素選択式のExpandableListview
- ヘルプ付きのExpandableListview + Checkbox (図2)
- 動的なLinearLayoutの追加 (図2のヘルプ部分)
- 酒情報,レビュー情報のSQLite3によるデータベース化
- ちょっとイケてる風のUI (図3)
残る課題
- カメラ・ドキュメントを使用するための権限の確認
今は権限がないと落ちる- 画像の保存ディレクトリを任意にする
MediaStoreの使い方がイマイチ分かっていない- DBの更新処理
DBにアクセスする回数を減らしたい- 各種例外処理
今はなんとなく実装している- 画像の削除
DBからリストを削除した時に,関連する画像も削除したい- 一部の値がハードコードされている
- 検索機能
酒情報やレビュー情報をフィルタリングして表示したい- DB解析機能
好みの酒のタイプを割り出したりしたい- ヘルプ機能
利き酒のやり方や,香りに関する説明を追加したい- エクセル書き出し機能
DBの内容を書き出したい開発の背景など
なぜ「利酒魔人1号」を開発することにしたかを説明します.
なぜ日本酒か(モチベーション・開発目的)
友達と温泉に行った際に,よく日本酒の利き酒をするのですが,全く当てられません.これは,自分の中で評価指標がなく,異なる酒が同じエリアにマッピングされているためだと考えました.
既存の日本酒用の利き酒アプリを探してみましたが,アプリの紹介画像から判断したところ,自由記述式のものが多いように思いました.これは,すでに評価軸があるユーザや,備忘録的な使い方で酒を楽しみたいユーザにとってはとても便利ですが,私の目的とは合っていないように思いました.
そこで,利き酒のやり方に則った評価指標を詰め込んだAndroidアプリを開発しました.自由記述式(感想など)の既存アプリでは解決できない「どう評価したらよいかわからない」を解決することが目的です.
謎のキャラについて
私がAndroidアプリ開発勉強のためにはじめて作ったアプリが割勘アプリでした.アプリ名が「割勘魔人2号」という名前でした(理由は失念).今回も,その流れを踏襲して,「利酒魔人1号」というアプリ名にしました.なぜKotlinか
- エンジニアは新しいものに触るべきだと思っているから
- Javaが好きじゃない
- Kotlinという名前が可愛い
参考情報
日本酒や利き酒に関して
[1] “木村克己”, 日本酒の教科書, 新星出版社, 2010
[2] 利き酒(ききざけ)のコツ
http://www.kamofuku.co.jp/hissyouhou/hissyouhou.htm
[3] 日本酒テイスティングの訓練方法
http://日本酒.biz/category20/entry204.html
[4] 利き酒のやり方
http://wakabajirushi.fc2web.com/kikisake.html
[5] KURAND 「これであなたも利き酒師気分!日本酒を表現するための言葉(色・香り・味)」
https://kurand.jp/blog/sake-taste-smell-color-kikizake/
[6] 極私的なワインテイスティングノート「日本酒テイスティングシートを作ってみました」
http://tnishino.blogspot.com/2015/01/blog-post.html
[7] ネタ酒場研究会「テイスティングノートのススメ」
http://www.netasakaba.net/?page_id=5232
[8] 灘酒研究会
http://www.nada-ken.com/main/アプリ開発に関して
Fragment遷移
[9] Fragmentによる画面遷移でハマった
https://qiita.com/tomo1139/items/62902093d1850085742d
[10] araemon/AndroidExercise
https://github.com/araemon/AndroidExercise/tree/master/TryFragment/app/src/main/java/com/apppppp/tryfragment
[11] Android 戻るボタンを押した時、積み上げたBackStackを1つ飛ばした画面に戻りたい
https://symfoware.blog.fc2.com/blog-entry-2037.htmlデータベース
[12] KotlinでAndroidアプリを開発(AnkoでSQLiteと一覧表示編)
https://qiita.com/naoi/items/6e6152c90c4219bbb055
[13] Anko-SQLite
https://github.com/Kotlin/anko/wiki/Anko-SQLite画像
[14] [android]ギャラリーもしくはカメラから画像を持ってくる
https://qiita.com/yukiyamadajp/items/137d15a4e65ed2308787
[15] ギャラリーから画像のURIとBitmapを取得する色んな方法
https://qiita.com/furusin_oriver/items/b6875f0703dbd49e7737
[16] Android 大きい画像を効果的に読み込む
http://y-anz-m.blogspot.com/2012/08/android.html
[17] Android: Bitmapを画面サイズにリサイズする
http://319ring.net/blog/archives/1504/
[18] [Android開発]削除した画像ファイルをギャラリーから削除する
https://erikori.com/blog/2015/06/13/android%E9%96%8B%E7%99%BA%E5%89%8A%E9%99%A4%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%AE%E3%83%A3%E3%83%A9%E3%83%AA%E3%83%BC%E3%81%8B%E3%82%89%E5%89%8A/
[19] [Android] 外部ストレージに画像を保存・読出しをする
https://akira-watson.com/android/external-storage-image.htmlリスト表示
[20] KotlinでRecyclerViewを使ったリスト表示を行う
https://qiita.com/Todate/items/297bc3e4d0f3d2477ed3
[21] RecyclerViewをAdapter.notifyDataSetChanged()を用いて更新できなかった理由を考察してみた
https://qiita.com/koujin/items/0bddffdab8f282a3cc6b
[22] Is it possible to create an expandable list AlertDialog?
https://stackoverflow.com/questions/6643625/is-it-possible-to-create-an-expandable-list-alertdialog
[23] Android ExpandableListView Using Kotlin With Example
https://tutorialwing.com/android-expandablelistview-using-kotlin-example/
[24] How to get checkbox state in expandable listview in android
https://stackoverflow.com/questions/33301708/how-to-get-checkbox-state-in-expandable-listview-in-androidダイアログ
[25] 【Android】パパっと使えるAlertDialog一覧
https://qiita.com/hokutonikukyu/items/ebbc0318c07035e04bf7
[26] Android - Custom Dialog with List Of Items (Java & Kotlin) !!
https://medium.com/@makkenasrinivasarao1/android-custom-dialog-with-list-of-items-ba1ab0e78e16Kotlin
[27] Kotlin ExtensionsのParcelizeアノテーションでdata classをスッキリさせる
http://tech.furyu.jp/blog/?p=6378
[28] Uri → String、String → Uri の型変換
https://qiita.com/superman9387/items/56ef1bd3f98f5eab5244
[29] Kotlinで時間を選ぶPickerを扱う
https://qiita.com/sass9872/items/b01d024f52249f79f316
[30] Calendar型⇄Date型の変換
https://hacknote.jp/archives/24285/
[31] onTouchは1回タッチで2回呼ばれる。
http://blog.livedoor.jp/shizuku_kun/archives/51362627.html
[32] SharedPreferences でアプリの設定値を保存する
https://maku77.github.io/android/fw/shared-preference.html
[33] Kotlin らしく SharedPreferences をシンプルに扱う Extension を実装する (Moshi Json シリアライズ対応)
https://qiita.com/irgaly/items/4b149068b2f62289e7a5
[34] AndroidでViewを角丸にする
https://qiita.com/masaibar/items/3df3ebbb981cd274fe78
- 投稿日:2019-12-22T18:47:27+09:00
SkyWayでiOS/Androidアプリを作るときの勘所
メリークリスマス
(まだ早い)
SkyWayでアプリを作ろう
iOS/AndroidのSDKが提供されているので作れはしますが、抑えるところ抑えておかないとユースケースによってはいくつか困るケースが出てきます。最近質問されたのでまとめておきます。
各種音声の入出力先のハンドリング
- Line
- Bluetooth
- デバイス
のハンドリングを自分でする必要があります。また抜き先に関しても同様。
iOSであればAVAudioSession
が握っている、AndroidであればAudioDeviceInfo
でとれるので、必要な入力元と出力先の検知と切り替えは自前で実装しましょう。この際にAndroidでは isWiredHeadsetOnのようなメソッドは非推奨。自前でハンドリングするためAudioSessionの管理には注意しましょう。
バックグラウンドでの処理
Androidは端末によりますが、iOSでは
UIBackgroundModes - audio
を許可してください。
バックグラウンドへの遷移時はどちらもカメラは無効になるためremoteへは黒い画面が表示されます。
上記設定をしておけば音声はつなぎ続けることができます。エラーハンドリング
https://webrtc.ecl.ntt.com/en/ios-reference/a00053_source.html
SKW_PEER_ERR_NO_ERROR = 0, SKW_PEER_ERR_BROWSER_INCOMPATIBLE = -1, SKW_PEER_ERR_DISCONNECTED = -2, SKW_PEER_ERR_INVALID_ID = -3, SKW_PEER_ERR_INVALID_KEY = -4, SKW_PEER_ERR_NETWORK = -5, SKW_PEER_ERR_PEER_UNAVAILABLE = -6, SKW_PEER_ERR_SSL_UNAVAILABLE = -7, SKW_PEER_ERR_SERVER_ERROR = -8, SKW_PEER_ERR_SOCKET_ERROR = -9, SKW_PEER_ERR_SOCKET_CLOSED = -10, SKW_PEER_ERR_UNAVAILABLE_ID = -11, SKW_PEER_ERR_AUTHENTICATION = -12, SKW_PEER_ERR_WEBRTC = -20, SKW_PEER_ERR_ROOM_ERROR = -30, SKW_PEER_ERR_UNKNOWN = - 9999,Peerイベントだけでもこれだけあり、なおかつ頻度としてもwarningレベルでリトライすればいいケースも多いです。websocketが動く関係上、リトライの概念は必須ですがユースケースによるので最初はエラーきたらとりあえずリトライかけておきましょう。
フロントのエラーイベントは運用上解析のために保存しておきたいですが、このエラーは結構な頻度で投げられてしまうので、何も考えずにストアしていくとだいぶなボリュームになるので注意。
ネットワークの切断・切り替え
これはちょっと調べきれてないですが、接続時間とかが課金系と関わるユースケースなら考慮が必要で、wifi/3Gの切り替えみたいのが起きるケースは自分でハンドリングするのがベターです。
両OSでそれぞれ接続が3GかWifiかくらいはとれるので、自前でやること。
相手のストリームを確認してカメラかどうか見る
これはAPIがないので、自前でやるのが良いです。
真っ黒になるのでpixelを見ましょう。サポートデバイス
どうしても限界はあります。iOSでは問題ないですが、やはりAndroidでよくわからない動きをすることがあり・・・全部の網羅は厳しいかと思います。AndroidだとCベースでのライブラリ(*.so)を乗せる必要がありABIやCPUアーキテクチャに突っ込む必要があったりします。
CPUアーキテクチャはスマタブ等で確認することができます。
まぁ全体の90%くらいは大丈夫な印象です。最新版のSDKのサポートは5系以上です。電池と通信量
これはもう食います。なおかつ通信方式(p2p, sft)にも依存するし、そもそも並行して何を立ち上げているかなどなど、外部要因が多いので明記しないほうが良いと思います。
callOptionで帯域指定だけでも入れておきましょう。
まとめ
マジですぐ作れるけど、マジで安定運用まではWebRTCといろんなユースケースと向き合う必要があるので気合い入れていこうな。
- 投稿日:2019-12-22T18:08:42+09:00
XamarinアプリをAppium連携する方法について(Android編)
この記事は、Xamarin Advent Calendar 2019の19日目の記事です。
先日、Appiumの連携でiOSでハマった問題と解決方法をまとめた記事をアップしました。
今回は、XamarinアプリをAppium連携する際にXamarin.Androidで絶賛ハマっている内容を紹介しようと思います。
環境
- MacBookPro Mojave 10.14
- Appium 1.15.1
- Visual Studio for Mac 8.3.7
- Xamarin.Android 10.0.6.2
- Xamarin.iOS 13.6.0.12
Appium連携
セットアップ
まず前提知識として、ネイティブのAndroidアプリをAppiumで実行する方法を理解している前提で話を進めていきます。
Xamarin.Androidの場合、
Droid/Properties/AndroidManifest.xml
には、最低限の設定しか行わず、各Activityクラスにプロパティを設定する必要があり、もしNameプロパティ
がActivityクラスに設定が無い場合は、ビルドされたAndroidManifest.xml
のandroid:name
タグにハッシュ付きのActivityクラス名が設定されます。
ちなみにビルドされたAndroidManifest.xml
は、Droid/obj/Debug/android/AndroidManifest.xml
にあるので、一度プロジェクト名.Droid
をビルドする必要があります。
次に上記の情報を踏まえてAppium側と連携する設定を行なっていきます。設定の際に一度、実機/エミュレータにアプリをビルドしておく必要があります。
.apk
の場所ですが、Droid/bin/Debug/
配下にビルドされています。問題
次の動画のようにアプリが異常動作し、Appiumは毎度アプリを入れ直すのでその後のアプリは壊れてしまします。
普通にhttps://t.co/AV3tOMaU1KのアプリとAppiumを連携させるとこうなる笑 pic.twitter.com/lAN4ptqgum
— gremito #ものラジ CSM® フリーランス (@grem_ito) December 22, 2019検証
次の検証を行なってみました。
- Activityクラス名にNameプロパティを付けてハッシュを無くす。
- 対象のAndroidバージョンをデバイスに合わせてポップアップを出さなくする。
- MainActivityをスプラッシュ画面ではなく、メイン画面に変更する。
結果としては、全く改善できませんでした笑汗
1は動画と同じ状況で2は動画後半のスプラッシュ画面でフリーズしてしまう状態でAppiumもローディングから抜け出せない状態になってしまします。
3は、メイン画面を開けるもののAppiumがローディングから抜け出せない状況が続き、最終的にはタイムアウトのエラーで異常終了してしまう結果となります。どうしたら正しく連携するのか...Xamarinの沼だwww pic.twitter.com/050gSnqv6u
— gremito #ものラジ CSM® フリーランス (@grem_ito) December 22, 2019Appiumのログを見てもタイムアウト以外のエラーは、
Got an error during apksigner execution ...
だけで、これが原因であれば署名をしていなかったネイティブのAndroidアプリでAppium連携できないはずだけども正常に連携できたので原因では無さそうで、、、打つ手無し状態です笑汗[Appium] Welcome to Appium v1.15.1 [Appium] Non-default server args: [Appium] allowInsecure: { [Appium] } [Appium] denyInsecure: { [Appium] } [Appium] Appium REST http interface listener started on 0.0.0.0:4723 [HTTP] --> GET /wd/hub/sessions [HTTP] {} [GENERIC] Calling AppiumDriver.getSessions() with args: [] [GENERIC] Responding to client with driver.getSessions() result: [] [HTTP] <-- GET /wd/hub/sessions 200 8 ms - 40 [HTTP] [HTTP] --> POST /wd/hub/session [HTTP] {"desiredCapabilities":{"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"Android Emulator","platformName":"Android","newCommandTimeout":0,"connectHardwareKeyboard":true}} [MJSONWP] Calling AppiumDriver.createSession() with args: [{"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"Android Emulator","platformName":"Android","newCommandTimeout":0,"connectHardwareKeyboard":true},null,null] [BaseDriver] Event 'newSessionRequested' logged at 1577003532774 (17:32:12 GMT+0900 (JST)) [Appium] Appium v1.15.1 creating new AndroidUiautomator2Driver (v1.37.2) session [BaseDriver] Creating session with MJSONWP desired capabilities: { [BaseDriver] "app": "/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk", [BaseDriver] "appActivity": "net.gremito.app.appiumtestforxamarin.MainActivity", [BaseDriver] "appPackage": "net.gremito.app.appiumtestforxamarin", [BaseDriver] "automationName": "UiAutomator2", [BaseDriver] "deviceName": "Android Emulator", [BaseDriver] "platformName": "Android", [BaseDriver] "newCommandTimeout": 0, [BaseDriver] "connectHardwareKeyboard": true [BaseDriver] } [BaseDriver] The following capabilities were provided, but are not recognized by Appium: [BaseDriver] connectHardwareKeyboard [BaseDriver] Session created with session id: 9a87fb97-0ded-404e-89c1-24158c5438b4 [BaseDriver] Using local app '/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' [UiAutomator2] Checking whether app is actually present [ADB] Found 6 'build-tools' folders under '/Users/gremito/Library/Android/sdk' (newest first): [ADB] /Users/gremito/Library/Android/sdk/build-tools/29.0.2 [ADB] /Users/gremito/Library/Android/sdk/build-tools/29.0.1 [ADB] /Users/gremito/Library/Android/sdk/build-tools/28.0.3 [ADB] /Users/gremito/Library/Android/sdk/build-tools/28.0.2 [ADB] /Users/gremito/Library/Android/sdk/build-tools/28.0.0 [ADB] /Users/gremito/Library/Android/sdk/build-tools/27.0.3 [ADB] Using 'adb' from '/Users/gremito/Library/Android/sdk/platform-tools/adb' [AndroidDriver] Retrieving device list [ADB] Trying to find a connected android device [ADB] Getting connected devices... [ADB] Connected devices: [{"udid":"emulator-5554","state":"device"}] [AndroidDriver] Using device: emulator-5554 [ADB] Using 'adb' from '/Users/gremito/Library/Android/sdk/platform-tools/adb' [ADB] Setting device id to emulator-5554 [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell getprop ro.build.version.sdk' [ADB] Current device property 'ro.build.version.sdk': 28 [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell getprop ro.build.version.release' [ADB] Current device property 'ro.build.version.release': 9 [ADB] Device API level: 28 [UiAutomator2] Relaxing hidden api policy [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings put global hidden_api_policy_pre_p_apps 1' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings put global hidden_api_policy_p_apps 1' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings put global hidden_api_policy 1' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 wait-for-device' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell echo ping' [AndroidDriver] Pushing settings apk to device... [ADB] Getting install status for io.appium.settings [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package io.appium.settings' [ADB] 'io.appium.settings' is installed [ADB] Getting package info for 'io.appium.settings' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package io.appium.settings' [ADB] Using 'apkanalyzer' from '/Users/gremito/Library/Android/sdk/tools/bin/apkanalyzer' [ADB] Starting '/Users/gremito/Library/Android/sdk/tools/bin/apkanalyzer' with args ["manifest","print","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/io.appium.settings/apks/settings_apk-debug.apk"] [ADB] The version name of the installed 'io.appium.settings' is greater or equal to the application version name ('2.14.2' >= '2.14.2') [ADB] There is no need to install/upgrade '/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/io.appium.settings/apks/settings_apk-debug.apk' [ADB] Getting IDs of all 'io.appium.settings' processes [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'pgrep --help; echo $?'' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell pgrep -f io\\.appium\\.settings' [AndroidDriver] io.appium.settings is already running. There is no need to reset its permissions. [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell appops set io.appium.settings android\:mock_location allow' [Logcat] Starting logcat capture [ADB] Getting install status for io.appium.uiautomator2.server [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package io.appium.uiautomator2.server' [ADB] 'io.appium.uiautomator2.server' is installed [ADB] Getting package info for 'io.appium.uiautomator2.server' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package io.appium.uiautomator2.server' [ADB] Starting '/Users/gremito/Library/Android/sdk/tools/bin/apkanalyzer' with args ["manifest","print","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v4.3.0.apk"] [ADB] The version name of the installed 'io.appium.uiautomator2.server' is greater or equal to the application version name ('4.3.0' >= '4.3.0') [UiAutomator2] io.appium.uiautomator2.server installation state: sameVersionInstalled [ADB] Checking app cert for /Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v4.3.0.apk [ADB] Using 'apksigner' from '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner' [ADB] Starting '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner' with args '["verify","--print-certs","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v4.3.0.apk"]' [ADB] apksigner stdout: Signer #1 certificate DN: EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android, L=Mountain View, ST=California, C=US [ADB] Signer #1 certificate SHA-256 digest: a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc [ADB] Signer #1 certificate SHA-1 digest: 61ed377e85d386a8dfee6b864bd85b0bfaa5af81 [ADB] Signer #1 certificate MD5 digest: e89b158e4bcf988ebd09eb83f5378e87 [ADB] [ADB] '/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v4.3.0.apk' is already signed. [ADB] Getting install status for io.appium.uiautomator2.server.test [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package io.appium.uiautomator2.server.test' [ADB] 'io.appium.uiautomator2.server.test' is installed [ADB] Checking app cert for /Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-debug-androidTest.apk [ADB] Starting '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner' with args '["verify","--print-certs","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-debug-androidTest.apk"]' [ADB] apksigner stdout: Signer #1 certificate DN: EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android, L=Mountain View, ST=California, C=US [ADB] Signer #1 certificate SHA-256 digest: a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc [ADB] Signer #1 certificate SHA-1 digest: 61ed377e85d386a8dfee6b864bd85b0bfaa5af81 [ADB] Signer #1 certificate MD5 digest: e89b158e4bcf988ebd09eb83f5378e87 [ADB] [ADB] '/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-debug-androidTest.apk' is already signed. [UiAutomator2] Server packages are not going to be (re)installed [UiAutomator2] Waiting up to 30000ms for services to be available [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell pm list instrumentation' [UiAutomator2] Instrumentation target 'io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner' is available [UiAutomator2] Forwarding UiAutomator2 Server port 6790 to 8200 [ADB] Forwarding system: 8200 to device: 6790 [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 forward tcp\:8200 tcp\:6790' [ADB] Checking app cert for /Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk [ADB] Starting '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner' with args '["verify","--print-certs","/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk"]' [ADB] Got an error during apksigner execution: Command '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner verify --print-certs /Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' exited with code 1 [ADB] apksigner stderr: DOES NOT VERIFY [ADB] ERROR: Missing META-INF/MANIFEST.MF [ADB] [ADB] '/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' is not signed with debug cert [ADB] Using 'zipalign' from '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/zipalign' [ADB] '/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' is not zip-aligned. Aligning [ADB] Signing '/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' with default cert [ADB] Starting '/Users/gremito/Library/Android/sdk/build-tools/29.0.2/apksigner' with args '["sign","--key","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-adb/keys/testkey.pk8","--cert","/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-adb/keys/testkey.x509.pem","/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk"]' [ADB] Getting install status for net.gremito.app.appiumtestforxamarin [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package net.gremito.app.appiumtestforxamarin' [ADB] 'net.gremito.app.appiumtestforxamarin' is installed [ADB] Getting package info for 'net.gremito.app.appiumtestforxamarin' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package net.gremito.app.appiumtestforxamarin' [ADB] Starting '/Users/gremito/Library/Android/sdk/tools/bin/apkanalyzer' with args ["manifest","print","/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk"] [ADB] The version name of the installed 'net.gremito.app.appiumtestforxamarin' is greater or equal to the application version name ('1.0.0' >= '1.0.0') [ADB] There is no need to install/upgrade '/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk' [AndroidDriver] Performing fast reset on 'net.gremito.app.appiumtestforxamarin' [ADB] Getting install status for net.gremito.app.appiumtestforxamarin [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys package net.gremito.app.appiumtestforxamarin' [ADB] 'net.gremito.app.appiumtestforxamarin' is installed [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am force-stop net.gremito.app.appiumtestforxamarin' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell pm clear net.gremito.app.appiumtestforxamarin' [AndroidDriver] Performed fast reset on the installed 'net.gremito.app.appiumtestforxamarin' application (stop and clear) [UiAutomator2] Performing shallow cleanup of automation leftovers [UiAutomator2] No obsolete sessions have been detected (Error: socket hang up) [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am force-stop io.appium.uiautomator2.server.test' [UiAutomator2] Starting UIAutomator2 server 4.3.0 [UiAutomator2] Using UIAutomator2 server from '/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v4.3.0.apk' and test from '/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-debug-androidTest.apk' [UiAutomator2] Waiting up to 30000ms for UiAutomator2 to be online... [ADB] Creating ADB subprocess with args: ["-P",5037,"-s","emulator-5554","shell","am","instrument","-w","io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner"] [WD Proxy] Matched '/status' to command name 'getStatus' [WD Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body [WD Proxy] Got an unexpected response with status undefined: {"code":"ECONNRESET"} [Instrumentation] io.appium.uiautomator2.server.test.AppiumUiAutomator2Server: [WD Proxy] Matched '/status' to command name 'getStatus' [WD Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body [WD Proxy] Got an unexpected response with status undefined: {"code":"ECONNRESET"} [WD Proxy] Matched '/status' to command name 'getStatus' [WD Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body [WD Proxy] Got response with status 200: {"sessionId":"None","value":{"ready":true,"message":"UiAutomator2 Server is ready to accept commands"}} [UiAutomator2] The initialization of the instrumentation process took 3277ms [WD Proxy] Matched '/session' to command name 'createSession' [WD Proxy] Proxying [POST /session] to [POST http://localhost:8200/wd/hub/session] with body: {"capabilities":{"firstMatch":[{"platform":"LINUX","webStorageEnabled":false,"takesScreenshot":true,"javascriptEnabled":true,"databaseEnabled":false,"networkConnectionEnabled":true,"locationContextEnabled":false,"warnings":{},"desired":{"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"Android Emulator","platformName":"Android","newCommandTimeout":0,"connectHardwareKeyboard":true},"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"emulator-5554","platformName":"Android","newCommandTimeout":0,"connectHardwareKeyboard":true,"deviceUDID":"emulator-5554"}],"alwaysMatch":{}}} [WD Proxy] Got response with status 200: {"sessionId":"6572217d-4279-4704-9475-7e2c95ba3153","value":{"sessionId":"6572217d-4279-4704-9475-7e2c95ba3153","capabilities":{"firstMatch":[{"platform":"LINUX","webStorageEnabled":false,"takesScreenshot":true,"javascriptEnabled":true,"databaseEnabled":false,"networkConnectionEnabled":true,"locationContextEnabled":false,"warnings":{},"desired":{"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"Android Emulator","platformName":"Android","newCommandTimeout":0,"connectHardwareKeyboard":true},"app":"/Users/gremito/Xamarin/AppiumTestForXamarin/Droid/bin/Debug/net.gremito.app.appiumtestforxamarin.apk","appActivity":"net.gremito.app.appiumtestforxamarin.MainActivity","appPackage":"net.gremito.app.appiumtestforxamarin","automationName":"UiAutomator2","deviceName":"emulator-5554","platformName":... [WD Proxy] Determined the downstream protocol as 'W3C' [WD Proxy] Proxying [GET /appium/device/info] to [GET http://localhost:8200/wd/hub/session/6572217d-4279-4704-9475-7e2c95ba3153/appium/device/info] with no body [WD Proxy] Got response with status 200: {"sessionId":"6572217d-4279-4704-9475-7e2c95ba3153","value":{"androidId":"f5a8d6d0364faa08","manufacturer":"Google","model":"Android SDK built for x86","brand":"google","apiVersion":"28","platformVersion":"9","carrierName":"Android","realDisplaySize":"1080x1920","displayDensity":420,"networks":[{"type":1,"typeName":"WIFI","subtype":0,"subtypeName":"","isConnected":true,"detailedState":"CONNECTED","state":"CONNECTED","extraInfo":null,"isAvailable":true,"isFailover":false,"isRoaming":false,"capabilities":{"transportTypes":"NET_CAPABILITY_SUPL","networkCapabilities":"","linkUpstreamBandwidthKbps":1048576,"linkDownBandwidthKbps":1048576,"signalStrength":-50,"networkSpecifier":null,"SSID":null}},{"type":0,"typeName":"MOBILE","subtype":13,"subtypeName":"LTE","isConnected":true,"detailedState":"CONNECTED","state":"CONNECTED","extraInfo":"epc.tmobile.com","isAvailable":true,"isFailover":false,"isRoaming":false,"capabilities":{"transportTypes":"NET_CAPABILITY_MMS","networkCapabilities":"","linkUpstreamBandwidthKbps... [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell dumpsys window' [AndroidDriver] Screen already unlocked, doing nothing [UiAutomator2] Starting 'net.gremito.app.appiumtestforxamarin/net.gremito.app.appiumtestforxamarin.MainActivity and waiting for 'net.gremito.app.appiumtestforxamarin/net.gremito.app.appiumtestforxamarin.MainActivity' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am start -W -n net.gremito.app.appiumtestforxamarin/net.gremito.app.appiumtestforxamarin.MainActivity -S' [UiAutomator2] Deleting UiAutomator2 session [UiAutomator2] Deleting UiAutomator2 server session [WD Proxy] Matched '/' to command name 'deleteSession' [WD Proxy] Proxying [DELETE /] to [DELETE http://localhost:8200/wd/hub/session/6572217d-4279-4704-9475-7e2c95ba3153] with no body [WD Proxy] Got response with status 200: {"sessionId":"6572217d-4279-4704-9475-7e2c95ba3153","value":null} [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am force-stop net.gremito.app.appiumtestforxamarin' [Instrumentation] . [Logcat] Stopping logcat capture [ADB] Removing forwarded port socket connection: 8200 [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 forward --remove tcp\:8200' [UiAutomator2] Restoring hidden api policy to the device default configuration [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings delete global hidden_api_policy_pre_p_apps' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings delete global hidden_api_policy_p_apps' [ADB] Running '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell settings delete global hidden_api_policy' [BaseDriver] Event 'newSessionStarted' logged at 1577003572240 (17:32:52 GMT+0900 (JST)) [MJSONWP] Encountered internal error running command: Error: Cannot start the 'net.gremito.app.appiumtestforxamarin' application. Visit https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/activity-startup.md for troubleshooting. Original error: Error executing adbExec. Original error: 'Command '/Users/gremito/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am start -W -n net.gremito.app.appiumtestforxamarin/net.gremito.app.appiumtestforxamarin.MainActivity -S' timed out after 20000ms'. Try to increase the 20000ms adb execution timeout represented by 'adbExecTimeout' capability [MJSONWP] at ADB.startApp (/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-adb/lib/tools/apk-utils.js:153:11) [HTTP] <-- POST /wd/hub/session 500 39492 ms - 713 [HTTP] [HTTP] --> DELETE /wd/hub/session [HTTP] {} [HTTP] No route found. Setting content type to 'text/plain' [HTTP] <-- DELETE /wd/hub/session 404 7 ms - 57 [HTTP]さいごに
引き続き調査を進めているので、解決次第アップデートします?
小ネタ①:Xcodeが古くXamarin.iOSが最新の場合
現在扱っているMBPは、Xcode10.1でiOSターゲットが12.1であり、その状態で最新のXamarin.iOSを扱うとiOSアプリをビルドすることができません。
次のようにターゲットを変えられるのですが、Xamarin.iOSが勝手にバージョン制御しているようでXcodeを最新にする必要があるようです。小ネタ②:Xamarin.iOSは正常にAppium連携できる
別環境で試してみたところXamarin.iOSの方は正常にAppium連携できました笑
- 投稿日:2019-12-22T18:05:26+09:00
Codelabs から学ぶ Direct Share
Codelabsの Direct Share to an Android app (eventタグ:Android Dev Summit 2019) を元にDirect Share(ダイレクトシェア)について説明します。
Direct Share とは
Android 6.0 から導入された機能で、アプリからコンテンツを共有する時に直接共有する相手を選択することができるというものです。この機能を使わない場合は、まず共有するアプリを選択した上で連絡先を指定する必要があります。
Android 6.0 (APIレベル23) では
ChooserTargetService
を利用する手法でしたが、 Android 10 (APIレベル20) では ChooserTargetService DirectShare API が Sharing Shortcuts API に置き換えられました。こちらではターゲットの読み込み時間を短縮することができます。ChooserTargetService DirectShare API と Sharing Shortcuts API
旧式の DirectShare API ではプルモデルが使用されていたのに対し、新式の Sharing Shortcuts API ではプッシュモデルが使用されています。これによりシェアターゲットを取得するプロセスが大幅に高速化されています。新式の API を使う場合は、シェアターゲットのリストを事前に用意し、例えば新しい連絡先が追加された時などにそのリストを更新する必要があります。
新式 API に書き換えないといけないわけではありません。が、Android 10 以降の Android Sharesheet では新式 API で提供された共有ターゲットの方が優先されるようになるので、旧式 API だと他の共有ターゲットに埋もれてしまう可能性はあります。
後方互換性を保ちたいからと言って旧式 API と新式 API を併用すると望ましくない挙動が発生する恐れがあります。なので旧式 API の代わりに
ShortcutManagerCompat
を使用するのが望ましいです。今回作るもの
コードは Codelabs の Direct Share to an Android app を使います。
・プレーンなテキストが送信できる
・他アプリからテキストを共有しようとした時に、今回作ったアプリが選択肢として出てくる
というものです。
https://github.com/googlecodelabs/android-direct-shareこの Codelabs ではすでに Direct Share 以外の部分は実装されていて、自分でコードを書く部分はこれくらいでした。
- 共有ショートカットを公開する
- 古いAndroidバージョンとの後方互換性を保ちつつ Direct Share 機能を使えるようにする
- コンテンツプレビューにタイトルとサムネイルを追加する
実装
shortcuts.xml
ここでは以下のことを宣言します。
- シェアするデータのタイプ
- 共有ショートカットのカテゴリ
- 共有インテントを扱うActivity
app/src/main/res/xml/shortcuts.xml<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <share-target android:targetClass="com.example.android.directshare.SendMessageActivity"> <data android:mimeType="text/plain" /> <category android:name="com.example.android.directshare.category.TEXT_SHARE_TARGET" /> </share-target> </shortcuts>
<category>
は公開済みショートカットと共有ターゲット定義を一致させるために使用するものです。この値は自由に決めて良く、また、1つの要素に複数のカテゴリを含めることができます。AndroidManifest.xml
shortcuts.xml
をandroid.intent.action.MAIN
アクションとandroid.intent.category.LAUNCHER
カテゴリを intent-filter に持つアクティビティ内で宣言する必要があります。<activity android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <!-- Reference resource file where the app's shortcuts are defined --> <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> </activity>
SendMessageActivity
では<intent-filter>
タグ内で、共有する時に扱うタイプを定義する必要があります。SendMessageActivity
は起動した時にインテントの内容を確認します。連絡先に関する情報がないときはSelectContactActivity
を起動して連絡先を選択し、その結果をonActivityResult
で取得します。<activity android:name=".SendMessageActivity" android:label="@string/app_name" android:theme="@style/DirectShareDialogTheme"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> <!-- 後方互換性を保つための記述 --> <meta-data android:name="android.service.chooser.chooser_target_service" android:value="androidx.sharetarget.ChooserTargetServiceCompat" /> </activity>ファイルコンテンツURIを安全に生成、共有するために
FileProvider
の定義も記述します。<provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.android.directshare.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>build.gradle
ShortcutManager API を使う準備をします。この API にはショートカットを更新、削除、追加するメソッドがあります。
※ShortcutManager とのやり取りはバックグラウンドスレッドで行う必要があります。Codelabs では
ShortcutManagerCompat
を使用しています。ShortcutManagerCompat
は旧式の ChooserTargetService DirectShare API との後方互換性を備えた共有ショートカットを提供する AndroidX の API です。プロジェクトで使うときはcore
を gradle に追加します。sharetarget
には古いAndroidバージョンでも動作するChooserTargetServiceCompat
が入っています。build.gradleimplementation "androidx.core:core:${versions.androidxCore}" implementation "androidx.sharetarget:sharetarget:${versions.shareTarget}"MainActivity
共有するテキストを入力、送信する画面です。
今回の例では、ユーザがアプリを開くたび(= MainActivity が立ち上がるたび)に共有ショートカットをプッシュします。この処理の詳細は
SharingShortcutsManager
の章で説明します。onCreatesharingShortcutsManager = SharingShortcutsManager().also { it.pushDirectShareTargets(this) }SHARE ボタンをタップした時の処理はこちらです。
private fun share() { val sharingIntent = Intent(Intent.ACTION_SEND) sharingIntent.type = "text/plain" sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, bodyEditText.text.toString()) // sharesheetのプレビューに表示されるタイトルを設定(optional) sharingIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.send_intent_title)) // Android Q 以降ではタイトル、画像などのプレビューを表示させることができる // sharesheetのプレビューに表示される画像のURIを設定(optional) val thumbnail = getClipDataThumbnail() thumbnail?.let { sharingIntent.clipData = it sharingIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } startActivity(Intent.createChooser(sharingIntent, null)) } private fun getClipDataThumbnail(): ClipData? { val contentUri = saveThumbnailImage() // フォルダアクセスの必要があるので、AndroidManifest.xmlでFileProviderの定義をする ClipData.newUri(contentResolver, null, contentUri) ... }SelectContactActivity
共有する人物の選択肢を RecyclerView で表示しています。 Direct Share 特有の実装はないので、ここでは説明を省きます。
SendMessageActivity
送る相手とメッセージを表示します。
SEND
をタップするとトーストが表示されます。Direct Share を使用した場合は
Intent.EXTRA_SHORTCUT_ID
を取得することができます。今回は連絡先IDがセットされているので、これを元に連絡先のUI表示を行います。val shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)SharingShortcutsManager
ShortcutManager とやり取りするクラスです。ショートカットに関連付けるカテゴリは自分たちでつけることができますが、
shortcuts.xml
で定義したものと一致する必要があります。private val categoryTextShareTarget = "com.example.android.directshare.category.TEXT_SHARE_TARGET"公開するショートカットのリストを追加するコードは以下の通りです。ここでは
Contact.kt
で定義した4つの連絡先を追加しています。ちなみにShortcutManagerCompat.getMaxShortcutCountPerActivity
で定義されている以上の数のショートカットを追加するとアプリがクラッシュしてしまいます。fun pushDirectShareTargets(context: Context) { val shortcuts = ArrayList<ShortcutInfoCompat>() // さっきのカテゴリをSet化 val contactCategories = setOf(categoryTextShareTarget) for (id in 0 until maxShortcuts) { val contact = Contact.byId(id) // ショートカットが静的ショートカットとして開かれた時のみ送られる val staticLauncherShortcutIntent = Intent(Intent.ACTION_DEFAULT) shortcuts.add( // idはアクティビティが共有インテントを受け取った時の識別子として必要 // idは受け取ったインテントでEXTRA_SHORTCUT_IDとして受け取れます ShortcutInfoCompat.Builder(context, Integer.toString(id)) // 共有先選択ダイアログに表示される短いラベルとアイコン .setShortLabel(contact.name) .setIcon(IconCompat.createWithResource(context, contact.icon)) .setIntent(staticLauncherShortcutIntent) // ショートカットがキャッシュされるようにしている // ショートカットの公開を停止してもsharesheetに表示される .setLongLived(true) // ショートカットのフィルタリングに使われる .setCategories(contactCategories) // 共有先の提案最適化のため // 指定はオプションであるものの、強く推奨されている .setPerson( Person.Builder() .setName(contact.name) .build() ) .build() ) } // 本当はCRUD操作等々で管理を改善した方がいいらしい ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) }これで、 Direct Share サンプルアプリ作成完了です。
参考文献
- 投稿日:2019-12-22T13:01:46+09:00
新しく追加されるViewTreeLifecycleOwnerについて
プルリクをなんとなく見ていて発見して面白そうだったので書いてみました。 (まだ未リリースっぽいです)
https://android-review.googlesource.com/q/project:platform/frameworks/support+viewtreelifecycleownerViewTreeLifecycleOwnerとは
ViewからLifecycleOwnerを取得できる仕組みです。
例えば以下のようにすることでLifecycleOwnerを取得できます。このLifecycleOwnerは直近のFragmentやActivityを取得できる形になります。
個人的にはCustomViewなどでライフサイクルを取得するのに便利なのではと思っています。view.findViewTreeLifecycleOwner()どのように実現されるのか?
ComponentActivity内
ルートのViewであるDecorViewに対してActivityをセットしています
@Override public void setContentView(@LayoutRes int layoutResID) { // Set the VTLO before setting the content view so that the inflation process // and attach listeners will see it already present ViewTreeLifecycleOwner.set(getWindow().getDecorView(), this); super.setContentView(layoutResID); }Fragment内
Fragmentでも同様にセットしています。
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ... ViewTreeLifecycleOwner.set(mView, mViewLifecycleOwner);ViewTreeLifecycleOwner
ViewTreeLifecycleOwnerではただ単純にタグに入れるだけです
public static void set(@NonNull View view, @Nullable LifecycleOwner lifecycleOwner) { view.setTag(R.id.view_tree_lifecycle_owner, lifecycleOwner); }どのようにLifecycleOwnerを取得するのか?
以下のように単純にparentをたどることで取得できるようです。public static LifecycleOwner get(@NonNull View view) { LifecycleOwner found = (LifecycleOwner) view.getTag(R.id.view_tree_lifecycle_owner); if (found != null) return found; ViewParent parent = view.getParent(); while (found == null && parent instanceof View) { final View parentView = (View) parent; found = (LifecycleOwner) parentView.getTag(R.id.view_tree_lifecycle_owner); parent = parentView.getParent(); } return found; } }
- 投稿日:2019-12-22T12:11:56+09:00
2019年人気アプリは?
令和元年となった2019年もそろそろおわり。来年はいよいよオリンピックイアー!
あっという間に月日がながれていきますが
スマホアプリにはどのような月日がながれているのでしょうか。
Android基準でここ10年ぐらいの人気アプリを振り返ってみます。2010年の年間アプリ人気ランキング
1位はホームアプリ「ADW.Launcher(donut)」
2位は設定管理アプリ「MySetting」
3位はタスク管理アプリ「Advanced Task Killer」ゲームアプリでは、
1位がアクションゲーム「Angry Birds」
2位がRPG「エルスの天秤(FREE)」
3位がRPG「魔王なんてたおしちゃうから!Lite」
4位がパズルゲーム「Tetris」
5位がアクションゲーム「侍魂(Samurai Spirits)」10年前は初代Xperiaが発売された年で
アプリもスマホの未完成度をおぎなうようなツールがメインですね。
自分もそのころからXperiaをつかっていますが、Android端末自体が不安定すぎて
電話着信すらままならない状態でガラケー派からは、なんでAndroidつかっているの?とよく批判されました。
ゲームも「Tetris」がベスト5にはいるなどまだパッとしないですね。AndroidOS:1.6
みんなで決めるベストアプリ総選挙2012
1位 パズル&ドラゴンズ
2位 漫画風製作所
3位 スマホ最適化のたしなみこのころにはスマホ利用が一般的になっておりiPhone派で大流行のアプリが周回遅れでAndroidにも登場するような流れとなっていました。
パズドラは2019年でも未だに根強いユーザーがのこっておりゲームの寿命の長さが伺えます。ちなみにこの頃次便はXperiaSXを使っており手のひらサイズのスマホが人気でした。
AndroidOSも4.1となり、すこしずつ動作も安定してきました。情報:アプリコ
2014年流行アプリランキング
1位 白猫プロジェクト
2位 メルカリ
3位 LINEツムツムFril(フリル)やスマートニュースも、このころで情報系アプリも人気でした。
ちなみにLNEは2013年のTOPAndroidOS 5.0
情報:アプリ情報研究所
2016年ベストアプリ
総合1位 AbemaTV
ゲーム1位 Pokémon GO
ベルトローカルアプリ UberEATS
ベストデザイン C CHANNELながらスマホを一般的にしたポケモンGoは3年前の出来事だったのですね。
Android 7.1となり主要なアプリをつかうにはAndoridもiPhoneどちらでも関係なくなってきたころかとおもいます。スマホが、巨大化してきたのもこのころですね。情報:Google Play
Google Play「ベストオブ 2019」
いよいよ今年はどうでしょうか?
ユーザー投票部門&生活お役立ち:PayPay
隠れた名作部門:スニーカーダンク - 人気・新作スニーカー情報アプリ
エンターテイメント:Mirrativ(ミラティブ)−スマホでかんたんゲーム配信&画面録画
ベスト5ゲーム:ドラゴンクエストウォーク個人的にはTikTokやYoutubeなど動画配信みるだけでは物足りなくなっており、自分も配信してみよっかなと日々かんがえているので、
Mirrativ(ミラティブ)−スマホでかんたんゲーム配信&画面録画
nana - 歌でつながる音楽コラボSNSなどの個人で配信する関連のアプリがランクインしているのことに共感できますね。。
来年は5Gもはじまるので、アプリの役割もおおきな変化があるかもしれませんね。AndroidOS 9.0
情報https://play.google.com/store/apps/editorial_collection/promotion_topic_bestof2019_apps_hub?hl=ja
- 投稿日:2019-12-22T11:42:01+09:00
Androidのライフサイクルの基礎からViewModel, LiveData�, Kotlin Coroutinesまでを流れるように説明したい
先日、Google Developers Expert for Androidになりました。
これからもよろしくおねがいいたします。
Androidの初心者がステップアップできるような記事を書いてみます。
なにかツッコミがあればコメントしてください?ライフサイクルについて少し学んだ後に、ViewModel, LiveData, Kotlin Coroutinesについて、ライフサイクルに関連する課題とその解決策という位置づけで話していきます。
1. ライフサイクルの基礎
ライフサイクルとは生き物の蝶でいうと生まれて、卵から幼虫になってさなぎになって、蝶になって死んでいく感じですが、AndroidのActivityやFragmentという表示を持つコンポーネントにもライフサイクルがあります。
どのようなライフサイクルの状態があるのか、どのようなライフサイクルのメソッドがあるのかを説明してきます。
なんとなくわかる方は2.の画面回転からどうぞ。Activityのライフサイクルの状態
Actiivtyのライフサイクルの変化で呼ばれるメソッドがあるので、それぞれのメソッドにログを入れて動作を確認してみましょう。
最近のActivityには今のライフサイクルの状態を確認できるgetLifecycle()
というメソッドが追加されており、それにより、ライフサイクルの状態を確認できます。class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d("MainActivity", "onCreate state:" + lifecycle.currentState) } override fun onStart() { super.onStart() Log.d("MainActivity", "onStart state:" + lifecycle.currentState) } override fun onResume() { super.onResume() Log.d("MainActivity", "onResume state:" + lifecycle.currentState) } override fun onPostResume() { // onPostResume()はonResumeの後に呼ばれます。 super.onPostResume() Log.d("MainActivity", "onPostResume state:" + lifecycle.currentState) } override fun onPause() { super.onPause() Log.d("MainActivity", "onPause state:"+lifecycle.currentState) } override fun onStop() { super.onStop() Log.d("MainActivity", "onStop state:"+lifecycle.currentState) } override fun onDestroy() { super.onDestroy() Log.d("MainActivity", "onDestroy state:"+lifecycle.currentState) } }アプリを起動すると以下のようなログが出ます。
onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMEDまずそれぞれの状態を説明しましょう。Activityには以下のような状態があります。
(codelabより https://codelabs.developers.google.com/codelabs/kotlin-android-training-lifecycles-logging/index.html?index=..%2F..android-kotlin-fundamentals#2 )まずActivityがインスタンス化されてすぐのデフォルトの状態では
state:INITIALIZED
状態であり、その後onCreate
の後にstate:CREATED
になり、onResume
後にstate:RESUMED
になります。このように状態が遷移していくことが、このログから観察できます。それぞれの状態について
INITIALIZED
Activityのインスタンスができているだけで画面などの用意はできていない状態。
CREATED
onCreate()によって画面などはできている(ActivityでViewが作られている)が、表示されてもおらず、フォーカスも持っていない状態。
(画面ができていると言っても表示するためのViewができているだけで、APIなどからデータを取得して表示するときには表示する内容がない場合もある。)どういうときにこの状態のままになるか?
→ Activityが別のActivityの裏にいて、画面が隠れているとき、この状態のままになる。STARTED
画面が表示されているが、フォーカスを持っていない状態
どういうときにこの状態のままになるか?
→ Activityが他の透過Activityの後ろに表示されているとき、マルチアクティビティで他のActivityにフォーカスがあたっている(選択されている)とき。RESUMED
画面がフォーカスを持っている状態。
どういうときにこの状態のままになるか?
→ 普通にActivityが最前面で表示されているとき。それぞれのメソッドについて
それぞれonCreate()、onStart()、onResume()について説明していきます。onPause()などのメソッドについてもこの中で触れます。
onCreate()
INITIALIZEDからCREATEDに変わるときに呼ばれる。
タイミング
Activityのインスタンスが作られたときに一度だけ呼び出されます。
逆にonDestroy()はActivityのインスタンスが破棄されるときに一度だけ呼ばれます。何をするか?
通常、ここでActivityで表示するViewを作成
setContentView(R.layout.activity_main)
を行ったりして、Activityで必要になるものを作っていきます。onStart()
CREATEDからSTARTEDに変わるときに呼ばれる。
タイミング
画面が表示されたタイミングで発火されます。
例えばMainActivityからDetailActivityに遷移していて、戻るボタンでMainActivity戻ってきたときには、MainActivityのインスタンスはメモリ不足で破棄されていなければそのまま使われるので、onCreateは呼ばれず、onStartから呼ばれることになります。
逆に画面が表示されなくなったらonStop()が呼び出されます。何をするか?
例えば動画プレイヤーであれば画面が表示されていないのに動画を再生するのは微妙ですよね?そういうときにonStart()で動画の再生を開始しておいて、onStopで動画を止めるようにすれば、うまく動作させることができます。
onResume()
STARTEDからRESUMEDに変わるときに呼ばれる。
タイミング
画面がフォーカスを持ったタイミングで発火されます。
Activityは透過させることができ、裏に以前に表示されていたActivityを表示させておくことができたりします。その場合に表示はされているのですが、フォーカスを持っていない状態になります。
また、Androidにはマルチウインドウという機能があり、それを使っている場合は画面に複数のActivityが表示されます。その場合にはフォーカスされている画面でだけフォーカスを持ちます。(Android 10から一部仕様が変わりました)
逆にフォーカスを失ったときにonPauseが呼ばれます。
onPostResume()はonResumeの後に呼ばれます。何をするか?
ログ計測などで使われたりはよくありますが、そこまで頻繁には使われるメソッドではないです。フォーカスがあたったときに更新したいなどは行うことができます。
2. 画面回転とViewModel
Androidのライフサイクルの難しいところと言われる画面回転について記述しておきます。
Androidでは画面回転するとActivityのインスタンスが作り直されます。アプリの起動起動 onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMED --- ここで画面回転開始 onPause state:STARTED onStop state:CREATED onDestroy state:DESTROYED ← 一度Activityのインスタンスが作り直される onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMED画面回転すると何が困るのか?
こんな感じのTextViewがあったとしましょう。
<TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:textSize="100sp" />これをクリックで+1ずつしていくコードを書いたとしましょう。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textView = findViewById<TextView>(R.id.text) textView.setOnClickListener { textView.text = (textView.text.toString().toInt() + 1).toString() }画面回転で初期化されます?
Activityにデータを保持しておくとActivityのインスタンスごと作り直されるので、このような現象が起きます。
(FragmentというActivity内の部品を表すコンポーネントがあり、このFragmentで画面回転での破棄を無効にするオプションがあるのですが、deprecatedになりました)onSaveInstanceState()を使った解決策
savedInstanceState()はonStop()の前に呼び出されるメソッドです。
savedInstanceStateはActivityが破棄されるときにデータを保存しておいて、取り出すことができます。
具体的にはonSaveInstanceStateでbundleにデータを入れ、onCreateの引数のsavedInstanceStateで取り出す形になります。
savedInstanceStateは画面回転だけでなく、アプリケーションのプロセスが破棄されても、システムサービスでデータが保持されているため、かなり強力な解決策です。class MainActivity : AppCompatActivity() { lateinit var textView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById<TextView>(R.id.text) textView.setOnClickListener { textView.text = (textView.text.toString().toInt() + 1).toString() } if (savedInstanceState != null) { textView.text = savedInstanceState.getString("count") } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("count", textView.text.toString()) }ただ、この方法にもデメリットがあります。
大きすぎるデータを保存するとTransactionTooLargeException
になってしまうため、ユーザーのリストや画像などには適していません。
また1つ1つデータを保存していくのは結構骨が折れます。ViewModelを使った解決策
ViewModelはArchitecture Componentと言われるライブラリの一部で、このデータの保持を楽にしてくれるコンポーネントです。
よくJetpackやAndroidXという言葉が出てくると思うので、以下に自分の理解している構造を書いておきます。つまりAndroid Jetpackの中にあるAndroidXの中にあるArchitecture Componentの中にあるViewModel。。です。(間違っていたら教えて下さい)
ViewModelはActivityと違って画面回転を通じて生き残ります。つまり、いちいち保存したりしなくてもViewModelでデータを保持させることでうまく動作させることができます。
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=en よりViewModelはViewModelProviderから取得しており、これにより画面回転してアクセスする元のActivityのインスタンスが変わっても同じViewModelのインスタンスを取得することができます。
class MainViewModel : ViewModel() { var count: Int = 0 } class MainActivity : AppCompatActivity() { lateinit var textView: TextView lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById<TextView>(R.id.text) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) textView.text = viewModel.count.toString() textView.setOnClickListener { viewModel.count++ textView.text = viewModel.count.toString() } }画面回転を克服して実装していくことができました
![]()
3. ViewModelとLiveData
もうViewModelを使えばAndroidアプリの実装は完璧でしょうか?
カウンターを作る人は多くないと思うので試しにもう少し実用的な例でやっていきましょう。
Androidのアプリではサーバーからデータを取得して表示するようなアプリはかなりよくあるパターンです。例えば以下のようなAPIのクラスがあったとしましょう。実際は通信して、データを取ってくる形になりますが、ここではスレッドを作ってコールバックでデータを返すだけにします。
class Api { fun fetch(url: String, onFetched: (response: String) -> Unit) { thread { // simulate api call Thread.sleep(5000) onFetched("$url:fetched") } } }そしてViewModelではViewModelで取得したときに、Activityでデータを受け取れるようにしたとしましょう(※ダメな例なので真似して実装しないでください。)
class MainViewModel : ViewModel() { val api = Api() var response: String? = null init { fetch() } private var onResponseChangedListeners: List<((response: String) -> Unit)> = mutableListOf() fun addOnResponseChangedListener(onResponseChangedListener: (response: String) -> Unit) { this.onResponseChangedListeners += onResponseChangedListener } private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> this.response = response this.onResponseChangedListeners.forEach { it(response) } } } } class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) val textView = findViewById<TextView>(R.id.text) textView.text = viewModel.response viewModel.addOnResponseChangedListener { response -> runOnUiThread { textView.text = response } } } }一見問題なく動くように見えますがこのコードにはいくつかの問題点があります。
問題1. Activityがメモリリークしている
ViewModelの生存期間はActivityより長いので、画面回転すると
onResponseChangedListeners
で保持されているので、Activityのインスタンスがリークします。ActivityのインスタンスがリークするとActivityは画面全体を保持しているので、メモリが枯渇するようになります。↓でActivityのインスタンスが暗黙的に渡されています。
viewModel.addOnResponseChangedListener { response ->デバッガーでJava heapをdumpしてActivity/Fragment Leaksにチェックを入れるとリークしているインスタンスを教えてくれます。
一応この問題に関しては、ActivityのonDestroy()でonResponseChangedListenersのlistenerを消すようにすれば一応うまく動きます。
問題2. UIへのデータの反映忘れが起こりやすい
addOnResponseChangedListenerではfetch()が終わったときしか反映されないので、画面回転後にActivityが再生成された後にはTextViewに値が反映されません。
val textView = findViewById<TextView>(R.id.text) // **↓の行を忘れても一応動くが画面回転でtextが表示されなくなる** textView.text = viewModel.response // ** ↑ ** viewModel.addOnResponseChangedListener { response -> runOnUiThread { textView.text = response } }問題3. onStop以降でも
addOnResponseChangedListener
のコールバックが呼ばれるAPIの応答に5秒かかっている間に普通にホームボタンが押されたりする可能性があります。そのときにActivityはCREATED状態になります。
AndroidではonStopとonDestoryの間、CREATED状態の時に呼び出すとクラッシュするAndroidのFrameworkのAPIが存在します。(幸い、TextViewへのsetでは大丈夫です。)例えばこのタイミングでFragmentをレイアウトに追加する(FragmentTransactionのcommit()など)とクラッシュが発生します。
これを対策するには今の状態をみて、次のonStart以降で処理を動かすなどかなり工夫が必要になります。![]()
昔は以下のような処理をキューにためておいて、onResume以降で処理するなどを頑張ってして、なんとかしていました。
https://stackoverflow.com/a/8122789/4339442LiveDataを使った解決策
自分で上記の問題をそれぞれ対応していくのはかなかな大変です。
そこでArchitecture ComponentのLiveDataは上記の問題を解決するものになります。LiveDataはobserve(観測)できるAndroidのライフサイクルを考慮したデータホルダーとなります。
MutableLiveDataは変更可能なLiveDataでsetValue()
やpostValue()
を呼ぶことで変更することができます。setValue
はMainThread
で値を入れるとき、postValue
はMainThread
以外から呼ばれるときに利用します。
MutableLiveData
の親クラスにLiveDataクラスがあります。これはデータの変更をobserve()
を呼ぶことで観測することができます。
以下の例ではfetch()でデータを_responseにデータをセットして、MainActivity内でデータをobserve()
することで反映しています。class MainViewModel : ViewModel() { val api = Api() private val _response: MutableLiveData<String> = MutableLiveData() val response: LiveData<String> get() = _response init { fetch() } private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> _response.postValue(response) } } } class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) val textView = findViewById<TextView>(R.id.text) viewModel.response.observe(this, Observer { response -> textView.text = response }) } }実際にどのように上記の問題を解決しているのか見ていきましょう。
- 問題1. LiveDataが"Activityがメモリリークしている"に対してどうやって対応しているか?
observeメソッドに
this
を渡しています。ライフサイクルを保持するActivityやFragmentはLifecycleOwner
というinterfaceを実装しています。LiveDataはライフサイクルがDESTROYED に変わったときにオブザーバーを削除してくれます。そのため、メモリリークが発生しません。viewModel.response.observe(this, Observer { response -> textView.text = response })
- 問題2. LiveDataが"UIへのデータの反映忘れが起こりやすい"に対してどうやって対応しているか?
LiveDataをobserve()したとき、ライフサイクルがonStart以降になったときに、オブザーバーを呼び出してくれます。
そのため画面回転後にonCreate()が呼ばれ、そこでobserve()
した場合に、ちゃんと呼び出してくれるのでデータを反映することができます。
そのため、データが変更されていなくても呼び出されるので、onCreateメソッドの中でViewModelが保持しているLiveDataのデータを直接見ることなしに、UIを変更していくことができます。
- 問題3. LiveDataが"onStop以降でも
addOnResponseChangedListener
のコールバックが呼ばれる"に対してどうやって対応しているか?observeしたときに渡すActivityやFragment(
LifecycleOwner
)のライフサイクルがSTARTED以降でないと変更を呼び出さないので、問題ありません。他にもLiveDataはいくつかの問題を解決してくれます。Android Developerを確認してみましょう。
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja4. ViewModelとKotlin Coroutiens
ここまでで、一応いい感じに動くアプリができました。
ただ、まだ残念ながら少し問題は残っています。問題1. ViewModelのリーク
以下を実行したときに、スレッドによってコールバックが保持されてしまっています。もしコールバックがずっと呼び出されなければ、ViewModelのインスタンスが破棄されなかったり、ViewModelはもう使われないのに不要に呼び出されてしまったりします。
private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> _response.postValue(response) } }問題2. コールバック地獄
DBから読み出して、なければAPIから取得したい場合はどうでしょうか?行おうとすると以下のようになります。このようにコールバックが深くなっていくことをコールバックヘルといいます。この場合はそんなにはわかりにくくないですが、実際にはもっと複雑になっていきます。 (本来このような処理はRepositoryなどに分離するのが普通ですが、分離してもこの問題は残ります。)
class MainViewModel : ViewModel() { val db = Db() val api = Api() private val _contents: MutableLiveData<String> = MutableLiveData() val contents: LiveData<String> get() = _contents init { fetch() } private fun fetch() { db.read { contents: String? -> if(contents != null) { _contents.postValue(contents) return@read } api.fetch("http://api.example.com/hogehoge") { response -> _contents.postValue(response) } } } }問題3. メインスレッドかどうかを気にしたプログラミングが必要となる
LiveDataはメインスレッド以外で値をセットするにはpostValueを使う必要があります。コールバックなどで今はメインスレッドか?などを気にしながらプログラミングしていく必要があります。
private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> // **このコールバック内はメインスレッドではないため、 // ここでsetValue()ではなく、postValue()を使わないとクラッシュする** _response.postValue(response) } }Kotlin Corotuinesを使った解決策
Coroutinesは非同期処理のデザインパターンで、Kotlinに実装されたものがKotlin Coroutinesです。
AndroidはFirst Class Coroutines Supportしています。
https://www.youtube.com/watch?v=BOHK_w09pVA
これにより上記の問題が解決できます。
Coroutinesのコードを理解するにはいくつか理解しなくてはいけない概念が存在します。
Coroutinesの中断と再開
コルーチンを使うと、
launch{}
の中で以下のようにメインスレッドを使うコードとAPIの呼び出しをするようなコードを混ぜて書くことができます。launch { progress.isVisible = true val result = api.fetch() progress.isVisible = false show(result) }Androidのメインスレッドで普通に上記のようなコードを書くと、メインスレッドを通信中にブロックしてアプリがタップしても何をしても反応しなくなり、フリーズ状態になります。Application Not Responding(ANR)が発生します。
適切に実装されたコルーチンのメソッドであれば、このコードでANRなどの問題は起こりません。
なぜならKotlin Coroutinesには中断、再開という概念があるからです。
具体的にはこのapi.fetch()を呼んだときにCorotuinesを中断
状態に入り、中断に入っている間は他のタップしたときの反応などメインスレッドを使う処理を実行させることができ、fetch()が終わったときにまたこのapi.fetch()
の次の行に戻ってきて、メインスレッドで処理の続きをできる、再開
できます。またCoroutinesを
launch()
するとJobのインスタンスが取得でき、cancel()を呼ぶことで、途中で処理を止めることができます。val job = launch { progress.isVisible = true val result = api.fetch() progress.isVisible = false show(result) } // 不要になったらキャンセルする job.cancel()Coroutines Scope
先程のコードの例は少し間違っており、実際はコルーチンスコープがないとコルーチンは
launch()
メソッドを呼ぶことができません。
Kotlin Corotuinesは構造化することができ、親のJobをCoroutiensScopeに渡して作成し、CoroutineScopeのcancelを呼ぶことで、子のCoroutinesを全てキャンセルしていくことができます。val scope = CoroutineScope(Job()) scope.launch { ... } scope.launch { ... } // 不要になったらcancelする scope.cancel()Coroutines Dispatcher
実際どのスレッドで処理が実行されるのかが気になると思います。
以下のように書くことで途中でスレッドを切り替えて処理することができます。Dispatchers.MAIN = メインスレッド(AndroidではUIを触る)
Dispatchers.IO = I/O関連を処理するためのスレッドが利用される
Dispatchers.DEFAULT = それ以外の計算系に利用されるscope.launch { progress.isVisible = true val result = withContext(Dispatchers.IO){ URL("").openConnection().getInputStream()... ... } progress.isVisible = false show(result) }これをただメソッドに分けて書くと以下のようになります。このsuspend functionとは、中断可能なメソッドという意味です。勘違いしてほしくないのが、このsuspendを使ったからといって勝手にバックグラウンドスレッドになったりしないということです。
val scope = CoroutineScope(Job()) scope.launch { progress.isVisible = true val result = fetchApi() progress.isVisible = false } } private suspend fun fetchApi(): String { return withContext(Dispatchers.IO) { URL("").openConnection().getInputStream()... ... } }Kotlin Corotuinesを使った解決策のコード
これを利用したKotlin Corotuinesを使った解決策では以下のようになります。
viewmodel-ktx 2.1.0を使うとviewModelScopeというものが用意されており、これを使うことで、ViewModelが破棄されるときにCorotuinesScopeをキャンセルすることができます。class Db { suspend fun read(): String? { return withContext(Dispatchers.IO) { // simulate db read delay(5000) null } } } class Api { suspend fun fetch(url: String): String { return withContext(Dispatchers.IO) { // simulate api call delay(5000) "$url:fetched" } } } class MainViewModel : ViewModel() { val db = Db() val api = Api() private val _contents: MutableLiveData<String> = MutableLiveData() val contents: LiveData<String> get() = _contents init { fetch() } private fun fetch() { viewModelScope.launch { val contents = db.read() if (contents != null) { _contents.postValue(contents) return@launch } val response = api.fetch("http://api.example.com/hogehoge") _contents.value = response } } }このコードがどのように問題を解決するのかを見ていきましょう。
Kotlin Corotuiensが"問題1. ViewModelのリーク"に対してどうやって対応しているか?
viewModelScope
がキャンセルされることによって、コルーチンがキャンセルされるので、問題なく動作します。Kotlin Corotuiensが"問題2. コールバック地獄"に対してどうやって対応しているか?
Kotlin Coroutinesの中断、再開によってコールバックなしに非同期処理をコーディングしていくことができます。Kotlin Corotuiensが"問題3. メインスレッドかどうかを気にしたプログラミングが必要となる"に対してどうやって対応しているか?
viewModelScope
はメインスレッドで実行され、明示的に切り替えなければ基本的にメインスレッドで行われるため、APIコールの後であっても今のスレッドを気にせずにコーディングしていくことができます。実際どのようにKotlin CoroutinesでAPIやDBを呼び出すメソッドを実装していったら良いのか?
APIやDB呼び出しのコードがdelay()などを使ったデモコードになっておりわかりにくかったと思います。
実際にはRetrofitやRoomはsuspend functionに対応しているので、自動的にsuspend functionを定義しておくことで実装を生成してくれるため、問題なく実装できます。
またもしそのような方法が提供されていなくても以下のようにsuspendCancellableCoroutine
を利用することで問題なく実装できます。suspend fun fetch(): String { return suspendCancellableCoroutine<String> { cancellableContinuation -> myApi.fetch( // 自分で用意したAPI onSuccess = { result: String -> cancellableContinuation.resume(result) }, onFailure = { e: Throwable -> cancellableContinuation.resumeWithException(e) } ) cancellableContinuation.invokeOnCancellation { // キャンセル処理 myApi.cancelFetch() } } }またはブロッキングして取得する方法があるのであれば以下のような方法も使えます。(CoroutinesはキャンセルしたときにThreadをintrerrupedしないので処理が途中でキャンセルされないので注意が必要です)
suspend fun fetch(): String { return withContext(Dispatchers.IO) { api.blockingFetch() } }まとめ
Androidのライフサイクルの基礎的なところから、画面回転の問題をViewModelで解決し、データを監視する問題をLiveDataで解決、非同期処理の問題をKotlin Coroutinesで解決していくことができました。
実際にAndroid Developerには以下のようにそれぞれの問題について書かれています。それをつないで書いてみたのが今回の記事になります。そのため、部分的にわかりにくい部分があれば、以下を参照していただけると結構わかっていくのではないかと思います。ViewModel
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
LiveData
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
Coroutines
https://developer.android.com/kotlin/coroutines
- 投稿日:2019-12-22T11:42:01+09:00
Androidのライフサイクルの基礎からViewModel, LiveData, Kotlin Coroutinesまでを流れるように説明したい
先日、Google Developer Expert for Androidになりました。
これからもよろしくおねがいいたします。
Androidの初心者がステップアップできるような記事を書いてみます。
なにかツッコミがあればコメントしてください?ライフサイクルについて少し学んだ後に、ViewModel, LiveData, Kotlin Coroutinesについて、ライフサイクルに関連する課題とその解決策という位置づけで話していきます。
1. ライフサイクルの基礎
ライフサイクルとは生き物の蝶でいうと生まれて、卵から幼虫になってさなぎになって、蝶になって死んでいく感じですが、AndroidのActivityやFragmentという表示を持つコンポーネントにもライフサイクルがあります。
どのようなライフサイクルの状態があるのか、どのようなライフサイクルのメソッドがあるのかを説明していきます。
なんとなくわかる方は2.の画面回転からどうぞ。Activityのライフサイクルの状態
Actiivtyのライフサイクルの変化で呼ばれるメソッドがあるので、それぞれのメソッドにログを入れて動作を確認してみましょう。
最近使われるActivity(AppCompatActivity)には今のライフサイクルの状態を確認できるgetLifecycle()
というメソッドが追加されており、それにより、ライフサイクルの状態を確認できます。class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d("MainActivity", "onCreate state:" + lifecycle.currentState) } override fun onStart() { super.onStart() Log.d("MainActivity", "onStart state:" + lifecycle.currentState) } override fun onResume() { super.onResume() Log.d("MainActivity", "onResume state:" + lifecycle.currentState) } override fun onPostResume() { // onPostResume()はonResumeの後に呼ばれます。 super.onPostResume() Log.d("MainActivity", "onPostResume state:" + lifecycle.currentState) } override fun onPause() { super.onPause() Log.d("MainActivity", "onPause state:"+lifecycle.currentState) } override fun onStop() { super.onStop() Log.d("MainActivity", "onStop state:"+lifecycle.currentState) } override fun onDestroy() { super.onDestroy() Log.d("MainActivity", "onDestroy state:"+lifecycle.currentState) } }アプリを起動すると以下のようなログが出ます。
onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMEDまずそれぞれの状態を説明しましょう。Activityには以下のような状態があります。
(codelabより https://codelabs.developers.google.com/codelabs/kotlin-android-training-lifecycles-logging/index.html?index=..%2F..android-kotlin-fundamentals#2 )まずActivityがインスタンス化されてすぐのデフォルトの状態では
state:INITIALIZED
状態であり、その後onCreate
の後にstate:CREATED
になり、onStart
の後にstate:STARTED
、onResume
後にstate:RESUMED
になります。このように状態が遷移していくことが、このログから観察できます。それぞれの状態について
INITIALIZED
Activityのインスタンスができているだけで画面などの用意はできていない状態。
CREATED
onCreate()によって画面などはできている(ActivityでViewが作られている)が、表示されてもおらず、フォーカスも持っていない状態。
(画面ができていると言っても表示するためのViewができているだけで、APIなどからデータを取得して表示するときには表示する内容がない場合もある。)どういうときにこの状態のままになるか?
→ Activityが別のActivityの裏にいて、画面が隠れているとき、この状態のままになる。STARTED
画面が表示されているが、フォーカスを持っていない状態
どういうときにこの状態のままになるか?
→ Activityが他の透過Activityの後ろに表示されているとき、マルチアクティビティで他のActivityにフォーカスがあたっている(選択されている)とき。RESUMED
画面がフォーカスを持っている状態。
どういうときにこの状態のままになるか?
→ 普通にActivityが最前面で表示されているとき。それぞれのメソッドについて
それぞれonCreate()、onStart()、onResume()について説明していきます。onPause()などのメソッドについてもこの中で触れます。
onCreate()
INITIALIZEDからCREATEDに変わるときに呼ばれる。
タイミング
Activityのインスタンスが作られたときに一度だけ呼び出されます。
逆にonDestroy()はActivityのインスタンスが破棄されるときに一度だけ呼ばれます。何をするか?
通常、ここでActivityで表示するViewを作成
setContentView(R.layout.activity_main)
を行ったりして、Activityで必要になるものを作っていきます。onStart()
CREATEDからSTARTEDに変わるときに呼ばれる。
タイミング
画面が表示されたタイミングで発火されます。
例えばMainActivityからDetailActivityに遷移していて、戻るボタンでMainActivity戻ってきたときには、MainActivityのインスタンスはメモリ不足で破棄されていなければそのまま使われるので、onCreateは呼ばれず、onStartから呼ばれることになります。
逆に画面が表示されなくなったらonStop()が呼び出されます。何をするか?
例えば動画プレイヤーであれば画面が表示されていないのに動画を再生するのは微妙ですよね?そういうときにonStart()で動画の再生を開始しておいて、onStopで動画を止めるようにすれば、うまく動作させることができます。
onResume()
STARTEDからRESUMEDに変わるときに呼ばれる。
タイミング
画面がフォーカスを持ったタイミングで発火されます。
Activityは透過させることができ、裏に以前に表示されていたActivityを表示させておくことができたりします。その場合に表示はされているのですが、フォーカスを持っていない状態になります。
また、Androidにはマルチウインドウという機能があり、それを使っている場合は画面に複数のActivityが表示されます。その場合にはフォーカスされている画面でだけフォーカスを持ちます。(Android 10から一部仕様が変わりました)
逆にフォーカスを失ったときにonPauseが呼ばれます。
onPostResume()はonResumeの後に呼ばれます。何をするか?
ログ計測などで使われたりはよくありますが、そこまで頻繁には使われるメソッドではないです。フォーカスがあたったときに更新したいなどは行うことができます。
2. 画面回転とViewModel
Androidのライフサイクルの難しいところと言われる画面回転について記述しておきます。
Androidでは画面回転するとActivityのインスタンスが作り直されます。アプリの起動起動 onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMED --- ここで画面回転開始 onPause state:STARTED onStop state:CREATED onDestroy state:DESTROYED ← 一度Activityのインスタンスが作り直される onCreate state:INITIALIZED onStart state:CREATED onResume state:STARTED onPostResume state:RESUMED画面回転すると何が困るのか?
こんな感じのTextViewがあったとしましょう。
<TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:textSize="100sp" />これをクリックで+1ずつしていくコードを書いたとしましょう。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textView = findViewById<TextView>(R.id.text) textView.setOnClickListener { textView.text = (textView.text.toString().toInt() + 1).toString() }画面回転で初期化されます?
Activityにデータを保持しておくとActivityのインスタンスごと作り直されるので、このような現象が起きます。
(FragmentというActivity内の部品を表すコンポーネントがあり、このFragmentで画面回転での破棄を無効にするオプションがあるのですが、deprecatedになりました)onSaveInstanceState()を使った解決策
savedInstanceState()はonStop()の前に呼び出されるメソッドです。
savedInstanceStateはActivityが破棄されるときにデータを保存しておいて、取り出すことができます。
具体的にはonSaveInstanceStateでbundleにデータを入れ、onCreateの引数のsavedInstanceStateで取り出す形になります。
savedInstanceStateは画面回転だけでなく、アプリケーションのプロセスが破棄されても、システムサービスでデータが保持されているため、かなり強力な解決策です。class MainActivity : AppCompatActivity() { lateinit var textView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById<TextView>(R.id.text) textView.setOnClickListener { textView.text = (textView.text.toString().toInt() + 1).toString() } if (savedInstanceState != null) { textView.text = savedInstanceState.getString("count") } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("count", textView.text.toString()) }ただ、この方法にもデメリットがあります。
大きすぎるデータを保存するとTransactionTooLargeException
になってしまうため、ユーザーのリストや画像などには適していません。
また1つ1つデータを保存していくのは結構骨が折れます。ViewModelを使った解決策
ViewModelはArchitecture Componentと言われるライブラリの一部で、このデータの保持を楽にしてくれるコンポーネントです。
よくJetpackやAndroidXという言葉が出てくると思うので、以下に自分の理解している構造を書いておきます。つまりAndroid Jetpackの中にあるAndroidXの中にあるArchitecture Componentの中にあるViewModel。。です。(間違っていたら教えて下さい)
ViewModelはActivityと違って画面回転を通じて生き残ります。つまり、いちいち保存したりしなくてもViewModelでデータを保持させることでうまく動作させることができます。
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=en よりViewModelはViewModelProviderから取得しており、これにより画面回転してアクセスする元のActivityのインスタンスが変わっても同じViewModelのインスタンスを取得することができます。
class MainViewModel : ViewModel() { var count: Int = 0 } class MainActivity : AppCompatActivity() { lateinit var textView: TextView lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById<TextView>(R.id.text) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) textView.text = viewModel.count.toString() textView.setOnClickListener { viewModel.count++ textView.text = viewModel.count.toString() } }画面回転を克服して実装していくことができました
![]()
3. ViewModelとLiveData
もうViewModelを使えばAndroidアプリの実装は完璧でしょうか?
カウンターを作る人は多くないと思うので試しにもう少し実用的な例でやっていきましょう。
Androidのアプリではサーバーからデータを取得して表示するようなアプリはかなりよくあるパターンです。例えば以下のようなAPIからデータを取得するクラスがあったとしましょう。実際は通信して、データを取ってくる形になりますが、ここではスレッドを作ってコールバックでデータを返すだけにします。
class Api { fun fetch(url: String, onFetched: (response: String) -> Unit) { thread { // simulate api call Thread.sleep(5000) onFetched("$url:fetched") } } }そしてViewModelではViewModelで取得したときに、Activityでデータを受け取れるようにしたとしましょう(※ダメな例なので真似して実装しないでください。)
class MainViewModel : ViewModel() { val api = Api() var response: String? = null init { fetch() } private var onResponseChangedListeners: List<((response: String) -> Unit)> = mutableListOf() fun addOnResponseChangedListener(onResponseChangedListener: (response: String) -> Unit) { this.onResponseChangedListeners += onResponseChangedListener } private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> this.response = response this.onResponseChangedListeners.forEach { it(response) } } } } class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) val textView = findViewById<TextView>(R.id.text) textView.text = viewModel.response viewModel.addOnResponseChangedListener { response -> runOnUiThread { textView.text = response } } } }一見問題なく動くように見えますがこのコードにはいくつかの問題点があります。
問題1. Activityがメモリリークしている
ViewModelの生存期間はActivityより長いので、画面回転すると
onResponseChangedListeners
で保持されているので、Activityのインスタンスがリークします。ActivityのインスタンスがリークするとActivityは画面全体を保持しているので、メモリが枯渇するようになります。↓でActivityのインスタンスが暗黙的に渡されています。
viewModel.addOnResponseChangedListener { response ->デバッガーでJava heapをdumpしてActivity/Fragment Leaksにチェックを入れるとリークしているインスタンスを教えてくれます。
一応この問題に関しては、ActivityのonDestroy()でonResponseChangedListenersのlistenerを消すようにすれば一応うまく動きます。
問題2. UIへのデータの反映忘れが起こりやすい
addOnResponseChangedListenerではfetch()が終わったときしか反映されないので、画面回転後にActivityが再生成された後にはTextViewに値が反映されません。
val textView = findViewById<TextView>(R.id.text) // **↓の行を忘れても一応動くが画面回転でtextが表示されなくなる** textView.text = viewModel.response // ** ↑ ** viewModel.addOnResponseChangedListener { response -> runOnUiThread { textView.text = response } }問題3. onStop以降でも
addOnResponseChangedListener
のコールバックが呼ばれるAPIの応答に5秒かかっている間に普通にホームボタンが押されたりする可能性があります。そのときにActivityはCREATED状態になります。
AndroidではonStopとonDestoryの間、CREATED状態の時に呼び出すとクラッシュするAndroidのFrameworkのAPIが存在します。(幸い、TextViewへのsetでは大丈夫です。)例えばこのタイミングでFragmentをレイアウトに追加する(FragmentTransactionのcommit()など)とクラッシュが発生します。
これを対策するには今の状態をみて、次のonStart以降で処理を動かすなどかなり工夫が必要になります。![]()
昔は以下のような処理をキューにためておいて、onResume以降で処理するなどを頑張ってして、なんとかしていました。
https://stackoverflow.com/a/8122789/4339442LiveDataを使った解決策
自分で上記の問題をそれぞれ対応していくのはかなかな大変です。
そこでArchitecture ComponentのLiveDataは上記の問題を解決するものになります。LiveDataはobserve(観測)できるAndroidのライフサイクルを考慮したデータホルダーとなります。
MutableLiveDataは変更可能なLiveDataでsetValue()
やpostValue()
を呼ぶことで変更することができます。setValue
はMainThread
で値を入れるとき、postValue
はMainThread
以外から呼ばれるときに利用します。
MutableLiveData
の親クラスにLiveDataクラスがあります。これはデータの変更をobserve()
を呼ぶことで観測することができます。
以下の例ではfetch()でデータを_responseにデータをセットして、MainActivity内でデータをobserve()
することで反映しています。class MainViewModel : ViewModel() { val api = Api() private val _response: MutableLiveData<String> = MutableLiveData() val response: LiveData<String> get() = _response init { fetch() } private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> _response.postValue(response) } } } class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) val textView = findViewById<TextView>(R.id.text) viewModel.response.observe(this, Observer { response -> textView.text = response }) } }実際にどのように上記の問題を解決しているのか見ていきましょう。
- 問題1. LiveDataが"Activityがメモリリークしている"に対してどうやって対応しているか?
observeメソッドに
this
を渡しています。ライフサイクルを保持するActivityやFragmentはLifecycleOwner
というinterfaceを実装しています。LiveDataはライフサイクルがDESTROYED に変わったときにオブザーバーを削除してくれます。そのため、メモリリークが発生しません。viewModel.response.observe(this, Observer { response -> textView.text = response })
- 問題2. LiveDataが"UIへのデータの反映忘れが起こりやすい"に対してどうやって対応しているか?
LiveDataをobserve()したとき、ライフサイクルがonStart以降になったときに、オブザーバーを呼び出してくれます。
そのため画面回転後にonCreate()が呼ばれ、そこでobserve()
した場合に、ちゃんと呼び出してくれるのでデータを反映することができます。
そのため、データが変更されていなくても呼び出されるので、onCreateメソッドの中でViewModelが保持しているLiveDataのデータを直接見ることなしに、UIを変更していくことができます。
- 問題3. LiveDataが"onStop以降でも
addOnResponseChangedListener
のコールバックが呼ばれる"に対してどうやって対応しているか?observeしたときに渡すActivityやFragment(
LifecycleOwner
)のライフサイクルがSTARTED以降でないと変更を呼び出さないので、問題ありません。他にもLiveDataはいくつかの問題を解決してくれます。Android Developerを確認してみましょう。
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja4. ViewModelとKotlin Coroutiens
ここまでで、一応いい感じに動くアプリができました。
ただ、まだ残念ながら少し問題は残っています。問題1. ViewModelのリーク
以下を実行したときに、スレッドによってコールバックが保持されてしまっています。もしコールバックがずっと呼び出されなければ、ViewModelのインスタンスが破棄されなかったり、ViewModelはもう使われないのに不要に呼び出されてしまったりします。
private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> _response.postValue(response) } }問題2. コールバック地獄
DBから読み出して、なければAPIから取得したい場合はどうでしょうか?行おうとすると以下のようになります。このようにコールバックが深くなっていくことをコールバックヘルといいます。この場合はそんなにはわかりにくくないですが、実際にはもっと複雑になっていきます。 (本来このような処理はRepositoryなどに分離するのが普通ですが、分離してもこの問題は残ります。)
class MainViewModel : ViewModel() { val db = Db() val api = Api() private val _contents: MutableLiveData<String> = MutableLiveData() val contents: LiveData<String> get() = _contents init { fetch() } private fun fetch() { db.read { contents: String? -> if(contents != null) { _contents.postValue(contents) return@read } api.fetch("http://api.example.com/hogehoge") { response -> _contents.postValue(response) } } } }問題3. メインスレッドかどうかを気にしたプログラミングが必要となる
LiveDataはメインスレッド以外で値をセットするにはpostValueを使う必要があります。コールバックなどで今はメインスレッドか?などを気にしながらプログラミングしていく必要があります。
private fun fetch() { api.fetch("http://api.example.com/hogehoge") { response -> // **このコールバック内はメインスレッドではないため、 // ここでsetValue()ではなく、postValue()を使わないとクラッシュする** _response.postValue(response) } }Kotlin Corotuinesを使った解決策
Coroutinesは非同期処理のデザインパターンで、Kotlinに実装されたものがKotlin Coroutinesです。
AndroidはFirst Class Coroutines Supportしています。
https://www.youtube.com/watch?v=BOHK_w09pVA
これにより上記の問題が解決できます。
Coroutinesのコードを理解するにはいくつか理解しなくてはいけない概念が存在します。
Coroutinesの中断と再開
コルーチンを使うと、
launch{}
の中で以下のようにメインスレッドを使うコードとAPIの呼び出しをするようなコードを混ぜて書くことができます。launch { progress.isVisible = true val result = api.fetch() progress.isVisible = false show(result) }Androidのメインスレッドで普通に上記のようなコードを書くと、メインスレッドを通信中にブロックしてアプリがタップしても何をしても反応しなくなり、フリーズ状態になります。Application Not Responding(ANR)が発生します。
適切に実装されたコルーチンのメソッドであれば、このコードでANRなどの問題は起こりません。
なぜならKotlin Coroutinesには中断、再開という概念があるからです。
具体的にはこのapi.fetch()を呼んだときにCorotuinesを中断
状態に入り、中断に入っている間は他のタップしたときの反応などメインスレッドを使う処理を実行させることができ、fetch()が終わったときにまたこのapi.fetch()
の次の行に戻ってきて、メインスレッドで処理の続きをできる、再開
できます。またCoroutinesを
launch()
するとJobのインスタンスが取得でき、cancel()を呼ぶことで、途中で処理を止めることができます。val job = launch { progress.isVisible = true val result = api.fetch() progress.isVisible = false show(result) } // 不要になったらキャンセルする job.cancel()Coroutines Scope
先程のコードの例は少し間違っており、実際はコルーチンスコープがないとコルーチンは
launch()
メソッドを呼ぶことができません。
Kotlin Corotuinesは構造化することができ、親のJobをCoroutiensScopeに渡して作成し、CoroutineScopeのcancelを呼ぶことで、子のCoroutinesを全てキャンセルしていくことができます。val scope = CoroutineScope(Job()) scope.launch { ... } scope.launch { ... } // 不要になったらcancelする scope.cancel()Coroutines Dispatcher
実際どのスレッドで処理が実行されるのかが気になると思います。
以下のように書くことで途中でスレッドを切り替えて処理することができます。Dispatchers.MAIN = メインスレッド(AndroidではUIを触る)
Dispatchers.IO = I/O関連を処理するためのスレッドが利用される
Dispatchers.DEFAULT = それ以外の計算系に利用されるscope.launch { progress.isVisible = true val result = withContext(Dispatchers.IO){ URL("").openConnection().getInputStream()... ... } progress.isVisible = false show(result) }これをただメソッドに分けて書くと以下のようになります。このsuspend functionとは、中断可能なメソッドという意味です。勘違いしてほしくないのが、このsuspendを使ったからといって勝手にバックグラウンドスレッドになったりしないということです。
val scope = CoroutineScope(Job()) scope.launch { progress.isVisible = true val result = fetchApi() progress.isVisible = false } } private suspend fun fetchApi(): String { return withContext(Dispatchers.IO) { URL("").openConnection().getInputStream()... ... } }Kotlin Corotuinesを使った解決策のコード
これを利用したKotlin Corotuinesを使った解決策では以下のようになります。
viewmodel-ktx 2.1.0を使うとviewModelScopeというものが用意されており、これを使うことで、ViewModelが破棄されるときにCorotuinesScopeをキャンセルすることができます。class Db { suspend fun read(): String? { return withContext(Dispatchers.IO) { // simulate db read delay(5000) null } } } class Api { suspend fun fetch(url: String): String { return withContext(Dispatchers.IO) { // simulate api call delay(5000) "$url:fetched" } } } class MainViewModel : ViewModel() { val db = Db() val api = Api() private val _contents: MutableLiveData<String> = MutableLiveData() val contents: LiveData<String> get() = _contents init { fetch() } private fun fetch() { viewModelScope.launch { val contents = db.read() if (contents != null) { _contents.postValue(contents) return@launch } val response = api.fetch("http://api.example.com/hogehoge") _contents.value = response } } }このコードがどのように問題を解決するのかを見ていきましょう。
Kotlin Corotuiensが"問題1. ViewModelのリーク"に対してどうやって対応しているか?
viewModelScope
がキャンセルされることによって、コルーチンがキャンセルされるので、問題なく動作します。Kotlin Corotuiensが"問題2. コールバック地獄"に対してどうやって対応しているか?
Kotlin Coroutinesの中断、再開によってコールバックなしに非同期処理をコーディングしていくことができます。Kotlin Corotuiensが"問題3. メインスレッドかどうかを気にしたプログラミングが必要となる"に対してどうやって対応しているか?
viewModelScope
はメインスレッドで実行され、明示的に切り替えなければ基本的にメインスレッドで行われるため、APIコールの後であっても今のスレッドを気にせずにコーディングしていくことができます。実際どのようにKotlin CoroutinesでAPIやDBを呼び出すメソッドを実装していったら良いのか?
APIやDB呼び出しのコードがdelay()などを使ったデモコードになっておりわかりにくかったと思います。
実際にはRetrofitやRoomはsuspend functionに対応しているので、自動的にsuspend functionを定義しておくことで実装を生成してくれるため、問題なく実装できます。
またもしそのような方法が提供されていなくても以下のようにsuspendCancellableCoroutine
を利用することで問題なく実装できます。suspend fun fetch(): String { return suspendCancellableCoroutine<String> { cancellableContinuation -> myApi.fetch( // 自分で用意したAPI onSuccess = { result: String -> cancellableContinuation.resume(result) }, onFailure = { e: Throwable -> cancellableContinuation.resumeWithException(e) } ) cancellableContinuation.invokeOnCancellation { // キャンセル処理 myApi.cancelFetch() } } }またはブロッキングして取得する方法があるのであれば以下のような方法も使えます。(CoroutinesはキャンセルしたときにThreadをintrerrupedしないので処理が途中でキャンセルされないので注意が必要です)
suspend fun fetch(): String { return withContext(Dispatchers.IO) { api.blockingFetch() } }まとめ
Androidのライフサイクルの基礎的なところから、画面回転の問題をViewModelで解決し、データを監視する問題をLiveDataで解決、非同期処理の問題をKotlin Coroutinesで解決していくことができました。
実際にAndroid Developerには以下のようにそれぞれの問題について書かれています。それをつないで書いてみたのが今回の記事になります。そのため、部分的にわかりにくい部分があれば、以下を参照していただけると結構わかっていくのではないかと思います。ViewModel
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
LiveData
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
Coroutines
https://developer.android.com/kotlin/coroutines
- 投稿日:2019-12-22T01:33:57+09:00
【Android】SharedPreferencesの復習
はじめに
[MIWALAB Advent Calendar 2019]の12月22日は@Yu-shunが担当させていただきます.
最初に言っておくと,僕は巳波研ではありません!が,アウトプットする機会を定期的に作りたかったので@tackkyにお願いして書かせていただくことになりました.よろしくお願いします.今回やったこと
今回は
SharedPreferences
の復習のためにLoginっぽい機能を実装してみました.開発環境はAndroid Studio
,言語はKotlin
を使っています.
Android Studio
とは,Androidアプリ開発用の公式のIDEです.様々なツールやフレームワークがあらかじめ用意されており,高い生産性で高速に開発が可能となっています.Kotlin
とは,JetBrainsが2011年に発表したプログラミング言語です.Javaとの互換性が非常に高く,シンプルかつNull安全が保障されています.またGoogleがAndroid開発の公式言語として採用しています.SharedPreferencesとは
SharedPreferences
はデバイス内にデータを保存するための仕組みです.保存されたデータはActivityが終了しても保持されるので,基本的にはアプリの設定を保存するために用いられます.例えば,ユーザーの情報設定や状態保存などの単純で少量の環境設定保存でよく使われます.
今回はSetting画面でSharedPreferences
へのデータ保存を行い,Login画面でSharedPreferences
に保存したデータとの照合を行っています.ログインに成功した場合はResult画面で結果を表示しています.ではそれぞれの画面を見ていこうと思います.Setting画面
まずはSetting画面について説明します.まずログイン画面の設定ボタンを押すことでSetting画面に遷移します.ここで
SharedPreferences
のインスタンスを取得し,ユーザ名やパスワードのデータを書き込み,SAVEボタンを押すことでデータの保存を行っています.
※今回はxmlファイル(レイアウト)の説明は割愛させていただきます.コードと解説
SettingActivity.kt// //前略 // val saveButton = findViewById<Button>(R.id.saveButton) val passcodeEditText = findViewById<EditText>(R.id.passcodeEditText) val usernameEditText = findViewById<EditText>(R.id.usernameEditText) //1.インスタンスの取得 val sharedPreferences = getSharedPreferences("userData",Context.MODE_PRIVATE) saveButton.setOnClickListener(View.OnClickListener { //saveButton top val passcode = passcodeEditText.text.toString() val username = usernameEditText.text.toString() //ユーザ名とパスワードの長さが4以上10以下の時 if(passcodeEditText.text.toString().length >= 4 && passcodeEditText.text.toString().length <= 10 && usernameEditText.text.toString().length >= 4 && usernameEditText.text.toString().length <= 10){ //2.データの書き込み val editor = sharedPreferences.edit() editor.putString("passcode", passcode) editor.putString("username", username) editor.apply() //toast表示 val toast = Toast.makeText(this, "パスワードを設定しました", Toast.LENGTH_SHORT) toast.show() finish() //LoginActivityへ戻る } })1. インスタンスの取得
getSharedPreferences()
で,SharedPreferences
のインスタンスを取得します.1つ目の引数では,保存するデータのファイル名を設定し,2つ目の引数では,このファイルのアクセス権限を設定しています.今回はファイル名に"userData",アクセス権限に"MODE_PRIVATE(自分のみ読み書き可能)"を指定しています.2. データの書き込み
sharedPreferences.edit()
は,SharedPreferences
の持つデータを変更する複数のメソッドを持っています.editor.putXXX("key", value)
とすることで,key-valueという形式でデータをセットすることができます.この時,value
の型はXXX
で指定した型となります.そしてeditor.apply()
でセットしたデータをファイルに書き込みます.Login画面
次にLogin画面について説明します.まずはSetting画面と同様に
SharedPreferences
のインスタンスを取得します.次に保存しているデータを読み込み,入力した値と一致するかを確認し,一致する場合はインテントでResult画面に遷移させています.(ここでは遷移先にユーザ名を与え,Result画面で「こんにちは○○さん」と表示させています.)コードと解説
LoginActivity.kt// //前略 // loginButton.setOnClickListener{// loginButton tap //1.インスタンスを取得 val sharedPreferences = getSharedPreferences("userData", Context.MODE_PRIVATE) //2.データの読み込み val passcode = sharedPreferences.getString("passcode", "") val username = sharedPreferences.getString("username", "") //ユーザ名,パスワード共に入力した値と保存した値が一致した時 if (passcode.toString() == passEditText.text.toString() && username.toString() == usernameEditText.text.toString()) { //toast表示 val toast =Toast.makeText(this, "ログイン成功",Toast.LENGTH_SHORT) toast.show() //結果画面(ResultActivity)へ遷移 val intent = Intent(this,ResultActivity::class.java) intent.putExtra("UserName", username) startActivity(intent) } }1. インスタンスを取得
ここはSetting画面と同様.2. データの読み込み
sharedPreferences.getXXX("key", defaultValue)
メソッドを呼びます.1つ目の引数に指定したkey
にデータがあればそのデータを取得し変数に代入します.もしなかった場合は,2つ目の引数で設定したdefaultValue
の値が代入されます.最後に
今回書かせていただいた
SharedPreferences
は,WebでいうCookie
などのAndroidバージョンです.アプリを立ち上げた際に毎回入力する情報などは,SharedPreferences
によって管理することで,ユーザが感じるであろう煩わしさを軽減させることに繋がると思います.ぜひ使ってみてください.GitHubにソースコード載せておきます⇒コチラ
参考文献
https://qiita.com/kskso9/items/8aed6c0717b309e5c6d2
https://qiita.com/YAmi/items/3e5640f365277d70f255