Dagger によって Fragment に注入されるオブジェクトを Fragment.setUserVisibleHint の中で使う場合の注意

次のようなコードがあったとして、

class FooFragment : Fragment() {
    @Inject
    lateinit var bar: Bar

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        bar.baz()
    }
}

ここで setUserVisibleHint(Boolean) のドキュメントを見てみると、

https://developer.android.com/reference/android/support/v4/app/Fragment#setuservisiblehint

Note: This method may be called outside of the fragment lifecycle. and thus has no ordering guarantees with regard to fragment lifecycle method calls.

Fragment のライフサイクルに関係なく呼び出されるということはつまり、 onAttach(Context) で初期化される前提の barsetUserVisibleHint(Boolean)の中で参照しようとする上記のコードは NG なのか。 ということで、どうしよっかって感じ。

NonNull -> Nullable

bar を Nullable にする方法が考えられるけど、本来 bar は NonNull 前提で扱いたい。 Fragment に対してコンストラクタインジェクションが使えないからしょうがなく onAttach(Context) で初期化せざるを得ないけど、 onAttach(Context) で必ず初期化されるからという絶対前提があることで NonNull で lateinit にしてたのもある。

class FooFragment : Fragment() {
    @JvmField
    @Inject
    var bar: Bar? = null

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        bar?.baz()
    }

isInitialized

isInitialized で初期化されているか判定する方法が考えられる。

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/is-initialized.html

絶対初期化されるから!と宣言してるのに isInitialized で判定するのは矛盾している気がする。 判定処理の実装漏れの要因になりそうだけど、そもそも setUserVisibleHint(Context) を使う上では setUserVisibleHint(Context) の挙動は知っておこうぜみたいなところがあるので、やむなしって感じ。

class FooFragment : Fragment() {
    @Inject
    lateinit var bar: Bar

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        if (this::bar.isInitialized) {
            bar.baz()
        }
    }
}

まとめ

setUserVisibleHint(Context) 以外でも bar を参照するのであれば後者を選ぶのがいいのかな。 setUserVisibleHint(Context) のために Nullable にするのは微妙だけど、どちらを選んでも微妙なところ。 isInitialized が実装された背景が気になる。

未婚の自分が「エンジニアとデザイナの夫婦 FM」 第 1 , 2 回目を聴いて

聴きながら感想を書きなぐろう

パパエンジニアが日々どうやって家族と過ごしたり、仕事したり、子育てしたり、技術を磨いたりしてるのかなー知りたいなーってところから、そんなpodcastないなーってところで勢いでやってみました。

note.mu

note.mu

こうして聴き始めました

書きなぐった文章の前に、聴いたキッカケを。

31 歳で未婚の自分。 20 代と比べて金銭的余裕は生まれ、未婚がゆえ自分の自由時間は多い。

そんな中で、将来は結婚して家庭を持ちたいという願望もあり、そうなるために 1 つの参考として、センパイ方の話は掻い摘んでおきたいな、という気持ちから聴き始めた。

PodCast を始めた経緯が、他のエンジニアさんはどのようにして家庭にコミットしているのか?なかなか自分の時間が取れないがみんなどうしてる?というものだったので、将来家庭を持ちたい者としては気になる部分。

生活のスケジュールは?

こちらの夫婦にはお子さんが 2 人いて、どちらも保育園に通っているとのこと。

よく言われていることなので想像は付いていたけど、朝5時に起きて支度して子供を保育園に送り、そのあと仕事を始め、仕事が終わったら保育園に迎えに行き、ご飯を食べさせお風呂に入れて寝かしつけて、さあ寝るまでは自分の時間だ!というライフサイクル。

今の自分のライフサイクルとは昼夜逆転といっても過言ではないものだったけど、まあそのあたりは「知ってる!!!実践したことないけどな!!!」という感じ。

大変そう(こなみかん)。

といえど、子供を持ったら、必然と無意識になっていくんだろうなあという、漠然とした "慣れ" が出てきて自然に適応できていくのかな。

生活スケジュールを確立する上で、あっそれよさそうって思ったのは、子供 2 人が 同じ保育園に通っている ということ。なるほど〜〜って感じ。送り迎えがより楽になりますねえ。子供が 1 人だとしても、職場や家に近い場所、というやり方でも楽になり、結果的に自分の時間が増えそうだ。うーん学び。

日々の学習どうしてる?

