晴れ、時々Android。

クレヨンしんちゃんが人生のマニュアルなヘヴィロック系ひよっこAndroidエンジニアのブログ

DroidKaigi 2019 に参加してきた話 【Day 1】

去る先日、1年に1度全国の名だたるAndroidエンジニアが集まる夢の祭典DroidKaigi2019が開催されました。

2016年からAndroidを触り始めて、今年ついに夢叶い参加することができました。

複数のセッションが同時に行われるため、物理的に全てのセッションを生で聞けないのが残念でした。

筆者が聞いたセッション内容や感想などは、逐一↓でめっちゃ呟いてるので覗いてやってください。
twitter.com

セッション内容の詳細はスライドや動画に譲り、ここではざっくり内容や感想をつらつら書いていきたいと思います。

しょっぱなお詫び

公開のタイミングを見失い、なんと開催後4ヶ月も経っての投稿となってしまいましたこと、ここにローリング土下座で謝罪いたします。

会期・会場

  • 会期: 2019年2月7日(木)、2月8日(金)
  • 会場: ベルサール新宿グランド コンファレンスセンター

詳細はこちら↓

droidkaigi.jp

西新宿の大きなビル。福岡では見られない大きさのビルに「うわ〜〜!おっきなビルだな〜!」とかっぺ丸出しでしたね。
これまで参加してきた勉強会とは比べ物にならないスケールを肌で感じました。

ウェルカムトーク

www.youtube.com

近未来を感じさせる背景をバックに、登壇者の方々の名前が並んでいきます。
これから始まるワクワクを予感させるとても素晴らしいオープニングでした。

この高揚感をそのままに、DroidKaigiは幕を開けます。

1. マルチモジュールなプロジェクトでテストはどう変わるか?

speakerdeck.com

www.youtube.com

DeNA@fgfgtkmさんのご発表。

  • マルチモジュール化によるメリット
  • 各モジュールの独立テスト
  • モジュール間の結合テスト
  • マルチモジュールにおけるテストメトリクスの収集方法

を中心にお話しされてました。

特に各モジュールのメトリクスを出力して結果をマージできるPITは、結果も見やすくとても良さそうですね。

これからマルチモジュール化しようかな、と考えている方の背中を押してくれる内容でした。

2. マルチモジュールプロジェクトでのDagger2を用いたDependency Injection

speakerdeck.com

www.youtube.com

DMMの@kgmyshinさんのご発表。

Apple Pencilで手書き文字や図をふんだんに使ったスライドを作られていて、聴きながらイメージが沸きやすかったですね。(先生が黒板使って教えてるみたいな感じ)

Dagger2でのDIを総おさらいした後、マルチモジュールでどのようにDIしていくのか?マルチモジュールでのDIで注意すべきことは何か?という内容でした。

筆者はDaggerからしばらく離れていてDIにはKodeinを使っていましたので、個人的にとても良い復習になり、マルチモジュールにおけるDIのイメージができたのが良かったなと思います。

ただ、KoinKodeinの話が一切出ず、Daggerを使うことによるメリットをちゃんと感じ取ることができなかったのが少し残念だったかな・・・と個人的に感じました。(筆者がただ気づいてないだけかもしれないんですけどね・・・笑)

昼食

f:id:dashimaki_dofu:20190221013652j:plain 1日目はマネーフォワードさんがランチスポンサーで、お弁当をいただきました。

魚・肉・卵・雑穀米と栄養のバランスがしっかりとれていて素晴らしいお弁当で、自炊厨の筆者は感動し、今後の参考にしようとこっそり思いました笑

そして何よりおいしかった!!ありがとうございました!!

3. Master of Android Theme

speakerdeck.com

www.youtube.com

勝手に敬愛している@konifarさんのご発表。

やっとご尊顔を拝見できて、テンションが爆上げでした。
普段手放せないぐらいに愛用していて、弊社エンジニアにも浸透しているウォレットアプリKyashさんのAndroidエンジニアの方です。

