20191223のAndroidに関する記事は13件です。

Androidアプリでのエラー処理について

この記事はCAM Advent Calendar 2019 の24日目の記事です。:golf:
昨日は@takehziさんのトランザクションとロックと二重課金でした。

:warning: エラー処理って何?

Androidに限らず、クライアントアプリ全般的に言えることですが、よく「エラーが発生しました。もう一度やり直してください。」みたいなポップアップを見かけると思うのですが、それを表示するまでに行う処理のことをここでのエラー処理とします。

ここで紹介するのはあくまで一つの手法です。

:thinking: やること

1. エラーを受け取る
2. 受け取ったエラーに対してそれぞれ処理を分ける

... 以上

と言っても、これだけだとさっぱりなので順を追って説明できればと思います。

1. エラーを受け取る

この受け取り方はそれぞれだと思いますが、今回はtry-catchExceptionなりを受け取る想定で書いていきます。

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などを返してもいいですが、RxJavaLiveDataを使っているのであればそちらを使ってView側へ通知を流す形にするとスッキリ書けます。

まとめ :notebook:

Exceptionクラスを継承したクラスを自作してのエラーハンドリングを紹介しました。
久しぶりの技術ブログだったので何を書こうか迷った挙げ句、基本的な内容になってしまいましたが、これからは定期的に書いていければと思っています。

:santa: 明日は@kidoyunaさんの記事が公開予定です。ぜひそちらもご覧ください。:eye:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

人種も育ってきた環境も違う二人がともに生活を始め、価値観の違いを埋めながら成長し、まだ見ぬ未来に希望をつなげる話

はじめに

先にタイトルのオチを書いてしまいましょう!

人種も育ってきた環境も違う二人がともに生活を始め、価値観の違いを埋めながら成長し、まだ見ぬ未来に希望をつなげる話

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
猫が好きな方はぜひ読んでみてください♪

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】はじめてのRoom

【Android】分かった気になれる!アーキテクチャ・MVVM概説 ではアーキテクチャ・MVVMの概要をコードを記述せずに概念のみ説明してみました。
その後の投稿では、実践編として各ライブラリを実際にコードを記述し記事にしています。
これまでの記事 :

MVVMシリーズ実践編第二回目のテーマは、Roomです。
DBの一つであるSQLiteを容易に扱うことのできるRoomライブラリについて記事を書こうと思います。

下図のMVVMの構造において、Roomが担うのは黄緑の枠線で示された部分です。
参照:https://developer.android.com/jetpack/docs/guide#overview

mvvm_room.png

はじめに

本記事では、Roomを使用しDB(SQLite)でのデータ保存・取得処理を実装します。
今回は、添付の画面キャプチャーのようなアプリを例に説明していきます。
このアプリは、前回記事:【Android】はじめてのDataBindingで作成したアプリにDBへのデータ保存機能を追加したものです。
「SET!!!」ボタンを押下したときに入力した文字列(Play Call)をDBに保存します。

(
Play Callとは
アメリカンフットボールの試合中に伝えられる作戦のことです。
本アプリでは、入力した文字列をPlay Callに見立てて各種オブジェクトを命名しました。
)

データ保存の流れ 次回アプリ起動時
room_american_football.gif Screenshot_1577005696.png

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.kt
class 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.kt
class 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の適用
    現在AsyncTaskDAOを呼び出していますが、Coroutinesに移行したいです。
    AsyncTaskだとデータを取得するためのクラスや保存するためのクラスを独自に定義する必要があり、ViewModelのファイルが肥大化してしまうからです。
    また、コンストラクタとしてViewModelDaoを渡しており、この部分が回りくどく冗長であると感じるからです。
    )

まとめ

今回は、Roomを用いてSQLiteでのデータ管理を実装してみました。
SQLiteの知識に乏しいため不明な点も残っていますが、基本は抑えられたのではないかと考えています。
今後は、CoroutinesやDIの実装を進めアーキテクチャMVVMシリーズ実践編を完結する予定です。

コメント、編集依頼は絶賛募集中です!

ソースコード

ソースコードはGitHubにあげています。
https://github.com/iTakahiro/ArchitectureFootball

関連記事

参考にした資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリを見比べたいじゃない

アプリを見比べたい

アプリもリリースしてから年月が経つと

「今のアプリの動きってどんなだっけ。。。」

「あれ?改修後はどうなるんだっけ??」

といった具合で改修前、改修後のアプリを見比べたい時なんかが発生してきます

ですが普通にアプリを端末にインストールするとPackege(ApplicationID)が一緒なもんだからアプリを上書きインストールしてしまうんですよね。。。

あーこまった。。

そんな時にFlavor

そう、そんな時に役立つのがFlavorなんです

