- 投稿日:2019-12-23T22:53:15+09:00
Androidアプリでのエラー処理について
この記事はCAM Advent Calendar 2019 の24日目の記事です。
昨日は@takehziさんのトランザクションとロックと二重課金でした。エラー処理って何?
Androidに限らず、クライアントアプリ全般的に言えることですが、よく「エラーが発生しました。もう一度やり直してください。」みたいなポップアップを見かけると思うのですが、それを表示するまでに行う処理のことをここでのエラー処理とします。
ここで紹介するのはあくまで一つの手法です。
やること
1. エラーを受け取る
2. 受け取ったエラーに対してそれぞれ処理を分ける... 以上
と言っても、これだけだとさっぱりなので順を追って説明できればと思います。
1. エラーを受け取る
この受け取り方はそれぞれだと思いますが、今回は
try-catch
でException
なりを受け取る想定で書いていきます。try { ... } catch(e: Exception) { // ここで受け取る }このとき
catch
には色んな種類のエラーがくることが想定されます。
何でもいいからエラーだったら「エラーが発生しました」というポップアップを出して終わりであればここで受け取って終わりですが、今回はエラーの種類によって出しわけたいのでこれでは不十分です。それを行うにはどういった種類のエラーなのかを判断するための情報が必要になります。
2. 受け取ったエラーに対してそれぞれ処理を分けるに行く前に準備をしましょう。
アプリ内で扱うエラーのクラスを作る
catch
で受け取れるためにExceptionクラスを継承したクラスを用意します。abstract class CustomException(errorMessage: String): Exception(errorMessage) abstract class ApiException(errorMessage: String): CustomException(errorMessage) abstract class LocalException(errorMessage: String): CustomException(errorMessage) ...エラーを種別にするために上記のようなクラスを作って継承関係でわけてもいいですが、今回はKotlinの
sealed class
を使って分けてみます。sealed class CustomException(errorMessage: String) : Exception(errorMessage) { sealed class ApiException(errorMessage: String) : CustomException(errorMessage) { data class NetworkException(val errorMessage: String): ApiException(errorMessage) ... } sealed class LocalException(errorMessage: String) : CustomException(errorMessage) { ... } }以上でエラークラスの準備は終わりです。
2. 受け取ったエラーに対してそれぞれ処理を分ける
受け取ったエラーに対してどのような処理を行うかを決定していきます。
try { ... } catch(e: Exception) { when(e) { is CustomException.ApiException.NetworkException -> { // やりたい処理 } ... else -> {...} } }とりあえずはここでやりたいことは終わりなのですが、このような書き方をすると全画面でエラーの分岐処理を書くことになります。
なのでエラーを処理する専用クラスを設けてそちらに処理をまとめると管理しやすくなります。/** * 例外を受け取って処理を分ける */ fun onErrorReceived(e: Exception) { when(e) { is CustomException.ApiException.NetworkException -> { // やりたい処理 } ... else -> {...} } }メソッドに返却値を設けて
Enum
などを返してもいいですが、RxJava
やLiveData
を使っているのであればそちらを使ってView側へ通知を流す形にするとスッキリ書けます。まとめ
Exception
クラスを継承したクラスを自作してのエラーハンドリングを紹介しました。
久しぶりの技術ブログだったので何を書こうか迷った挙げ句、基本的な内容になってしまいましたが、これからは定期的に書いていければと思っています。明日は@kidoyunaさんの記事が公開予定です。ぜひそちらもご覧ください。
- 投稿日:2019-12-23T22:06:20+09:00
人種も育ってきた環境も違う二人がともに生活を始め、価値観の違いを埋めながら成長し、まだ見ぬ未来に希望をつなげる話
はじめに
先にタイトルのオチを書いてしまいましょう!
人種も育ってきた環境も違う二人がともに生活を始め、価値観の違いを埋めながら成長し、まだ見ぬ未来に希望をつなげる話
↓
OSもこれまでの保守会社も違うiOSとAndroidアプリを一つの会社がまとめて受け持つことになり、過去の実装方針や思想の違いを感じながら保守を行い、いつ行われるかわからない次のリニューアルに向けで準備をすすめる話です。弊社では現在デイリーアクティブユーザー120万人超えのモバイルアプリの保守をしています。
2011年から8年間iOSの保守のみを受け持って来たのですが、その実績から今年の5月末にAndroidもやってほしいと言われ、是非にということで、そのお話をお受けしました。
私自身がiOSよりもAndroid寄りのエンジニアなので、いつかAndroidの保守もやれればと思っていたことが、ついに現実になったのです!!ところが、そんなに甘い話ではなかった
Androidは今まで保守していた会社が複数あり、内部の構造はかなり煩雑
クラスの命名規則が意味不明、変数はキャメルケースとスネークケースが入り混じり、可読性最悪
更に前回行ったUIリニューアルで問題が悪化、ライフサイクルを無視した書き方やそもそもライフサイクルが正常に呼ばれないなどたくさんの問題を抱えていました。さて本題
色々すっ飛ばしますが、あの手この手を使って修正を行い、最低限想定通りな挙動をすることろまで修正を行うことができました。
この半年間何度ココロが折れそうになったことか。。。。(←ただ愚痴りたかっただけw)
でも今回のテーマは未来に希望を繋げる話です。
過去は過去ということで一旦水に流して本当に流せるのか?おい!、次のリニューアルにどんなことをやってやろうか考えてワクワクしたいと思います。
夢は大きく持って勉強していきましょ言語
選択肢としてはほぼ一択と言っていいほどですよね
もし新規案件だとしたらフレームワーク使うという方法もあるかもしれませんAndroid
- Java
- Kotlin
iOS
- Objective-c
- Swift
アーキテクチャ
ここはそれなりに選択肢が多くなります。
初めてAndroidでMVVMに触れたときはかなりの衝撃で、それ以来AndroidではMVVMを溺愛しております
iOSのMVVMにはRxSwiftが不可欠、ならばいっそのことという感じで、Clean Architectureを選択Android
- MVP
- MVVM
- Clean Architecture
- Flux
iOS
- MVC
- MVP
- MVVM
- Clean Architecture
- Flux
- Redux
- VIPER
Rx
正直まだRxはちゃんと使ったことがありません、今回引き継いたAndroid版で使っていて正直意味不明
学習コストが高いと言われるのはやはり間違っていないのだなという印象、でもこれがないと始まらないんですよね。。。Android
- RxJava
- RxAndroid
- RxKotlin
iOS
- RxSwift
- RxCocoa
通信系
Androidに関してはVolleyはそもそもDeprecatedですし。。。
OkHttpとRetrofitの組み合わせはについては、何も言うことはありませんね
iOSはAlamofireをラップしたライブラリMoyaを選択します
更にシンプルに実装ができるそうAndroid
- Retrofit2
- OkHttp3
- Volley
iOS
- Alamofire
- Moya
画像系
画像系からはすでに使ったことのあるものをチョイスしました
Android
- glide
- picasso
iOS
- SDWebImage
- AlamofireImage
DB
やはりレガシーなDBより高速でわかりやすいRealmを使用します
Android
- MySQL
- Realm
iOS
- CoreData
- Realm
Jsonパーサー
Jsonパーサーも同じく使ったことがあるものを選択です
Android
- gson
- Jackson
iOS
- Codable
- SwiftyJSON
DI
Android
せっかくkotlinを使うのだらkoinを使いましょう
- dagger2
- koin
その他
その他にも便利そうなライブラリをいくつか
Android
- Data Binding Library
- LiveData
- MergeRecyclerAdapter
iOS
- FrameAccessor
- NSLayoutAnchor
まとめ
という感じで、未来に向けてボクの考える最強のアプリ開発設計です。
でもまだまだ抜けはある気がする。。。
もしこれが全部使いこなすことができれば、それなりのレベルの開発者になれるはず。
そしてどんどん新しい技術が出ているので、これだけにこだわらずアンテナを広げていきたいなと思います!そう言えば先日弊社が猫会社として取材されました!!
https://sippo.asahi.com/article/12960031
猫が好きな方はぜひ読んでみてください♪
- 投稿日:2019-12-23T21:14:06+09:00
【Android】はじめてのRoom
【Android】分かった気になれる!アーキテクチャ・MVVM概説 ではアーキテクチャ・MVVMの概要をコードを記述せずに概念のみ説明してみました。
その後の投稿では、実践編として各ライブラリを実際にコードを記述し記事にしています。
これまでの記事 :MVVMシリーズ実践編第二回目のテーマは、Roomです。
DBの一つであるSQLiteを容易に扱うことのできるRoomライブラリについて記事を書こうと思います。下図のMVVMの構造において、Roomが担うのは黄緑の枠線で示された部分です。
参照:https://developer.android.com/jetpack/docs/guide#overviewはじめに
本記事では、Roomを使用しDB(SQLite)でのデータ保存・取得処理を実装します。
今回は、添付の画面キャプチャーのようなアプリを例に説明していきます。
このアプリは、前回記事:【Android】はじめてのDataBindingで作成したアプリにDBへのデータ保存機能を追加したものです。
「SET!!!」ボタンを押下したときに入力した文字列(Play Call)をDBに保存します。(
Play Callとは
アメリカンフットボールの試合中に伝えられる作戦のことです。
本アプリでは、入力した文字列をPlay Callに見立てて各種オブジェクトを命名しました。
)
データ保存の流れ 次回アプリ起動時 Roomの導入
まず、appレベルの
build.gradle
にて、dependencies
にRoomライブラリを追加しsync
を実行します。
これで、導入は完了し各種Roomが提供するコンポーネントを使用することができるようになりました!build.gradle(app)dependencies { // 略 // room def room_version = "2.2.0-rc01" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" }Roomの実装
- Database
- Entity
- DAO(Data Access Object)
Roomライブラリは上記3つのコンポーネントを提供し、それらを用いてDB(SQLite)上でデータを管理します。
クラスやインターフェースなどにこれらのアノテーションをつけることで、Roomが提供する各コンポーネントとして振舞うようになります。それぞれのコンポーネントの説明を実際のコードを用いて行います。
Database:
@Database
SQLiteと直接接続する部分になります。
アプリ内でインスタンスを生成し、DBでのデータ管理を実装します。この
RoomDatabase
クラスは、abstract class
に@Database
アノテーションをつけることで定義されます。
Google公式ガイドによると、@Database
アノテーションがついたクラスは以下の条件を満たす必要があります。
RoomDatabase
クラスを継承すること- Databaseに関連づけられている
Entity
のリストをアノテーションに含むこと@Dao
アノテーションで定義されたインターフェースDao
の抽象メソッドを含むこと本アプリでの実装:
AppDatabase.kt@Database(entities = [PlayCallEntity::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun playCallDao(): PlayCallDao }Entity:
@Entity
DBのテーブルを示します。
data class
に@Entity
アノテーションをつけることで定義されます。
Entity
内の要素につけることのできるアノテーションとして、主に以下のものがあげられます。
@PrimaryKey
主キーに設定したい要素につけます。主キーは、1つのEntity
クラスに最低1つ必要です。
@ColumnInfo
DB内でのカラム情報をアプリとは別で設定したい場合につけます。
このアノテーションのname
プロパティが主に使用されるのではないかと思います。アプリ内では一般的にキャメルケースで要素を命名しますが、SQLiteではテーブル名やカラム名の大文字・小文字は区別されず、スネークケースで定義されることが多いと思われます。このような理由から、ColumnInfo
アノテーションのname
プロパティを使用してアプリで定義した要素名とは別の名前でSQLiteのカラム名を設定しておくのが良いと考えられます。
@Ignore
このアノテーションがついた要素はDBのカラムに追加されません。つまり、そのデータは永続化されません。本アプリでの実装:
PlayCallEntity.kt@Entity(tableName = "play_calls") data class PlayCallEntity( @PrimaryKey @ColumnInfo(name = "description") val description: String )DAO:
@Dao
DAO(Data Access Object)は、Databaseにアクセスするためのメソッドを格納するオブジェクト、インターフェースです。
interface
に@Dao
アノテーションをつけることで定義されます。
このインターフェース内で、以下のアノテーションをつけることで、データ挿入・削除・取得処理などを実行するメソッドを定義します。
挿入:
@Insert
Entity
を引数とするメソッドにつけるだけで、Entity
に基づくデータをDBに挿入できます。削除:
@Delete
削除したいレコードのEntity
を引数とするメソッドにつけるだけで、DB内の対象のEntity
を削除することができます。取得
@Query
アノテーション内にSQL文であるSELECT文を記述することで実装します。
SQL文についてはこちらを参照
基本的なSQL文 : Oracle公式ガイド本アプリでの実装:
@Dao interface PlayCallDao { // データの取得メソッド @Query("SELECT * FROM play_calls") fun loadAllPlayCall(): List<PlayCallEntity> // 挿入メソッド @Insert(onConflict = OnConflictStrategy.REPLACE) fun savePlayCall(playCallEntity: PlayCallEntity) }使ってみる
用意した
Room
のコンポーネントを使用し、Viewを変更するよう実装します。Databaseをインスタンス化
まず、定義したDatabaseをインスタンス化します。
今回はひとまず
Application
クラスを作成し、companion object
としてAppDatabase
のインスタンスを定義することでアプリ起動中はAppDatabase
のインスタンスを共有できるようにしました。
その後、onCreate
メソッド内でデータベースをビルドしました。これで、
AppDatabase
をアプリ内で使う準備ができました。Application.ktclass Application : Application() { companion object { lateinit var database: AppDatabase } override fun onCreate() { super.onCreate() // AppDatabaseをビルドする database = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "app_database" ).build() } }DAO内のメソッドを呼び出す
今回は
ViewModel
にて、database
およびPlayCallDao
を呼び出しました。
まず、重要な部分を抽出します。// PlayCallDaoをインスタンス化 private val dao = Application.database.playCallDao() // daoのデータ取得処理を呼び出す dao.loadAllPlayCall() // daoのデータ保存処理を呼び出す // playCallはメソッドに渡されたPlayCallクラスの引数 dao.savePlayCall(PlayCallEntity(description = playCall.first().description))次に、実際の実装です。
DAO内のメソッドはメインスレッドからは呼び出せないため、今回はひとまずAsyncTask
を採用しています。
これをViewから呼び出すことで、最初に添付したキャプチャーのように入力履歴のリストを表示することができます。
(RecyclerView実装の説明は省略)FootballViewModel.ktclass FootballViewModel { private val dao = Application.database.playCallDao() // 略 // Fragmentから呼ばれる fun loadPlayCallHistoryList() { val asyncLoad = AsyncLoad(dao, this) asyncLoad.execute() } // Fragmentから呼ばれる fun savePlayCall(playCall: PlayCall) { val asyncSave = AsyncSave(dao, this) asyncSave.execute(playCall) } } // Coroutinesを使いたい class AsyncLoad(private val dao: PlayCallDao, private val viewModel: FootballViewModel) : AsyncTask<Void, Void, List<PlayCall>>() { override fun onPreExecute() {} override fun doInBackground(vararg voids: Void): List<PlayCall>? { val playCallMutableList = mutableListOf<PlayCall>() dao.loadAllPlayCall().forEach { playCall -> playCallMutableList.add(PlayCall(description = playCall.description)) } return playCallMutableList } override fun onPostExecute(listOfPlayCalls: List<PlayCall>) { viewModel.setPlayCallHistoryList(listOfPlayCalls) } } class AsyncSave(private val dao: PlayCallDao, private val viewModel: FootballViewModel) : AsyncTask<PlayCall, PlayCall, Void>() { override fun onPreExecute() {} override fun doInBackground(vararg playCall: PlayCall): Void? { dao.savePlayCall(PlayCallEntity(description = playCall.first().description)) return null } override fun onPostExecute(result: Void?) { val asyncLoad = AsyncLoad(dao, viewModel) asyncLoad.execute() } }(
今後の編集予定箇所:
DIを実装する
Repository
でデータ管理に関する処理を実装できるよう修正したいです。
この程度のアプリではほとんど意味がありませんが、Repository
でラップしDIすることで以下のような様々な利点があるからです。
DIを実装し、あるデータの処理に関するメソッドを1つのRepository
に集約することで、
1.そのデータを扱う各ViewModel
の肥大化を抑えることができる
2.ViewModel
では、データの参照元(ローカル?リモート?どのDB?)を気にする必要がなくなる
3.修正時は、Repository
を修正するだけでよい
4.テストをしやすくなる(この利点を感じられるほど、私は十分にテストしたことがありませんが)Coroutinesの適用
現在AsyncTask
でDAO
を呼び出していますが、Coroutines
に移行したいです。
AsyncTask
だとデータを取得するためのクラスや保存するためのクラスを独自に定義する必要があり、ViewModelのファイルが肥大化してしまうからです。
また、コンストラクタとしてViewModel
やDao
を渡しており、この部分が回りくどく冗長であると感じるからです。
)まとめ
今回は、Roomを用いてSQLiteでのデータ管理を実装してみました。
SQLiteの知識に乏しいため不明な点も残っていますが、基本は抑えられたのではないかと考えています。
今後は、CoroutinesやDIの実装を進めアーキテクチャMVVMシリーズ実践編を完結する予定です。コメント、編集依頼は絶賛募集中です!
ソースコード
ソースコードはGitHubにあげています。
https://github.com/iTakahiro/ArchitectureFootball関連記事
参考にした資料
- 投稿日:2019-12-23T20:51:23+09:00
アプリを見比べたいじゃない
アプリを見比べたい
アプリもリリースしてから年月が経つと
「今のアプリの動きってどんなだっけ。。。」
「あれ?改修後はどうなるんだっけ??」
といった具合で改修前、改修後のアプリを見比べたい時なんかが発生してきます
ですが普通にアプリを端末にインストールするとPackege(ApplicationID)が一緒なもんだからアプリを上書きインストールしてしまうんですよね。。。
あーこまった。。
そんな時にFlavor
そう、そんな時に役立つのがFlavorなんです
前回こちらの記事でConstファイルとか切り替えられるよっていう話をしたんですけど、Flavorさんが変えられるのはファイルだけじゃなかった
接頭詞をつけちゃうよ
今回Flavorにやってもらう仕事はApplicationIDに接頭詞をつけることです
開発APIサーバに向いているFlavorにはdev
本番APIサーバに向いているFlavorにはprod
みたいな感じね
ついでにアプリの名前も変えちゃおう
開発API向きには「テスト」
本番API向きは「本番」
とかつけちゃう
これでインストールしてみよう
いいかんじ
見事「本番」アプリと「テスト」アプリを2つ一緒にインストールできました
名前もパット見でわかりやすくていいですね
Flavorを増やせば好きなようにアプリを見比べることができるので
この小技はおすすめです
では
- 投稿日:2019-12-23T19:01:09+09:00
【ぶっちゃけAndroidアプリ開発】AsyncTaskクラスの備忘録
このTipsはJava言語をある程度学んでいる方を前提にしています。
Web通信するAndroidアプリを開発する上で、AsyncTaskという非同期クラスを使う事になりました。その時に学んだ事を備忘録として記録します。
AsyncTaskに翻弄されている方の助けになればと思います。サンプルコード
AsyncTaskを継承して実装したサンプルコードです。(Android Developersより)
AsyncTaskは、呼び出し側と非同期で実行したい場合に使います。なので、呼び出し側はクラスをインスタンス化して実行(executeメソッド)する事になります。(呼び出し方は後述)private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return totalSize; } protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } protected void onPostExecute(Long result) { showDialog("Downloaded " + result + " bytes"); } }ジェネリクス
AsyncTaskを継承してクラスを作成していますが、AsyncTaskはジェネリクスを使用しているため継承する時に以下のような記述になります。
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { }ジェネリクス(総称型)というのは、クラスが引数の型を定めない場合に使うんだそうです。ここではとりあえずそう覚えておきます。
このサンプルでは、 URL, Integer, Long となっていますが、型が決まっていないので、クラスの実装によって他の型になります。doInbackgroundメソッドと可変長引数
AsyncTaskには必ずオーバーライドしなければならないabstructメソッドとして、doInBackgroundがあります。
doInBackgroundは、AsyncTaskが(非同期に)実行された時に裏側で動くいちばんメインの処理を記述します。protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return totalSize; }引数はピリオドが3つついたもの (URL... urls) となっています。
これは可変長引数と言って、受け取るのはurls[]配列変数となります。
なぜ配列変数の引数にしないかというと、呼び出す側には羅列して書いていいので、呼び出す前にいちいち配列を作らなくていいというメリットがあるからだそうです。(呼び出し方は後述)doInBackgroundメソッドの引数 (URL... urls) は、クラス継承する時にジェネリクスで指定した1つ目の型です。戻り値の totalSize は、3つ目の型(ここではLONG型として戻す)です。
呼び出し側
呼び出し側の記述です。
new DownloadFilesTask().execute(url1, url2, url3);executeメソッドで実行するとdoInBackgroundメソッドの処理が裏側で動きます。
url1とurl2とurl3が、可変長引数として(結局は配列変数として)渡されます。onPostExecuteメソッド
サンプルで、AsyncTaskのonPostExecuteというメソッドを記述しています。
このメソッドは、doInBackgroundの処理が終わった後に呼び出されます。protected void onPostExecute(Long result) { showDialog("Downloaded " + result + " bytes"); }doPostExecuteメソッドの引数 result は、doInBackgroundの戻り値がセットされます。
ジェネリクス再び
最初に戻って、クラスを継承した時にジェネリクスに指定する1つ目の型はdoInBackgroundの引数と、3つ目の型はdoInBackgroundの戻り値(=onPostExecuteの引数)と合わせましょう。
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { }
ぶっちゃけAndroidアプリ開発は、アプリ開発にあたってとりあえず知っておくべきことをズバリ書きます。例外はありますのでご了承ください。
勉強中の身でありますので、誤りなどあればご指摘いただけると有り難いです。
- 投稿日:2019-12-23T10:57:02+09:00
Epoxyを使ってスクロールがかくつくと思ったらリサイクルされていなかった
この記事はand factory Advent Calendar 2019の24日目の記事です
きっかけ
とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。
対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。
LinearLayout // rootのViewGroup ∟ RecyclerView ∟ RelativeLayout ∟ RecyclerView ∟ RecyclerView ∟ LinearLayout ∟ ...
要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。
Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです
LinearLayoutをEpoxyに切り替える
- 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードを
EpoxyModelWithHolder
を拡張したクラスに移動しました動作テスト
起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。
古い端末だとスクロールでちょいちょいかくつきが気になります。
上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。
EppxyがViewPoolを管理している箇所
ActivityRecyclerPool.ktinternal class PoolReference( context: Context, val viewPool: RecyclerView.RecycledViewPool, private val parent: ActivityRecyclerPool ) : LifecycleObserver { private val contextReference: WeakReference<Context> = WeakReference(context) val context: Context? get() = contextReference.get()
Epoxyは独自でViewPoolを管理しているようです。RecyclerViewでは1つのActivityで保持するインスタンスがViewTypeごとに5つまでと制限されていますが、Epoxyはこの制限が外されています。この辺りの仕様からもEpoxyとRecyclerViewが別々の実装を持っていることがなんとなくわかります。
RecyclerView内でViewPoolからキャッシュを取得するコード(多分)
RecyclerView.java// この辺りでキャッシュを探しているようです ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 省略.. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i);
- RecyclerViewでも保持しているViewPoolからキャッシュを探しているようです
- メソッド名からにじみ出ていますが、ViewPool以外でもキャッシュを保持しているクラスがいるようですね。adapterとかmChildHelperとか、どんな役割なのか気になります。
- RecyclerView.java
原因
今回のリファクタでrootのLinearLayoutをEpoxyに変更しましたが。子ビューで使っていたRecyclerViewはそのままだったのが原因でした。
Epoxyは1つのActivity内のEpoxy同士で描画したViewをViewPoolにキャッシュして次回からリサイクルできますが、RecyclerviewのViewPoolはEpoxyのものとは別管理のため毎回inflateが発生していたため、古い端末でかくつきが起こっていました。
対応
- EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。
まとめ
- EpoxyはネストされたEpoxyでもViewPoolをがデフォルトで共有してくれるので便利です。
- Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
- 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました
参考
- 投稿日:2019-12-23T10:57:02+09:00
Epoxyのスクロールがかくつくので調べてみたらリサイクルされていなかった
この記事はand factory Advent Calendar 2019の24日目の記事です
きっかけ
とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。
対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。
LinearLayout // rootのViewGroup ∟ RecyclerView ∟ RelativeLayout ∟ RecyclerView ∟ RecyclerView ∟ LinearLayout ∟ ...
要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。
Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです
LinearLayoutをEpoxyに切り替える
- 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードを
EpoxyModelWithHolder
を拡張したクラスに移動しました動作テスト
起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。
古い端末だとスクロールでちょいちょいかくつきが気になります。
上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。
EppxyがViewPoolを管理している箇所
ActivityRecyclerPool.ktinternal class PoolReference( context: Context, val viewPool: RecyclerView.RecycledViewPool, private val parent: ActivityRecyclerPool ) : LifecycleObserver { private val contextReference: WeakReference<Context> = WeakReference(context) val context: Context? get() = contextReference.get()
Epoxyは独自でViewPoolを管理しているようです。RecyclerViewでは1つのActivityで保持するインスタンスがViewTypeごとに5つまでと制限されていますが、Epoxyはこの制限が外されています。この辺りの仕様からもEpoxyとRecyclerViewが別々の実装を持っていることがなんとなくわかります。
RecyclerView内でViewPoolからキャッシュを取得するコード(多分)
RecyclerView.java// この辺りでキャッシュを探しているようです ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 省略.. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i);
- RecyclerViewでも保持しているViewPoolからキャッシュを探しているようです
- メソッド名からにじみ出ていますが、ViewPool以外でもキャッシュを保持しているクラスがいるようですね。adapterとかmChildHelperとか、どんな役割なのか気になります。
- RecyclerView.java
原因
今回のリファクタでrootのLinearLayoutをEpoxyに変更しましたが。子ビューで使っていたRecyclerViewはそのままだったのが原因でした。
Epoxyは1つのActivity内のEpoxy同士で描画したViewをViewPoolにキャッシュして次回からリサイクルできますが、RecyclerviewのViewPoolはEpoxyのものとは別管理のため毎回inflateが発生していたため、古い端末でかくつきが起こっていました。
対応
- EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。
まとめ
- EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
- Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
- 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました
参考
- 投稿日:2019-12-23T10:57:02+09:00
Epoxyのスクロールがかくつくので調べてみたらリサイクルされていなかったので
この記事はand factory Advent Calendar 2019の24日目の記事です
きっかけ
とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。
対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。
LinearLayout // rootのViewGroup ∟ RecyclerView ∟ RelativeLayout ∟ RecyclerView ∟ RecyclerView ∟ LinearLayout ∟ ...
要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。
Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです
LinearLayoutをEpoxyに切り替える
- 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードを
EpoxyModelWithHolder
を拡張したクラスに移動しました動作テスト
起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。
古い端末だとスクロールでちょいちょいかくつきが気になります。
上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。
EppxyがViewPoolを管理している箇所
ActivityRecyclerPool.ktinternal class PoolReference( context: Context, val viewPool: RecyclerView.RecycledViewPool, private val parent: ActivityRecyclerPool ) : LifecycleObserver { private val contextReference: WeakReference<Context> = WeakReference(context) val context: Context? get() = contextReference.get()
Epoxyは独自でViewPoolを管理しているようです。RecyclerViewでは1つのActivityで保持するインスタンスがViewTypeごとに5つまでと制限されていますが、Epoxyはこの制限が外されています。この辺りの仕様からもEpoxyとRecyclerViewが別々の実装を持っていることがなんとなくわかります。
RecyclerView内でViewPoolからキャッシュを取得するコード(多分)
RecyclerView.java// この辺りでキャッシュを探しているようです ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 省略.. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i);
- RecyclerViewでも保持しているViewPoolからキャッシュを探しているようです
- メソッド名からにじみ出ていますが、ViewPool以外でもキャッシュを保持しているクラスがいるようですね。adapterとかmChildHelperとか、どんな役割なのか気になります。
- RecyclerView.java
原因
今回のリファクタでrootのLinearLayoutをEpoxyに変更しましたが。子ビューで使っていたRecyclerViewはそのままだったのが原因でした。
Epoxyは1つのActivity内のEpoxy同士で描画したViewをViewPoolにキャッシュして次回からリサイクルできますが、RecyclerviewのViewPoolはEpoxyのものとは別管理のため毎回inflateが発生していたため、古い端末でかくつきが起こっていました。
対応
- EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。
まとめ
- EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
- Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
- 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました
参考
- 投稿日:2019-12-23T10:57:02+09:00
Epoxyのスクロールがかくつくので調べたらリサイクルされていなかった
この記事はand factory Advent Calendar 2019の24日目の記事です
きっかけ
とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。
対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。
LinearLayout // rootのViewGroup ∟ RecyclerView ∟ RelativeLayout ∟ RecyclerView ∟ RecyclerView ∟ LinearLayout ∟ ...
要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。
Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです
LinearLayoutをEpoxyに切り替える
- 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードを
EpoxyModelWithHolder
を拡張したクラスに移動しました動作テスト
起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。
古い端末だとスクロールでちょいちょいかくつきが気になります。
上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。
EppxyがViewPoolを管理している箇所
ActivityRecyclerPool.ktinternal class PoolReference( context: Context, val viewPool: RecyclerView.RecycledViewPool, private val parent: ActivityRecyclerPool ) : LifecycleObserver { private val contextReference: WeakReference<Context> = WeakReference(context) val context: Context? get() = contextReference.get()
Epoxyは独自でViewPoolを管理しているようです。RecyclerViewでは1つのActivityで保持するインスタンスがViewTypeごとに5つまでと制限されていますが、Epoxyはこの制限が外されています。この辺りの仕様からもEpoxyとRecyclerViewが別々の実装を持っていることがなんとなくわかります。
RecyclerView内でViewPoolからキャッシュを取得するコード
RecyclerView.java// この辺りでキャッシュを探しているようです(おそらく) ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 省略.. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i);
- RecyclerViewでも保持しているViewPoolからキャッシュを探しているようです
- メソッド名からにじみ出ていますが、ViewPool以外でもキャッシュを保持しているクラスがいるようですね。adapterとかmChildHelperとか、どんな役割なのか気になります。
- RecyclerView.java
原因
今回のリファクタでrootのLinearLayoutをEpoxyに変更しましたが。子ビューで使っていたRecyclerViewはそのままだったのが原因でした。
Epoxyは1つのActivity内のEpoxy同士で描画したViewをViewPoolにキャッシュして次回からリサイクルできますが、RecyclerviewのViewPoolはEpoxyのものとは別管理のため毎回inflateが発生していたため、古い端末でかくつきが起こっていました。
対応
- EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。
まとめ
- EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
- Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
- 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました
参考
- 投稿日:2019-12-23T08:51:53+09:00
Vue.js で Android 向けに明朝体を使いたい
Andoroid に明朝体がないのは割と知られてる話で、そういう場合は Google Web フォントとか使うのが一般的ですが、
Vue.js でやったときにちょっとハマったのでメモ。TL;DR
- npm パッケージのフォントだとダメっぽい(パッケージ内での実装方法にもよるかも?)
- CSS の
@import
で読ませればOK- または vue-head パッケージ使ってるならそれを使ってもOK
Vue.js で Google Fonts を使う
実は、Google Fonts で公開されているフォントはほとんどが npm パッケージからの導入もできて、
typaface/xxx (あるいは typeface-xxx)みたいな感じで検索するとでてきます。main.js でインポートすればすぐつかえるので、私はいつもこの方法でやっていました。
たとえば、さわらび明朝ならこんなふうに。
インストール
npm install typeface-sawarabi-mincho --save # or yarn add typeface-sawarabi-minchoインポート
main.jsimport 'typeface-sawarabi-mincho/index.css'ところが、この方法だと Android でちゃんとウェブフォントが反映してくれませんでした。
Vue.js で Android でも表示できるように Google Fonts をつかう
じゃあ、どうすればよいのかという話ですが、普通に CSS の
@import
で CDN からインポートすればOKでした。
というかこうしないとうまく行かなかった…。FrontPage.vue<style> @import url('https://fonts.googleapis.com/css?family=Sawarabi+Mincho'); </style>もしも、普段は sass とか scss で書いていて、 style ブロックに
lang="scss"
とかつけていた場合は、それとは別に style ブロックを追加してあげます。FrontPage.vue<style> @import url('https://fonts.googleapis.com/css?family=Sawarabi+Mincho'); </style> <style lang="scss"> html { font-size: 62.5%; font-family: "Sawarabi Mincho", "Hiragino Mincho Pro", "MS Mincho", sans-serif; } #app { .section { // ... } } </style>これでOKでした。
vue-head で読ませる
CSS の
@import
はつかいたくないなぁ、って場合は、HEADタグに突っ込むわけですが、できれば public/index.html は触りたくないなぁ、ということで、<header></header>
にいろいろ追加できる vue-head を使えばできます。
これは、meta とか title を書き換えるのに使うことが多いですが、スタイルシートや JS、favicon なんかも読ませることができます。
(これらも HEAD 内に書いてるんでまぁ当たり前なのかもしれませんが、意外と忘れがちなのは私だけ?)FrontPage.vue<script> export default { head: { link: [ { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Sawarabi+Mincho' }, ] }, } </script>まとめ
- とりあえず、vue-router も使ってて、タイトル書き換えとかしてるなら vue-head つかってるだろうから、そこで読ませる。
- ルーター使ってないような LP などの超シンプルページの場合は、 style ブロックに
@import
するか、それでもヘッダーにはいろいろ書くだろうから、vue-head 入れちゃうのがやっぱり楽かも。- Vue + Electron みたいな感じで、PC/Mac 向けのアプリにするとかで、Android 全く考慮しないで良い&どうしてもオフラインで使わなくちゃいけないってときは、 npm の typeface シリーズを使ってもよいのかも
- 投稿日:2019-12-23T07:55:42+09:00
今年はクリぼっちが本当に少ない / Flutter
今年はクリぼっちが本当に少ない / Flutter
今年もやってまいりました!!クリスマス!この記事でわかること
- Flutterでアプリを作りましたので、その技術内容と悩み
- 恋をすることの素晴らしさ
- クリスマスの考察
この3つを中心に書いていければなと思います。
私の周りでクリぼっちが減った!?
最近、私の周りでは幸せ報告が後を絶えません。
いわゆる結婚ラッシュってやつでしょうか。
- 結婚しましたー(例のシンデレラの人)
- 結婚したぷるぷる!(大学の先輩)
- 結婚しました〜(インスタグラム多数)
- 付き合った報告(思い切って恋をしてみました!)
しかもそれだけでなく、恋をした効果なのか
- 結婚(シンデレラ) → 勇気を出してデザインのフリーランスになれた!
- 結婚(大学の先輩) → CEOでめちゃめちゃ稼ぐ
- 付き合った報告 → CTOになれた!
など恋をしつつも自分にコミットできている方が今年は多いと感じられました。
いや逆に、恋をしているからもっと頑張れるわけです。
やっぱり恋ってすごい
あらためて恋ってすごいなーと思います。
昔から、「衣食住」といいますが
「衣食住恋」で4大要素として入れるべきだと思います。
- 恋はパワー
- 恋は頑張れる
- 恋駆動開発したほうが生産性が上がると最近すごく思います!!!
コードギアスのシャーリーさんもこう言っていましたよね
「恋はパワーなの!誰かを好きになるとね、すっごいパワーが出るの。 毎日毎日その人のことを考えてアプリを書いちゃったり、 早起きしちゃったり、 マフラーを編んじゃったり、 CIを組んじゃったり、 滝に飛び込んでその人の名前をリファクタしたり、 私だって…!…その、ルルにはないの?誰かのために、いつも以上の何かが」 (コードギアスより)ということで、今日からは「恋駆動開発」を応援し
生産性をあげよう!!!ということで
思い出フォトアプリを作りました!!
「まずは、ノープランでお出かけしてください。」
- 1: 写真スポットに近づくと通知が来ます
- 2: カメラは好きなカメラでとって下さい
- 3: Instagramでどんな写真が投稿されているかわかります
技術要素
- Flutterで作ってみました(4~5人日ぐらい)
- CleanArchitectureでやってみた
- StatelessWidgetを細かく分けるよう心がけました
- Firebase
- Firestore(位置情報一覧)
- GeoHashという概念を知らなかった
- FirebaseDistribusion
- GithubActionsでCI/CDを構築
- FirebaseDistribusionでAdhoc配信してテスト
- GeoFlutterFire
- GPSまわりの計算が楽+Firestoreからデータを抜いてこれる!!
- LP
- studio.designで構築
- まじで早い
Flutterを使ってよかったこと
1: 位置情報系の計算もFirestoreとGeoFlutterFireで楽に計算できる
恥ずかしながら、GeoHashという概念を初めて知りました。
2008年2月にスタートしたgeohash.orgサービスの目的は、地上の地点を特定するための短いURLを提供することにあった。 電子メールやウェブサイト、ウェブサイトへの書き込みの際に便利になるからである。 例えば、緯度及び経度の組 57.64911,10.40744 からは u4pruydqqvj というハッシュが導き出され、 http://geohash.org/u4pruydqqvj というURLで表現される。(Wikipediaより)
位置情報を、GeoHash値で格納することによって検索パフォーマンスが上がり楽になるそうです!
GeoHash以外にも方法はあるみたいなので気になる方は調べてみるといいと思います。Firestoreに格納されている位置情報に対して
今の、緯度経度と範囲を指定するだけでデータが検索できてしまいます!便利でした!2: GitHubActionsでCI/CD
iOSネイティブの頃はCircleCIを主に使っていましたが、
GitHubActionsだとまた少ない定義で構築することができました!!!これによってiOS/Androidアプリを
FirebaseDistributionで一気に配信でき便利でした!
(iOSがPod周りのpath系めんどかった)3: CleanArchitectureでやってみた
こちらのリンクを参考に、iOSでの思想をFlutterにも入れて見ましたが、
Flutterの特性を活かせつついい感じで構築できたのではないかと思います。https://matteomanferdini.com/ios-architecture-lotus-mvc-pattern/
Flutterで感じた悩み
- ホットリロードは早いけど、初回インストールテストをするときにCocoaPodsなのでbuildが遅い!!
- どうしてもUIのネストが深くなりがち
- SwiftっぽいExtensionとかほしい
Flutterに対する考察
他のクロスプラットフォーム(Xamarin/React Native/Titanium)と比べて強い勢いを感じており、普及はもう少し進むのではないでしょうか
実際に勢いを調べてみました
これは勢いがありそうですね
実際、Titanium、Xamarinが出たときも振り切るか迷いましたが、OSSの管理がいつまで続くか、定着率はどれぐらいか、などを判断するとまだ振り切れませんでした。
WebもすべてGoogleによって統一される日が来る
アプリサイドから攻めているのもいい
- 今までってWebと同じでかけるぜー感が少し嫌だったのですが、ネイティブアプリエンジニアのアプリ開発が楽になるし、Webもいけるようになる感が、アプリエンジニアに広まりやすくなる切っ掛けにもなるのではないでしょうか
ミスったところ
1: あんまり考えずに、インスタの画像をハッシュタグ検索で抜いて持ってこようと思っていた
インスタの制限がきついのはなんとなく知っていただどうにかなると思っていたが、
どうやっても画像が抜けない
- Instagram Graph APIは7日30件までしか検索できないため断念
- Instagramはスクレイピング禁止
- WebViewで表示するも、読み込みが遅いのと毎回ログインを聞かれる(最悪)
○InstaAPIで行く案
・InstaAPI(古い)はもうkeyの新規発行してない
・GraphAPIはリミット制限○InstaSDK for Flutterで行く案
・そもそもタグ検索がない○WebViewで行く案
モバイル
document.querySelector('.xZ2Xk');PC
document.querySelector('.ZUqME.N9d2H[style="width: 100%;"]').style.display ="none";でログイン画面が消える(macのブラウザでuser agent操作)けど、flutter webviewだと消えないくそう
onPageFinished onWebViewCreatedで実行するもできない
○Tumblr
○投稿型にする
○画像を表示しない
○手作業で2000件選ぶ
などなど手はありそうではあるが有識者の方アドバイスください。
2: GitHub Actionsでアプリを配信するようにした
- MacOSのビルドはLinuxのコンテナに比べて10倍料金が高いため、何度かfastlane実行してたりテストしてたらお金がかかっていた(従量制にしていた) -> ブランチ制限などで対応
やっぱりものづくりって面白い
- 何より楽しい!
- みんなが使ってくれることを想像し、にやにやしてしまいます。結局これって恋と同じかもしれません。
クリぼっちは少ないのか
- 私の周りの方で、恋をしてみようかなと思ってくださって行動した方がいます。
- そして見事に、恋の良さに気づいて生産性がバク上がりした人もいます。(よく逆に捉えられます)
- 感謝をしてくれたのがとても嬉しかったです。
結論:私の身の回りでは「クリぼっちが少ない」
恋をしよう!!
- ペアーズとWithはおすすめです!
https://www.pairs.lv/
https://with.is/welcomeクレジット
- おかむー(https://twitter.com/okamu_ro)
- せり (https://twitter.com/_serinuntius)
- ブライアン (https://twitter.com/skinnybrian_tw)
- その他、協力者のみなさん
まとめ
- Flutterは勢いがある!
- 恋をして生産性をあげよう!
そしてこの度、no plan(ノープラン)株式会社を立ち上げて初のサービスリリースになりました!
no plan株式会社 を設立しましたー?
— おかむー (@okamu_ro) 2019年10月10日
共同創業者の @_serinuntius と一緒にやっていきます!!
何をやるかは…なんだっけ。
ノープラン株式会社が、
ノープランの人たちに、
ノープランであることが幸せに思える
ノープランファーストなサービスを作ろうと思いますが…
これからがんばります。 https://t.co/P1DNjXbmsq今回のグラマブルは
ノープランのまま散歩しても、思い出をしっかりと残せるサービス となりノープラン株式会社が、
ノープランの人たちに、
ノープランであることが幸せに思える
ノープランファーストなサービスを作ろうと思いますノープランファーストをまずは1つ実現できたのではないかと思います!!!
アプリは即興なので使いにくいところありますがブラッシュアップできればと思います!!!
ありがとうございました!!!
- 投稿日:2019-12-23T01:51:22+09:00
Androidアプリを公開する
初めてAndroidアプリを公開したので手順をメモします。
準備するもの
- Google Playデベロッパーアカウント
- APKファイル
- ストア掲載用スクリーンショットファイル
- ストア掲載用アプリアイコンファイル
- ストア掲載用ヘッダー画像
手順
Google Play Consoleにログインします。
すでに内部テストなどでアプリ作成が完了している場合には該当のアプリを選択します。新規作成の場合には[アプリの作成]をクリックし、言語とタイトルをつけて作成してください。
私は内部テスト後なので作成されたアプリをクリックして進めます。
左メニューからリリース管理>アプリのリリースをクリックします。
製品版トラックの[管理]をクリックします。
[リリースを編集]をクリック
[追加するAndroid App Bundle とAPK]の中にAPKファイルを追加します。
内部テストと同じファイルをアップロードする場合にはドロップするとアップロードエラーになるのでライブラリからの追加を行います。
リリース名とリリースノートを入力して保存後確認します。
ストアの掲載情報を埋めます。
ストア掲載情報の登録後、またアプリのリリース>製品版管理>リリースを編集をクリックします。
画面右下の[確認]をクリック
画面右下の[製品版として公開を開始]をクリック
ポップアップで確認をクリック。
もうこれで公開されたかのように思えてしまいますが
ダッシュボードを見ると審査中になっていることがわかります。
また動きがあったら更新します。
参考リンク
- 投稿日:2019-12-23T01:45:02+09:00
[Android / Kotlin / ARCore]ARCoreでTextViewを出す
AR触ってみたい。
widgets(UIの部品 例:TextView、ImageView)がARで表示できるらしい
ここ。sceneformってなんぞやって話だけどこれできたらくっそ面白そうだと思ったので、
今回はTextViewをAR上に表示させるところまでやろうと思います。エミュレーターでARCoreアプリを動かすために
いやPixelとかGalaxyとか使いますからって方は飛ばしていいぞ。
てかARCore対応端末無いのにARアプリ作ろうとしてる人、どこからやる気が出てるんだ。Google Play 開発者サービス(AR)を入れます
https://github.com/google-ar/arcore-android-sdk/releases
ここからAPKをDLして、エミュレーターにドラッグアンドドロップしてインストールしてください。今回は記事作成時最新Ver(Google_Play_Services_for_AR_1.14.1_x86_for_emulator.apk)を入れます。
成功するとアプリ一覧画面に表示されると思います。
環境
なまえ あたい 端末 エミュレーター / Google Pixel 3 XL Android 10 minSdkVersion 24(ARCoreのせい) 実装
MainActivityができてる段階まで来てください。
build.gradle(appフォルダの中)書き足す
参考:https://developers.google.com/ar/develop/java/sceneform#configure-project
こうなっていると思いますが、
build.gradleapply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 buildToolsVersion "29.0.2" defaultConfig { applicationId "io.github.takusan23.aredittext" minSdkVersion 24 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }ここから書き足していきます。
build.gradleapply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 buildToolsVersion "29.0.2" defaultConfig { applicationId "io.github.takusan23.aredittext" minSdkVersion 24 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } //Java 8が必要みたい。 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { //ArFragmentなど implementation "com.google.ar.sceneform.ux:sceneform-ux:1.14.0" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }AndroidManifest書き足す
参考:https://developers.google.com/ar/develop/java/sceneform#manifest
まず上の方にこんなかんじに、<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="なんとか~"> <!-- Both "AR Optional" and "AR Required" apps require CAMERA permission. --> <uses-permission android:name="android.permission.CAMERA" /> <!-- Sceneform requires OpenGL ES 3.0 or later. --> <uses-feature android:glEsVersion="0x00030000" android:required="true" /> <!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. --> <uses-feature android:name="android.hardware.camera.ar" />
<application>
の中にも書き足します<!-- Indicates that app requires ARCore ("AR Required"). Causes the Google Play Store to download and install Google Play Services for AR along with the app. For an "AR Optional" app, specify "optional" instead of "required". --> <meta-data android:name="com.google.ar.core" android:value="required" />全部つけるとこうなります
AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.github.takusan23.aredittext"> <!-- Both "AR Optional" and "AR Required" apps require CAMERA permission. --> <uses-permission android:name="android.permission.CAMERA" /> <!-- Sceneform requires OpenGL ES 3.0 or later. --> <uses-feature android:glEsVersion="0x00030000" android:required="true" /> <!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. --> <uses-feature android:name="android.hardware.camera.ar" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <!-- Indicates that app requires ARCore ("AR Required"). Causes the Google Play Store to download and install Google Play Services for AR along with the app. For an "AR Optional" app, specify "optional" instead of "required". --> <meta-data android:name="com.google.ar.core" android:value="required" /> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>MainActivityにFragmentを置く。
参考:https://developers.google.com/ar/develop/java/sceneform#scene-view
ConstraintLayoutは使い方がよくわからないのでLinearLayoutに置き換えて、Fragmentをドラッグします。できたらArFragmentを選択してOKです!
layout_heightの値をmatch_parentにして最大まで広げるようにしましょう。
それとidもfragmentだとわかりにくくなるので、ar_fragmentとかにしときましょう。xmlだとこうなっていると思います。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity" > <fragment android:id="@+id/ar_fragment" android:name="com.google.ar.sceneform.ux.ArFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>Sceneformが利用可能かチェックする
https://developers.google.com/ar/develop/java/sceneform
には書いてありませんが、ARCoreのサンプルには書いてあったので、Kotlinに変換して使います。MainActivity.ktfun checkIsSupportedDeviceOrFinish(activity: Activity): Boolean { val MIN_OPENGL_VERSION = 3.0 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Toast.makeText(activity, "SceneformにはAndroid N以降が必要です。", Toast.LENGTH_LONG).show() activity.finish() return false } val openGlVersionString = (activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).deviceConfigurationInfo.glEsVersion if (java.lang.Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) { Toast.makeText(activity, "SceneformにはOpen GL 3.0以降が必要です。", Toast.LENGTH_LONG).show() activity.finish() return false } return true }これを
setContentView()
の前で使います。MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //条件満たしてなければActivity終了させる if(!checkIsSupportedDeviceOrFinish(this)){ return } setContentView(R.layout.activity_main) }ArFragment取得
MainActivityで置いたArFragmentを取得します。
MainActivity.ktlateinit var arFragment: ArFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //条件満たしてなければActivity終了させる if(!checkIsSupportedDeviceOrFinish(this)){ return } setContentView(R.layout.activity_main) //ArFragment取得 arFragment = supportFragmentManager.findFragmentById(R.id.ar_fragment) as ArFragment }起動してみる
ここまでで特に問題がなければ、
カメラの権限許可がきて、許可すると映ると思います。映れば成功です。ここまで間違えずついてこれてます。
いよいよViewを現実に表示させる・・・!
まずlayoutフォルダにar_layout.xmlという名前で作成します。
次に現実で表示させたいUI部品を並べます。
今回はTextViewを置きます。(タイトル詐欺回避)ar_layout.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffff" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10sp" android:gravity="center" android:text="てきすとだよー" /> </LinearLayout>ARで扱えるように
MainActivity.kt//レイアウトをARに・・・ lateinit var viewRenderable: ViewRenderable読み込みます。
MainActivity.kt//レイアウトを読み込む ViewRenderable.builder() .setView(this, R.layout.ar_layout) .build() .thenAccept { renderable -> viewRenderable = renderable } //読み込み成功 .exceptionally { //読み込み失敗 it.printStackTrace() Toast.makeText(this, "読み込みに失敗しました。", Toast.LENGTH_LONG).show() null }最後に押したらレイアウトをARで表示させるところを
MainActivity.ktarFragment.setOnTapArPlaneListener { hitResult, plane, motionEvent -> if (::viewRenderable.isInitialized) { //初期化済みのとき、利用可能 // Create the Anchor. val anchor = hitResult.createAnchor() val anchorNode = AnchorNode(anchor) anchorNode.setParent(arFragment.arSceneView.scene) // Create the transformable andy and add it to the anchor. val node = TransformableNode(arFragment.transformationSystem) node.setParent(anchorNode) node.renderable = viewRenderable node.select() } }これで動くはずです!!!実行してみましょう!!!
てきすとだよー
押したら消す
MainActivity.ktnode.setOnTapListener { hitTestResult, motionEvent -> node.isEnabled = false }isEnabledにtrueで消えますがこれでいいのかは不明。誰か頼んだ
ARで表示したTextViewのテキストを変更したい
MainActivityにEditTextとボタンを置きます。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity" > <fragment android:id="@+id/ar_fragment" android:name="com.google.ar.sceneform.ux.ArFragment" android:layout_width="match_parent" android:layout_weight="1" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android:id="@+id/ar_change_textview" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:ems="10" android:inputType="textPersonName" android:text="てきすとだよー" /> <Button android:id="@+id/ar_change_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="変更" /> </LinearLayout> </LinearLayout>そしたらボタンを押したらテキストを変更する処理を書きます。
MainActivity,.kt//テキスト変更 ar_change_button.setOnClickListener { //テキスト取得 val text = ar_change_textview.text.toString() //ARで表示するレイアウト取得 val linearLayout = viewRenderable.view as LinearLayout //TextView取得 val textView = linearLayout.findViewById<TextView>(R.id.textView) //変更 textView.text = text }動くはずです。
なんかEditTextの部分が黒くなるけどなんで?
写真にして保存する
おまけです。
疲れたのでコードだけ。レイアウトにボタンを追加してidを「ar_take_a_picture」にしてください。MainActivity.kt//撮影ボタン押したとき ar_take_a_picture.setOnClickListener { //PixelCopy APIを利用する。のでOreo以降じゃないと利用できません。 val bitmap = Bitmap.createBitmap( arFragment.view?.width ?: 100, arFragment.view?.height ?: 100, Bitmap.Config.ARGB_8888 ) val intArray = IntArray(2) arFragment.view?.getLocationInWindow(intArray) try { PixelCopy.request( arFragment.arSceneView as SurfaceView, //SurfaceViewを継承してるらしい。windowだと真っ暗なので注意! Rect( intArray[0], intArray[1], intArray[0] + (arFragment.view?.width ?: 0), intArray[1] + (arFragment.view?.height ?: 0) ), bitmap, { copyResult: Int -> if (copyResult == PixelCopy.SUCCESS) { //成功時 //ここのフォルダは自由に使っていい場所(サンドボックス) val mediaFolder = externalMediaDirs.first() //写真ファイル作成 val file = File("${mediaFolder.path}/${System.currentTimeMillis()}.jpg") //Bitmap保存 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream()) Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show() } }, Handler() ) } catch (e: IllegalArgumentException) { e.printStackTrace() Toast.makeText(this@MainActivity, "失敗しました。", Toast.LENGTH_LONG).show() } }?:の値がくっそ雑だけどゆるして
Android 10で動作確認済です。対象範囲別ストレージ対策。
写真データは
/sdcard/Android/media/パッケージID
に入っています。おわりに
GitHubに公開しておきます。
https://github.com/takusan23/AREditTextついでに参考にしたプロジェクトも。
https://github.com/google-ar/sceneform-android-sdk/tree/master/samples/hellosceneformPixelCopy参考にしました。
https://friegen.xyz/getdrawingcache-deprecated/おつです。888888