朝 4 ~ 5 時ぐらいに勉強するとのことで、家庭にコミットしたい状況だからこそ、そのぐらいしか時間が取れないのか〜、と。

自分は、じっくり時間をかけて自分の体や脳に知識を染み込ませていくような勉強スタイルなので、短時間で良い感じの学習効果を得られるスタイルを確率させないと、子供を持ったときに、学習しても実感がでなさそうだな〜という感想を持った。

逆に、寝かしつけたあとに勉強はどうだろう?という話に対して、そもそもその時間になっていたら疲労困憊の状況であり、学習効率は皆無であろうとのこと。わかる。人間、睡魔には勝てない。むしろ睡眠の質によって快適な生活が左右されるので「学習 < 睡眠」の評価式は確定も同然。

学習効率という意味でこれは優先度高いなと思うのは、仕事の中で技術的チャレンジができる環境に自分の身を置けることなのかな。やっぱり個人でやるより仕事でやったほうが、気合いの入れ方 1 つでも違うし、売り上げに繋がり回り回って自分の給料に影響があると思うと、最短最適な学習を無意識にするんだと思う。そういう環境を実現できる会社を選んでいくのも、家庭を持つ上で重要だよね、と再認識した。

子供を持つことによる価値観の変化

デザイナーである奥さまは、出産を契機に感性や興味の対象などがガラッと変わったとのこと。

子供を持ったことがないのでわからないが、もし子供を持ったとき、たぶん「あ〜自分の親もこう感じてたんだな」と、自分の昔を思い出し、あの時の親の気持ちを想像することはありそう。けど、このへんは、生活スケジュールだとか学習リズムなど形や数字で表せられるものとは違う、ヒトの気持ちの部分なんで全然想像ができなかった。

生活と仕事との両立で意識してること

コード、いつまで書き続けたいですかね。

老いても書き続けたい」という声をぼちぼちよく耳にするたびに、 それって言うても今の自身の脳みそから出てきた言葉なんだよね っていつも心の中でひねくれながら思っちゃう自分がいる。

なので、自分の中での "仕事に対する思い" としては 今やれることを大切にしたい という考えがある。将来、何かのキッカケでカメラマンに本気で取り組むこともあるだろうし。

ただ、どのような仕事にせよ、妻と子供を食べさせていけて将来子供が大きくなっても安定した家庭を築けるだけの賃金をもらうこと、は前提に置いておきたい。それが結果的に仕事と生活を両立する上での土台となるから。とはいえ、お金を管理する部分は少し苦手なので出来れば妻に任せたい部分がある。おこづかい制でも全然いいですので!

雑感

会話の内容だけ見れば感じられないけど、合間の笑いだったり相槌だったり、旦那様と奥様の仲の良さが滲み出てる会話だった。第 2 回目はデザイナーである奥様のパーソナリティがわかる会だったのもあり、夫婦の現実味を感じられる PodCast で良かった。あと、無意識に奥様が司会進行してるような感じが微笑ましかった。

人生って感じ。

セカンドキャリア

将来、何をしているのかわからない自分が「昔はこういうときがあったなあ」と、

過去を思い返してほしいがために、最近の心境を将来の自分へ向けて残しておく。


この業界に入りソフトウェアエンジニア職になってから丸6年が経とうとしています。

ノルマ未達成だと月のお給料が5万になってしまうブラックな仲介業を営む不動産屋で体を壊し、物件の内見時に活用していたスマホの地図アプリに感激したことから、もともとやってみたかったプログラミングでモノを創る仕事に身を寄せてからというもの、元気に過ごしてますか。

今、自分は、この職に対して少しばかり、疲れあるいはストレスのようなものを感じています。

...

この業界の技術進歩はとにかく早いです。

少し前に修得したと思った技術は、更にパワーアップして帰ってきます。いつでもワクワクできます。

この業界に入ってからは、目をギラギラと輝かせながら新しい技術を追ってプログラミングをしていましたね。 Android が 2.3 から 3.0 / 4.0 にアップデートされたとき、青くてカッチョいい Holo Theme が好きでした。 5.0 から登場した Material Design には酔狂な気持ちでいつも見つめていました。まわりのエンジニアと切磋琢磨して技術を吸収し成長していく環境、一見、青臭いドラマのような言い回しだけど割と嫌いじゃなかったですよね。

───── そして現在。

今は何かに追われるかのような感覚で新しい技術を学習し続けています。