前回こちらの記事でConstファイルとか切り替えられるよっていう話をしたんですけど、Flavorさんが変えられるのはファイルだけじゃなかった

接頭詞をつけちゃうよ

今回Flavorにやってもらう仕事はApplicationIDに接頭詞をつけることです
開発APIサーバに向いているFlavorにはdev
スクリーンショット 2019-12-23 20.38.22.png
本番APIサーバに向いているFlavorにはprod
スクリーンショット 2019-12-23 20.37.53.png

みたいな感じね

ついでにアプリの名前も変えちゃおう
開発API向きには「テスト」
スクリーンショット 2019-12-23 20.41.52.png
本番API向きは「本番」
スクリーンショット 2019-12-23 20.41.32.png

とかつけちゃう

これでインストールしてみよう

いいかんじ

Screenshot_20191223-204809.png

見事「本番」アプリと「テスト」アプリを2つ一緒にインストールできました

名前もパット見でわかりやすくていいですね

Flavorを増やせば好きなようにアプリを見比べることができるので

この小技はおすすめです

では

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ぶっちゃけ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アプリ開発は、アプリ開発にあたってとりあえず知っておくべきことをズバリ書きます。例外はありますのでご了承ください。
勉強中の身でありますので、誤りなどあればご指摘いただけると有り難いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Epoxyを使ってスクロールがかくつくと思ったらリサイクルされていなかった

この記事はand factory Advent Calendar 2019の24日目の記事です

きっかけ

とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。

対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。

LinearLayout // rootのViewGroup
  ∟ RecyclerView
  ∟ RelativeLayout
  ∟ RecyclerView
  ∟ RecyclerView
  ∟ LinearLayout
  ∟ ...
  • 要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。

  • Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです :relaxed:

LinearLayoutをEpoxyに切り替える

  • 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードをEpoxyModelWithHolderを拡張したクラスに移動しました

動作テスト

  • 起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。

  • 古い端末だとスクロールでちょいちょいかくつきが気になります。

  • 上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。

EppxyがViewPoolを管理している箇所

ActivityRecyclerPool.kt
internal 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が別々の実装を持っていることがなんとなくわかります。

  • ActivityRecyclerPool.kt

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が発生していたため、古い端末でかくつきが起こっていました。:zap:

対応

  • EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。

まとめ

  • EpoxyはネストされたEpoxyでもViewPoolをがデフォルトで共有してくれるので便利です。
  • Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
  • 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Epoxyのスクロールがかくつくので調べてみたらリサイクルされていなかった

この記事はand factory Advent Calendar 2019の24日目の記事です

きっかけ

とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。

対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。

LinearLayout // rootのViewGroup
  ∟ RecyclerView
  ∟ RelativeLayout
  ∟ RecyclerView
  ∟ RecyclerView
  ∟ LinearLayout
  ∟ ...
  • 要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。

  • Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです :relaxed:

LinearLayoutをEpoxyに切り替える

  • 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードをEpoxyModelWithHolderを拡張したクラスに移動しました

動作テスト

  • 起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。

  • 古い端末だとスクロールでちょいちょいかくつきが気になります。

  • 上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。

EppxyがViewPoolを管理している箇所

ActivityRecyclerPool.kt
internal 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が別々の実装を持っていることがなんとなくわかります。

  • ActivityRecyclerPool.kt

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が発生していたため、古い端末でかくつきが起こっていました。:zap:

対応

  • EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。

まとめ

  • EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
  • Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
  • 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Epoxyのスクロールがかくつくので調べてみたらリサイクルされていなかったので

この記事はand factory Advent Calendar 2019の24日目の記事です

きっかけ

とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。

対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。

LinearLayout // rootのViewGroup
  ∟ RecyclerView
  ∟ RelativeLayout
  ∟ RecyclerView
  ∟ RecyclerView
  ∟ LinearLayout
  ∟ ...
  • 要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。

  • Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです :relaxed:

LinearLayoutをEpoxyに切り替える

  • 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードをEpoxyModelWithHolderを拡張したクラスに移動しました

動作テスト

  • 起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。

  • 古い端末だとスクロールでちょいちょいかくつきが気になります。

  • 上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。

EppxyがViewPoolを管理している箇所

ActivityRecyclerPool.kt
internal 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が別々の実装を持っていることがなんとなくわかります。

  • ActivityRecyclerPool.kt

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が発生していたため、古い端末でかくつきが起こっていました。:zap:

対応

  • EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。

まとめ

  • EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
  • Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
  • 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Epoxyのスクロールがかくつくので調べたらリサイクルされていなかった

この記事はand factory Advent Calendar 2019の24日目の記事です

きっかけ

とあるページの初回表示を高速化するためにをEpoxyのRecyclerView(以下、Epoxyと書きます)を使うようリファクタしました。

対象となるページの構成はこちらです。rootがLinearlayoutでその下にいくつかのViewをぶら下がっています。