Androidの画面(Activity / Fragment)における全体的な色使いを決めるThemeやStyleについて、まずその2つがどのようなものなのか?そしてThemeやStyleをどのように使い分けるのか?コードから動的にThemeやStyleを設定するにはどうしたらいいのか?などをお話しされていました。

  • ThemeやStyleのどの要素がどこにあたるのかをしっかり理解していれば、ダークモードやナイトモードの実装などが簡単にできるよ!
  • 目が回るほどあるAttributeも7つのみのカテゴリに分けられているので、カテゴリからだけ覚えると理解が楽だよ!

という2点が特に印象に残り、少し苦手意識のあったThemeやStyleへの壁がひとつ取れた気がします。

ちなみに今回から導入された同時通訳システムを使って英語で発表されていたセッションでした。

席にそれぞれレシーバーがあり、イヤホンを使ってリアルタイムで和訳⇄英訳が聞けます。

外国の方もいらっしゃいますし、英語もリスニングも苦手な筆者のような日本人にとってとても親切な試みだったと思います。

ぜひ来年以降も続けて欲しい・・・!

セッション動画に和訳は入っておりませんが、@konifarさんが、日本語で各スライドの補足を載せてくださってるのでこちらもご覧くださいませ↓

konifar.hatenablog.com

4. Understanding Kotlin Coroutines: コルーチンで進化するアプリケーション開発

speakerdeck.com

www.youtube.com

DroidKaigiオーガナイザーでもある@mhidakaさんのご発表。

Kotlin 1.3で正式リリースとなったCoroutine機能について基礎〜実践的な使い方を扱った内容です。

リリースとなってからまだ日も浅く、筆者も含め参加者の皆さんの注目も集めており、会場はあっという間に立ち見も埋まるぐらいの大盛況に。

筆者が今回のDroidKaigiで一番聞きたかったセッション。
そして一番聞けなかったセッション。

というのも、私事なんですがセッション中に母から「父が急性胆嚢炎で倒れた」という電話があり(手術の甲斐あって現在は退院)、重要なところをほとんど聞けなかった・・・。

ほとんど「??」な状況だったので、もう一度スライドや資料を使って学び直そうと思います。

@mhidakaさん、すみません・・・泣

Firebase Chat & After Party

Day1の全セッション後、DroidKaigiスタッフとDroidKaigi2019公式アプリのコントリビュータによるFirebase ChatとAfter Partyが開催されました。

公式アプリ開発での苦労話・笑い話が飛び交う中、ビールがデプロイされ、皆さんお酒を片手に談笑したり終始和やかな雰囲気でした。

f:id:dashimaki_dofu:20190221025001j:plain
Firebase Chatの様子。風船を持っているのがセッション登壇者の方ですね。

f:id:dashimaki_dofu:20190221025027j:plain
After Partyは立食ビュッフェスタイル。 筆者の盛り方&食べかけきたねぇ。

f:id:dashimaki_dofu:20190221025058j:plain
スイーツにはかわいいドロイド君のケーキが。もうかわいすぎて食べられない。

Day 2へ続く・・・

【Android 8.0】TextInputLayoutをフォーカスするとNullPointerExceptionでクラッシュする現象への対応

しゅっぱつおしんこ〜。しんちゃんだz(ry

文字入力欄などで、Hint表示やバリデーションエラーメッセージなどを表示するのにTextInputLayoutを使うことがしばしばありますよね。

今回は、これを使って実装していた時に特定OSで起きていた問題に対してのアプローチを書いていこうと思います。

起きていた不具合

TextInputLayoutで作った入力欄をフォーカスすると、以下のNullPointerExceptionを吐いてクラッシュすることがある

レイアウトはこんな感じ

<android.support.design.widget.TextInputLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:hintAnimationEnabled="true"
    app:hintEnabled="true"
    app:errorEnabled="false">

    <android.support.design.widget.TextInputEditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="プレースホルダーかつヒントだよ" />

</android.support.design.widget.TextInputLayout>

スタックトレース

java.lang.NullPointerException: 
  at android.app.assist.AssistStructure$WindowNode.<init> (AssistStructure.java:484)
  at android.app.assist.AssistStructure.<init> (AssistStructure.java:1908)
  at android.app.ActivityThread.handleRequestAssistContextExtras (ActivityThread.java:3056)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1820)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:6592)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:240)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:769)