その感覚があることで、この職に対して少しばかり、疲れあるいはストレスのようなものを感じているようです。

この職に飽きてしまったけど、いまのところは売り手市場であるこの業界にしがみつきたい、という気持ちで居続けているからな気がしますが、本当のところはよくわかりません。

コレだという理由が確定しておらず、うまく言語化が出来ていない状態ですが、 部屋の中で流しそうめんをやる ぐらいには元気なので大丈夫です、心配ご無用です。

...

最近、こんな記事を見ました。もう忘れてると思うので引用もしておきますね。

hikakujoho.com

─── お二人はゲームイベント企画などを行う会社「忍ism」も経営されているとうかがいました。会社経営で食べていくというのは考えているのでしょうか。

ももち それは自分たちの将来の目標ですね。もちろん、プロゲーマーとして賞金もお給料もあるんですけど、プロゲーマーになった1年後くらいから"引退したとき自分たちに何が残るんだろう"と考えていたんです。そこでゲームの啓発活動を始めるために今の会社を作ったんです。

チョコ もちろん好きだからやっている、という大前提はあります。私はイベント運営が好きで、ももちは人に教えるのが好きで後進の育成がしたいという思いはあったので。

ももち 「ゲームをプレイする」以外でゲームに関われるセカンドキャリアが欲しかったんです。本当は引退してから考えればいいことなのかもしれないんですけど、それだと遅いですし、プロゲーマーとしての仕事がいつなくなるかも分からないですし。

ももちプロゲーマーが言い放った最後の文章。

視線移動が止まりましたよね。止まりました。天井を仰いだんです。転職、大いにアリだと思いました。

この業界に関われる今とは別の何か、を仕事にするのも悪くないと思います。

今の自分は肩までカメラ沼に浸かっています。自分の思うよう好きなように写真を撮っています。フォトグラファーは軌道に乗るまでが非常に大変だと聞いてますが、好きなことを仕事にするのはこれが初めてじゃないですよね。

DroidKaigi というカンファレンスの運営スタッフでもやりたいことをやらせてもらってます。カメラヌマーなので撮影チームで好きなように撮ってますよね。 DroidKaigi に限らずイベント運営業みたいな仕事もアリなんじゃない、と無責任に思っているんです。

hikakujoho.com

この業界に入りソフトウェアエンジニア職になってから丸6年が経とうとしています。 2018/03/30 には 31 歳になりました。

30 代が一番楽しいと今は豪語していますが、 30 代後半になったら 40 代が一番楽しいと言っていることでしょう。

どちらに転んでも成るように成ると思うので、疲れあるいはストレスのような今の気持ちを感じつつも、もう少しだけセカンドキャリアについて自問自答して考えてみたいと思います。

Flutter Meetup Tokyo ♯1 で LT した

Flutter Meetup Tokyo #1 - connpass で LT した

LT 要約

speakerdeck.com

イベント雑感

Flutter フレームワークの仕組み、マルチプラットフォーム開発やネイティブブリッジまわりの tips など、Flutter 経験者による発表がある傍ら、最近 Flutter を始めた方々からの発表などがあった。

参加者アンケートにおいても Flutter 経験時間にばらつきがあったので、まだまだ Flutter はこれからって印象だった。

会場のスクリーンは3枚ぐらいあったように見えたけど、当日は中央1枚で運用していた模様。このあたりは、参加者人数が多くて席が横に広がっていたのを鑑みると、両サイドにあるスクリーンも使ってよかったのかなと感じた。(両サイドにあったっけ?ちゃんと確認はしていない)

初回イベントに参加できたので運営・登壇者・参加者の方々に感謝と、フラッタラー目指していこうなというお気持ちが強まった次第です。

DroidKaigi 2018 スポンサー担当と撮影担当の舞台裏と心境

2018/02/08-09 に開催された DroidKaigi 2018 に、スタッフとして参加していた。

役割は、スポンサー担当と撮影担当。その舞台裏を記していこうと思う。

droidkaigi.jp

スポンサー担当

時はさかのぼり 2017/05 。

DroidKaigi 2018 の第 1 回目 MTG が開催される。前回 DroidKaigi 2017 開催日から 2 ヶ月後のことだった。

前回と同様、今回の会場はベルサール新宿グランド・ 5F コンファレンスセンターを貸し切って開催することになった。

そして前回と違うのは、 1F にあるホールの利用も計画に入っていること。ここでパーティやらオープニングを催す予定だ。