LinearLayout // rootのViewGroup
  ∟ RecyclerView
  ∟ RelativeLayout
  ∟ RecyclerView
  ∟ RecyclerView
  ∟ LinearLayout
  ∟ ...
  • 要素毎にaddViewしています。ただ、要素数がそれなりに多く、ページ初回表示するときに画面外の要素の処理も動いていました。

  • Epoxyでは画面外要素の処理はページ表示した時に動かないので、ページ表示の高速化が期待できそうです :relaxed:

LinearLayoutをEpoxyに切り替える

  • 先ほどのrootに配置していたLinearLayoutをEpoxyに切り替えて、子ビューのコードをEpoxyModelWithHolderを拡張したクラスに移動しました

動作テスト

  • 起動時のページ表示が早くなった(体感)気がします。少なくとも画面外の要素の処理は走っていないです。

  • 古い端末だとスクロールでちょいちょいかくつきが気になります。

  • 上下のスクロールを何度繰り返してもカクツクので、キャッシュが効いていなさそうです。EpoxyのViewPool周りの実装をみていきました。

EppxyがViewPoolを管理している箇所

ActivityRecyclerPool.kt
internal 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が別々の実装を持っていることがなんとなくわかります。

  • ActivityRecyclerPool.kt

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が発生していたため、古い端末でかくつきが起こっていました。:zap:

対応

  • EpoxyとRecyclerViewの混在するとViewPoolの共有が上手くいかないことがわかったので、RecyclerViewをEpoxyに変更して古い端末でもカクツキが軽減されたことを確認しました。

まとめ

  • EpoxyはネストされたEpoxyでもViewPoolをデフォルトで共有してくれるので便利です。
  • Epoxy使っているのにカクツクことがある場合は、子ビューにRecyclerViewが紛れていないか確認すると良いかもしれません。
  • 公式でViePoolのことがちゃんと書いてあるので、しっかり読むの大事だなと思いました

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
import '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 シリーズを使ってもよいのかも
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今年はクリぼっちが本当に少ない / Flutter

今年はクリぼっちが本当に少ない / Flutter
今年もやってまいりました!!クリスマス!

この記事でわかること

  • Flutterでアプリを作りましたので、その技術内容と悩み
  • 恋をすることの素晴らしさ
  • クリスマスの考察

この3つを中心に書いていければなと思います。

私の周りでクリぼっちが減った!?

最近、私の周りでは幸せ報告が後を絶えません。

いわゆる結婚ラッシュってやつでしょうか。

  • 結婚しましたー(例のシンデレラの人)
  • 結婚したぷるぷる!(大学の先輩)
  • 結婚しました〜(インスタグラム多数)
  • 付き合った報告(思い切って恋をしてみました!)

しかもそれだけでなく、恋をした効果なのか

  • 結婚(シンデレラ) → 勇気を出してデザインのフリーランスになれた!
  • 結婚(大学の先輩) → CEOでめちゃめちゃ稼ぐ
  • 付き合った報告 → CTOになれた!

など恋をしつつも自分にコミットできている方が今年は多いと感じられました。

いや逆に、恋をしているからもっと頑張れるわけです。

やっぱり恋ってすごい

あらためて恋ってすごいなーと思います。

昔から、「衣食住」といいますが

「衣食住恋」で4大要素として入れるべきだと思います。

  • 恋はパワー
  • 恋は頑張れる
  • 恋駆動開発したほうが生産性が上がると最近すごく思います!!!

コードギアスのシャーリーさんもこう言っていましたよね

「恋はパワーなの!誰かを好きになるとね、すっごいパワーが出るの。
毎日毎日その人のことを考えてアプリを書いちゃったり、
早起きしちゃったり、
マフラーを編んじゃったり、
CIを組んじゃったり、
滝に飛び込んでその人の名前をリファクタしたり、
私だって…!…その、ルルにはないの?誰かのために、いつも以上の何かが」

(コードギアスより)

ということで、今日からは「恋駆動開発」を応援し

生産性をあげよう!!!ということで

思い出フォトアプリを作りました!!

ヒューチャーシンク.png

「まずは、ノープランでお出かけしてください。」

  • 1: 写真スポットに近づくと通知が来ます
  • 2: カメラは好きなカメラでとって下さい
  • 3: Instagramでどんな写真が投稿されているかわかります

IMG_2375 (1) copy.png

技術要素

  • 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)と比べて強い勢いを感じており、普及はもう少し進むのではないでしょうか

    実際に勢いを調べてみました

Screen Shot 2019-12-13 at 11.25.55.png

これは勢いがありそうですね

実際、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

で実行するもできない

○Twitter
○Tumblr
○投稿型にする
○画像を表示しない
○手作業で2000件選ぶ
などなど手はありそうではあるが