不具合が起こった条件

  • Android 8.0.
    • 8.1以降は発生しない
  • Android8.0から実装されたAutofillが有効になっている。
  • 動的にsetText()でテキストがセットされたTextInputLayoutをフォーカスする

解決法

その1

hintを挿入する位置を TextInputEditText -> TextInputLayout に入れ替える

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="ヒントはここに書くよ"
    app:hintAnimationEnabled="true"
    app:hintEnabled="true"
    app:errorEnabled="false">

    <android.support.design.widget.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.design.widget.TextInputLayout>

その2

TextInputEditTextに以下の属性を追加する。

android:importantForAutofill="noExcludeDescendants"

原因

調べてみると、公式でも共有されている8.0特有のバグだそう↓ https://issuetracker.google.com/issues/62834931

内容を追ってみると、

  • 8.0のプレビューでこのバグは把握されていた
  • 正式版リリースで直したつもりだったけど直ってなかった!
  • 8.1で直したよ!

    The issue is fixed on Android 8.1.

ということで、8.0にのみ残るバグとなってしまったみたい。かなしみ・・・。

まとめ

  • hintはTextInputEditTextではなく、親のTextInputLayoutに書こう!
  • それかTextInputEditTextにandroid:importantForAutofill="noExcludeDescendants"を追加したらいいよ!
  • Googleもこのバグは把握していて、8.1で直したって言ってるよ!

おわりに

色んなアプリでこのような入力欄を作ることはたくさんあると思うし、8.0ももちろんサポートすることになるからこの問題は避けて通れないなぁ・・・

皆さん気をつけましょう・・・!!

じゃ、そゆことで〜

【Android】WebViewでJavaScriptと闘ったお話 〜LocalStorageへ認証情報を保存する〜

こんにちわんこそば、しんちゃんだゾ。(しつこい)

引き続きWebViewのお話。 WebViewでフロントエンドアクセスする時に、ログイン情報を引き継いだままアクセスしたいという課題がありまして、簡単にできるかなーと思ってたらかなり長い闘いになったので、残していきたいと思います。

実現したいこと

WebViewでフロントエンドにアクセスした時に、アプリ側でのログイン情報(=認証情報)を引き継ぎたい!

困ったこと

WebViewで認証情報(Auth-Key、Access-Token、Clientなど)を添えてアクセスする時は、

WebView#loadUrl(url: String, Header: HashMap<String, String>)

で、認証情報をヘッダに入れてアクセスするのが基本的ですね。
これで簡単にできるじゃーん、とか思ってました。最初はね。

でも罠があったんですね。

ヘッダで渡した認証情報はloadUrl()で渡したURLでのみ有効・・・

・・・へ?

つまり、loadUrl()で読み込んだURLから別のページに遷移すると、認証情報が失われちゃうんですね・・・これは困った。

解決法

WebViewのLocalStorageに認証情報を保存して、ページを遷移する際に都度LocalStorageの認証情報を取得し必要に応じて更新する

LocalStorageは、Cookieなどと同様にブラウザに対して永続的にデータを保存する仕組みです。

JavaScriptを有効にする

val webView = findViewById<WebView>(R.id.webview)
webView.settings.javaScriptEnabled = true

② WebStorage(LocalStorage)の利用を許可する

webView.settings.domStorageEnabled = true

③ WebViewClient#onPageFinished() でヘッダで送信した認証情報をLocalStorageから取得する。その際ローカルで保存している認証情報と照合させ、もし一致していなかったならLocalStorageを更新する。