f:id:sho5nn:20180211133156j:plain

1F ホールの利用に加えて、予定参加人数も前回より更に増した 1000 人として計画を進めることになり、それ即ち、全体予算も比例して増すことを表している。

DroidKaigi の準備に必要なお金の収入源は、一般参加者に購入していただくチケットの代金と、企業からいただく協賛金、この2つ。

様々な企業からの協賛を募るためにはまず、協賛金総額とスポンサープランの内容を練るところから始める必要がある。

画して、代表の mhidaka 氏 を中心に、今回登壇もしていた kmats 氏 と僕の 3 人が走り始めるのだった。

企業訪問

スポンサープランが fix した後、公式サイトや twitter 公式アカウントで、スポンサー募集の開始を告知する。 2017/07 のことだった。

その日から、企業からのお問い合わせに対応していくことになる。

どのようにスポンサープランが fix していったのか?については、内部情報のため明かすことはできない。

f:id:sho5nn:20180211143748j:plain

基本的にスタッフは、ボランティアとして活動している。

スタッフの多くはソフトウェアエンジニアであり、普段はプログラムのコードを書いてお賃金を貰っている生き物だ。僕も例に漏れない。

営業や広報の業務などと違い、頻繁にメールの文章を書くことも外回りをすることもない。痔に気をつけつつイスに座りながらコードと対峙する時間が、圧倒的に多い。

それとは正反対に、企業からのお問い合わせにはメールでやりとりし、初めて DroidKaigi に協賛していただく企業にはなるべく、ご挨拶に伺った。表はソフトウェアエンジニアの顔、裏は営業の顔、どこぞの特命係長かと思わす形振りと化した。

それらは、募集を開始した 2017/07 から 2018/01 頃まで継続的に続いた。

進捗管理

複数用意していたスポンサープランは順調に埋まっていき、DroidKaigi 2018 のスポンサー企業は最終的に 40 社ほどになった。

裏を返せば、長いライフサイクルの中で、非同期でメッセージングが行われる 40 もの singleton object を管理していたということになる。

いかように管理していたか?それは、まごころを込めた進捗管理表( Google スプレッドシート)によるものだった。

管理たいへんだよね問題は、前回・前々回から上がってはいたけど、最善最適な解決方法は未だ見つかってはいない。正直、無いと思っている。

少しでも楽にできないかと kintone のような業務アプリを触ってみたが、オーバーキル感が否めなかったのと、 Google スプレッドシートはスタッフ全員がある程度は使い慣れてることもあり、本格的に運用はしなかった。が、この選択は間違っていなかったと思う。新しいものを導入するには何かとリスクが付き物だし。

続ける理由

僕は、 DroidKaigi 2016, 2017, 2018 の 3 回、スタッフとして参加している。全てスポンサー担当。2016 のときに hotchemi 氏 からお誘いを受け、空いてる担当に成り行きで割り振られたのがきっかけだった。

僕個人として在り続けたいエンジニア像が恥ずかしながらあり、エンジニアのためのカンファレンスのスタッフ、という "縁の下の力持ち" 的な存在は、それと近しいものを感じている。

道中、スタッフを続けることの厳しさを感じることもあったが、当日を迎えると、やっててよかったと噛みしめることが多い。企業ブースで一般参加者と企業関係者が会話しているシーンを見ると、保証はできないけど何かのキッカケにでもなったら嬉しい、とか感じていた。

その感じた気持ちは、 DroidKaigi の開催趣旨からの影響もある。

DroidKaigi はエンジニアが主役の Android カンファレンスです。

Android 技術情報の共有とコミュニケーションを目的に 2018年2月8日(木)、9日(金) の 2 日間開催します。

セッションを聴講する、オフィスアワーで登壇者と直接会話をする、懇親会で今まで話したことがなかった人と会話して仲良くなる、どれもこれも、開催趣旨の現れとなったシーンだ。

参加者全員が少しでも何かを得られていれば、やっててよかったと自分もまた噛み締められる。無理しない範囲で今後も、エンジニアのためのカンファレンスのスタッフは続けていきたいと、スタッフ打ち上げの帰りに再確認した次第だった。

そして、開催趣旨の現れとなったシーンは写真におさめたくなる傾向が誰にでもある。僕も例に漏れず、スポンサー担当の傍ら、撮影担当としてカメラを片手に会場を回っていた。

ここで、撮影担当の舞台裏に話を移そう。

撮影担当