有識者の方アドバイスください。

2: GitHub Actionsでアプリを配信するようにした

  • MacOSのビルドはLinuxのコンテナに比べて10倍料金が高いため、何度かfastlane実行してたりテストしてたらお金がかかっていた(従量制にしていた) -> ブランチ制限などで対応

やっぱりものづくりって面白い

  • 何より楽しい!
  • みんなが使ってくれることを想像し、にやにやしてしまいます。結局これって恋と同じかもしれません。

クリぼっちは少ないのか

  • 私の周りの方で、恋をしてみようかなと思ってくださって行動した方がいます。
  • そして見事に、恋の良さに気づいて生産性がバク上がりした人もいます。(よく逆に捉えられます)
  • 感謝をしてくれたのがとても嬉しかったです。

結論:私の身の回りでは「クリぼっちが少ない」

恋をしよう!!

  • ペアーズとWithはおすすめです!

https://www.pairs.lv/
https://with.is/welcome

クレジット

まとめ

  • Flutterは勢いがある!
  • 恋をして生産性をあげよう!

そしてこの度、no plan(ノープラン)株式会社を立ち上げて初のサービスリリースになりました!

今回のグラマブルは
ノープランのまま散歩しても、思い出をしっかりと残せるサービス となり

ノープラン株式会社が、
ノープランの人たちに、
ノープランであることが幸せに思える
ノープランファーストなサービスを作ろうと思います

ノープランファーストをまずは1つ実現できたのではないかと思います!!!

アプリは即興なので使いにくいところありますがブラッシュアップできればと思います!!!

ありがとうございました!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidアプリを公開する

初めてAndroidアプリを公開したので手順をメモします。

準備するもの

手順

Google Play Consoleにログインします。
image.png

すでに内部テストなどでアプリ作成が完了している場合には該当のアプリを選択します。新規作成の場合には[アプリの作成]をクリックし、言語とタイトルをつけて作成してください。
私は内部テスト後なので作成されたアプリをクリックして進めます。
左メニューからリリース管理>アプリのリリースをクリックします。
image.png
製品版トラックの[管理]をクリックします。
image.png
[リリースを編集]をクリック
image.png
[追加するAndroid App Bundle とAPK]の中にAPKファイルを追加します。
image.png

内部テストと同じファイルをアップロードする場合にはドロップするとアップロードエラーになるのでライブラリからの追加を行います。
image.png
リリース名とリリースノートを入力して保存後確認します。
image.png
ストアの掲載情報を埋めます。
image.png

ストア掲載情報の登録後、またアプリのリリース>製品版管理>リリースを編集をクリックします。
画面右下の[確認]をクリック
image.png
画面右下の[製品版として公開を開始]をクリック
image.png
ポップアップで確認をクリック。
image.png
もうこれで公開されたかのように思えてしまいますが
image.png
ダッシュボードを見ると審査中になっていることがわかります。
image.png

また動きがあったら更新します。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android / Kotlin / ARCore]ARCoreでTextViewを出す

AR触ってみたい。

widgets(UIの部品 例:TextView、ImageView)がARで表示できるらしい

https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

Screenshot_1577032540.png

ここ。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)を入れます。

成功するとアプリ一覧画面に表示されると思います。

image.png

環境

なまえ あたい
端末 エミュレーター / Google Pixel 3 XL
Android 10
minSdkVersion 24(ARCoreのせい)

実装

MainActivityができてる段階まで来てください。

build.gradle(appフォルダの中)書き足す

参考:https://developers.google.com/ar/develop/java/sceneform#configure-project

こうなっていると思いますが、

build.gradle
apply 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.gradle
apply 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をドラッグします。

image.png

できたら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.kt
fun 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.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    //条件満たしてなければActivity終了させる
    if(!checkIsSupportedDeviceOrFinish(this)){
        return
    }
    setContentView(R.layout.activity_main)
}

ArFragment取得

MainActivityで置いたArFragmentを取得します。

MainActivity.kt
lateinit 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
}

起動してみる

ここまでで特に問題がなければ、
カメラの権限許可がきて、許可すると映ると思います。

image.png

image.png

映れば成功です。ここまで間違えずついてこれてます。

いよいよViewを現実に表示させる・・・!

参考:https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

まずlayoutフォルダにar_layout.xmlという名前で作成します。
image.png

次に現実で表示させたい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で扱えるように

参考:https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

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.kt
arFragment.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()
    }
}

これで動くはずです!!!実行してみましょう!!!

Screenshot_1577028389.png

てきすとだよー

押したら消す

MainActivity.kt
node.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
}

動くはずです。

Screenshot_1577029034.png

なんか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/hellosceneform

PixelCopy参考にしました。
https://friegen.xyz/getdrawingcache-deprecated/

おつです。888888

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む