private val webViewClient = object : WebViewClient() {

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        if (view != null) {
            // LocalStorageの認証情報の読み込み
            view.evaluateJavascript("""localStorage.getItem("auth-info")""") { resultString ->
                val authKey = "some-authKey"
                val accessToken = "some-accessToken"
                val client = "some-client"
                try {
                    if (resultString != "null") {
                        val json = JSONObject(resultString.toParsableJsonString())
                        val isSameAuthKey = json.getString(KEY_AUTH_KEY) == authKey
                        val isSameAccessToken = json.getString(KEY_ACCESS_TOKEN) == accessToken
                        val isSameClient = json.getString(KEY_CLIENT) == client
                        // ローカルの認証情報と一致したなら何もしない
                        if (isSameAuthKey && isSameAccessToken && isSameClient) return@evaluateJavascript
                    }
                    // LocalStorageへの認証情報の保存
                    val authInfo
                            = """{ "accessToken": "${accessToken}", "authKey": "${authKey}", "client": "${client}" }"""
                    view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)
                }
                // パースエラー時
                catch (e: JSONException) {
                    // 何かcatch処理
                }
                // getItemで取得した結果のJSON成型失敗時
                catch (e: IndexOutOfBoundsException) {
                    // 何かcatch処理
                }
            }
        }
    }
}

private fun String.toParsableJsonString(): String
        = this.substring(this.indexOf("{"), this.lastIndexOf("}") + 1).replace("""\"""", """"""")

注意が必要なところ

LocalStorageへ認証情報を保存する↓のところ

val authInfo = """{ "accessToken": "${accessToken}", "authKey": "${authKey}", "client": "${client}" }"""
view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)

JSONで認証情報を保存するので、authInfoJSON形式のStringにしているが、これをそのまま

view.evaluateJavascript("""localStorage.setItem("auth-info", $authInfo)""", null)

として保存すると、それ以降でLocalStorageから認証情報をJSON形式で取得できなくなってしまう。
(取得しに行った時のレスポンスが[object Object]となってしまいパースできなくなる)

文字列で渡しているんですが、JavaScript余計なお節介で勝手にJSONオブジェクトと認識して格納してしまうのが原因のよう。
ですので、一旦これを文字列に変換させる必要が。

そこで、JSON形式のデータを文字列に変換するメソッドであるJSON.stringify()を噛ませてあげることでしっかり文字列として保存され一件落着になります。
(これに半日つまずいてたなんて言えない・・・)

view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)

まとめ

  • LocalStorageを使えば認証情報を引き継げるよ!
  • localStorage.getItem()でLocalStorageから値を取得して、localStorage.setItem()で値をセットするよ!
  • localStorage.setItem()するときは、渡した文字列が勝手にJSON形式に変換されて正しく保存されなくなっちゃうので、JSON.stringify()を渡す文字列に嚙ますんだよ!

おわりに