スポンサー担当の項目が少し硬い文章だったので、ここからはゆるい感じで書きなぐろうと思う。

DroidKaigi の撮影担当は、セッション動画の撮影担当と、カメラパシャパシャする担当の2つに分かれる。僕はカメラパシャパシャする担当。

舞台裏と言ってもそんなに難しいことはしてなかった。

  1. 撮影したい人を募る
  2. 撮っておきたいものリストを作る
  3. シフトを組む
  4. 当日がんばって撮影する

撮影したい人を募る

スタッフも楽しくやるぞってノリなので、撮りたい人が撮ろう!という感じ。

f:id:sho5nn:20180211193157p:plain

撮っておきたいものリストを作る

2016, 2017 で「あれも撮っておきたかった」などなど KPT して反省点は見えていたので、今回 2018 では、あらかじめ「これは最低限撮っておきたいよね」リストをまず作成した。

f:id:sho5nn:20180211193635p:plain

シフトを組む

各々がよしなに撮影すると、撮っておきたいものリストから漏れたものが出てくると思ったので、タイムテーブルに合わせて担当を振り分けた。

f:id:sho5nn:20180211194042p:plain

当日がんばって撮影する

振り返り

  • 撮っておきたいものリストを作ったのは良かった。先に QA テストケースを書いて仕様漏れないかチェックする感じ。前回までの KPT が生きた気がする。
  • シフトは組んだものの、当日はわりと自由に撮っていた気がした。が、強いレンズ所持者が ATI を発揮してくれたりして有り難かった。
    • シフト組むぐらいまでしなくとも、認識合わせするぐらいでも十分だったと思う。
  • 全員のカメラの日付設定をちゃんと合わせよう、と事前に声掛けできてたのが最高だった。
    • これは、撮影写真を一般公開するために Google Photo アルバムでまとめるとき、EXIF の撮影日時ソートでいい感じの時系列で並べられるようにするため。
    • 2017 はこれがうまくできてなくて少し手間取った
  • オフィスアワーの風景をもう少しうまく撮りたかった
    • 登壇者と参加者の会話シーンを撮るのが難しかった。お互い対面で話しているので、 2 人の顔をいい感じにフレーム内におさめるのが難しい。どちらかが後ろを向いているか、 2 人とも横顔になってるか、の2パターンの構図がほとんど。

次は、もっと強いレンズで挑みたい。


拡張関数の定義先ファイルのパッケージ

例えば android.content.res.Resources に対して次のような拡張関数を生やすとして、

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

拡張関数の定義先を com.sample.app.extensions パッケージ配下に作成した ResourcesEx.kt とすると、

package com.sample.app.extensions

import android.content.res.Configuration
import android.content.res.Resources

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

使う側では次のようなコードになる

package com.sample.app.layer.presentation.view

import com.sample.app.extensions.isLandscape

class FooActivity : Activity {

  override fun onCreate(savedInstanceState: Bundle?) {
    if (resources.isLandscape()) {
      ...
    }
  }
}

こうするより...

ResourcesEx.ktcom.sample.app.extensions.android.content.res.Resources パッケージ配下にしとくと、

package com.sample.app.extensions.android.content.res.Resources

import android.content.res.Configuration
import android.content.res.Resources

fun Resources.isLandscape() = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

使う側では次のようなコードになり、 import 文がより明確になった。

package com.sample.app.layer.presentation.view

import com.sample.app.extensions.android.content.res.Resources.isLandscape

class FooActivity : Activity {

  override fun onCreate(savedInstanceState: Bundle?) {
    if (resources.isLandscape()) {
      ...
    }
  }
}

パッケージ名に「何に対する拡張関数か?」を明示的に含めとくと良いのではないかな。

パッケージ名に大文字が入ることに抵抗ある人には厳しそうだけど、個人的には感覚より明示的さを優先したい。

com.google.gms:oss-licenses を使ってオープンソースライセンスを表示する

ライブラリのライセンス管理・表示を簡単に行えるツールが Google Play サービス 11.2.0 から含まれるようになり、とりあえず表示するだけなら非常に簡単だったので、導入方法と注意点を残します。

https://developers.google.com/android/guides/opensource

導入

Android Studio 3.0 で新規作成したプロジェクトに対して導入してみます。

導入は非常に簡単です。

依存しているライブラリの pom からライセンス情報を取得するための Gradle プラグイン com.google.gms.oss.licenses.plugin と、ライセンス表記を行う Activity を提供するライブラリ play-services-oss-licenses を dependencies に追加します。

build.gradle

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
+       google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+       classpath 'com.google.gms:oss-licenses:0.9.1'
    }
}

app/build.gradle

  apply plugin: 'com.android.application'
  apply plugin: 'kotlin-android'
  apply plugin: 'kotlin-android-extensions'
+ apply plugin: 'com.google.gms.oss.licenses.plugin'

  android {
      compileSdkVersion 26
      defaultConfig {
          applicationId "com.github.sho5nn.sample"
          minSdkVersion 23
          targetSdkVersion 26
          versionCode 1
          versionName "1.0"
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
      buildTypes {
          release {
              minifyEnabled false
              proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
          }
      }
  }

  dependencies {
      implementation fileTree(dir: 'libs', include: ['*.jar'])
      implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
      implementation 'com.android.support:appcompat-v7:26.1.0'
      implementation 'com.android.support.constraint:constraint-layout:1.0.2'
      testImplementation 'junit:junit:4.12'
      androidTestImplementation 'com.android.support.test:runner:1.0.1'
      androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
+     implementation 'com.google.android.gms:play-services-oss-licenses:11.6.0'
  }

この状態でビルドすると app/src/main/res/raw ディレクトリに、プロジェクトが依存しているライブラリのライセンス情報をまとめたファイルが 2 つ生成されます。

// third_party_license_metadata

0:46 appcompat-v7
47:47 play-services-oss-licenses
95:46 animated-vector-drawable
142:46 support-vector-drawable
189:47 play-services-basement
237:46 support-v4
284:46 support-fragment
331:46 support-core-ui
378:46 support-media-compat
425:46 support-core-utils
472:46 support-compat
519:46 support-annotations
566:731 UTF
1298:2500 zlib
3799:19442 ICU4C
23242:11358 CCTZ
34601:680 STL
35282:1602 JSR 305
36885:1732 Protobuf Nano
38618:1481 darts_clone
40100:243 tz database
40344:1558 RE2
41903:3182 PCRE
45086:11358 safeparcel
56445:46 annotations
56492:46 runtime
56586:46 common
// third_party_licenses

http://www.apache.org/licenses/LICENSE-2.0.txt
https://developer.android.com/studio/terms.html
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
https://developer.android.com/studio/terms.html
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt
http://www.apache.org/licenses/LICENSE-2.0.txt

...

次に、ライセンス表記を行ってくれる Activity を呼び出すコードを書きます。

import com.google.android.gms.oss.licenses.OssLicensesMenuActivity

val intent = Intent(this, OssLicensesMenuActivity::class.java)
intent.putExtra("title", "おーぷんそーすらいせんす")
startActivity(intent)

これだけで、ライセンス一覧画面を表示することができます。ライセンス一覧画面からはライセンス詳細画面に遷移することができ、そこでライセンスを確認することができます。 ライセンス一覧画面の ActionBar のタイトルは Intent#putExtratitle のキー名で変更可能です。

f:id:sho5nn:20171110001734g:plain

注意点

既にアプリで定義している Theme が Theme.AppCompat.Light.NoActionBar など ActionBar がないものだった場合、ライセンス一覧画面から遷移する詳細画面を表示するときにクラッシュしてしまいます。これは play-services-oss-licensesActionBar#setTitle でライブラリ名を設定しようとしているためです。

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.app.ActionBar.setTitle(java.lang.CharSequence)' on a null object reference
   at com.google.android.gms.oss.licenses.OssLicensesActivity.onCreate(Unknown Source:30)
   at android.app.Activity.performCreate(Activity.java:6975)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) 
   at android.app.ActivityThread.-wrap11(Unknown Source:0) 
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) 
   at android.os.Handler.dispatchMessage(Handler.java:105) 
   at android.os.Looper.loop(Looper.java:164) 
   at android.app.ActivityThread.main(ActivityThread.java:6541) 
   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:767) 

回避方法としては、ライセンス詳細画面である OssLicensesActivity の Theme を、 ActionBar ありの Theme で上書きしてしまえばよいです。ライセンス一覧画面の OssLicensesMenuActivity も上書きできるので、ちょっとしたカスタマイズはできそう。

<manifest>
  <application>
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
+     <activity
+       android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+       android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
+       />
+     <activity
+       android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+       android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
+       />
  </application>
</manifest>

以上です。