iOSのWebViewはここの認証情報の保存を勝手によしなにやってくれるみたい。
AndroidのWebViewってめんどくs(ry

じゃ、そゆことで〜

参考

↓の記事のおかげで助かりました、ありがとうございました。
www.tam-tam.co.jp

【Android】 WebViewのheightがなかなか表示コンテンツのheightに合ってくれなくて困ったお話

お久しぶりです。しんちゃんだゾ。(大嘘) 意気揚々と初回エントリ書いて1ヶ月が経ってしまいましたが、今からちょっとずつ日々得たTipsを発信できればと思います。

今日はWebView周りで少し困ったことがあったので、備忘録で残しておきたいと思います。 重箱の隅をつつくようなTipsですが、温かい目で見ていただけると幸いです。

実現したいこと

AppBarLayoutとBehaviorを連動させたWebViewを表示したい!

困ったこと

AppBarLayoutとWebViewのBehaviorを連動させるには、WebViewをNestedScrollViewでネストさせ、android:isScrollContainer="false"を設定する必要があります↓

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:isScrollContainer="false"/>

</android.support.v4.widget.NestedScrollView>

これで、無事AppBarLayoutとBehaviorが連動するようになりましたが、今度はWebViewで表示している表示コンテンツのheightとNestedScrollViewのheightが合ってくれない・・・

じゃあNestedScrollViewのheightをwrap_contentにすればいいんじゃない?

と思い書いてみたら、なんかIDEから嫌な赤線が引かれた。

Placing a <WebView> in a parent element that uses a wrap_content size can lead to subtle bugs; use match_parent

・・・

WebViewをネストするViewはwrap_contentになってるとバグ起こすからmatch_parentを使え!! とのこと・・・

コンパイルエラーを起こすわけじゃなく、tools:ignore="WebViewLayout"をWebViewに追記すれば警告をsuppressすることはできます。
だけど「バグ起こすぞ」って言われたことを無視するまるで反抗期の子供みたいなことはしたくないので、なるべくバグがない安全な方法で実現したいわけです。

試してみたこと

その1: WebViewClient#onPageFinishedでWebView.contentHeightを取得してNestedScrollViewのHeightにセットする

ページのロードが終了したタイミングでコールバックされるonPageFinished()を使ってWebViewのcontentHeightを取得して、それをNestedScrollViewのheightにセットすればいいんじゃないか?

override fun onPageFinished(view: WebView?, url: String?) {
    super.onPageFinished(view, url)
    binding.scrollView.layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            view?.contentHeight ?: 0
    )
}

結果: onPageFinished()で正しいcontentHeightは取得できず撃沈

その2: WebViewのレイアウトが再レンダリングされる時にcontentHeightを捕まえに行く

だったら、WebViewのcontentHeightが変更され、再度レンダリングされるタイミングでNestedScrollViewのHeightにセットすれば・・・!!

結果: そんなコールバックメソッドなんてなかった

こうやってできた

方法: WebViewをScrollViewでネストして、それを丸ごとNestedScrollViewでネストする

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <!-- NestedScrollViewのlayout_heightをwrap_contentにすることで、子のコンテンツに応じてheightを変えるようにする -->

    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"> 
       <!-- WebViewの直接ネストしているScrollViewのlayout_heightはmatch_parentにする-->

        <WebView
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:isScrollContainer="false"/>

    </ScrollView>

</android.support.v4.widget.NestedScrollView>

単純だったよ・・・パトラッシュ・・・

おわりに

レイアウト上で簡単に対応できて良かったです。(傷だらけ)

コード上でダイナミックに変更すると、ある条件では動作しなかったり、ヒューマンエラーを起こす可能性も高くなるので。

同じことでお困りの方がいらっしゃれば、手助けになると幸いです。

じゃ、そゆことで〜〜

クレヨンしんちゃん系ひよっこAndroidエンジニアが生意気にもブログを始めたお話。

どうも、みなさん、はじめまして。

福岡でひよっこAndroidエンジニアをやってる者です。
詳しい自己紹介や経歴は次のエントリに譲るとして、ここでは簡単にご挨拶をさせていただきまする。

どんな人?

f:id:dashimaki_dofu:20180803165502j:plain
平成の終わりに三十路を迎えるAndroidエンジニア。
ひよっこ
クレヨンしんちゃんが人生のマニュアル。
人畜無害力の高い風貌とは似合わず、ハードなヘヴィロック・ラウドロックを好む。
浸透してもないし、浸透する必要もないと思いますが、一応日本と中国のハーフ。
(アジアンハーフの特徴のなさは異常)

どんなこと書くの?

Androidアプリ開発に関する備忘録やらTipsやらを書いていこうと思ってます。
今まで自分のひよっこさから情報の発信をためらっていましたが、個人的な備忘録がてら情報発信の場として使っていこうと思います。
少しでも同じ悩みを持っている人の助けになれば幸いだゾ。
個人的な趣味も書いていくかも。

おわりに

Twitterやってます。リンクはこちらから
喜ぶので是非覗いてあげてください。