アナログ金木犀

つれづれなるまままにつれづれする

【android】まれにレビューで指摘するParcelableについて

時差ボケからの早起きになってしまい、朝の時間に余裕ができたので小ネタを書いてみます。

Parcelableに関する全く同じ指摘をいくつかのプロジェクトでやってきました。

自分が見てきたプロジェクトでは、下記の条件を満たす場合はほぼ100%の確率で1回以上指摘しています。

  • kotlinを使っている
  • Parcelableをライブラリとか使わずに自分らで頑張ってる

この記事の結論はライブラリ使えってことではなく、ここを気をつけようって話になります。

テストには出ないエクササイズ

data class TestData(
        val num: Int,
        val str: String = "test"
) : Parcelable {

    constructor(source: Parcel) : this(
            source.readInt() + source.readInt() // ← ここ注目
    )

    override fun describeContents() = 0

    override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
        writeInt(num)
        writeString(str)
    }

    companion object {
        @JvmField
        val CREATOR: Parcelable.Creator<TestData> = object : Parcelable.Creator<TestData> {
            override fun createFromParcel(source: Parcel): TestData = TestData(source)
            override fun newArray(size: Int): Array<TestData?> = arrayOfNulls(size)
        }
    }
}

こんなクラスがあるとします。 ここ注目 という箇所に注目ください。

この時に TestData(4, "hi")インスタンスを作りIntentにputして遷移先でgetした時、numstrの値はどうなるでしょうか??

プロダクションコードでこんなのを書いてきたら困惑ものですが、遊びと思って考えてくださいませ。

ちなみに、ここで「なーんだ」とわかる人はこの先を読む必要がありません。

回答は最後にします。

よく指摘するケース

さて上記の回答&解説をする前に、よく指摘するケースも見ておきます。

data class TestData(
        val num1: Int?,
        val num2: Int?,
        val str: String
) : Parcelable {

    constructor(source: Parcel) : this(
            source.readInt(),
            source.readInt(),
            source.readString()
    )

    override fun describeContents() = 0

    override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
        num1?.let { writeInt(it) }
        num2?.let { writeInt(it) }
        writeString(str)
    }

    companion object {
        @JvmField
        val CREATOR: Parcelable.Creator<TestData> = object : Parcelable.Creator<TestData> {
            override fun createFromParcel(source: Parcel): TestData = TestData(source)
            override fun newArray(size: Int): Array<TestData?> = arrayOfNulls(size)
        }
    }
}

このコードには致命的な問題があります。わかりますでしょうか?

java.lang.IllegalStateException が発生したりします。

解説

イメージの話です。Parcel には直列的に値が積まれていきます。

TestData(1, 2, "aaa") の時、 Parcel には 1, 2, 3, a, a, a という風に値が積まれていきます。 3 は文字列のlengthですね。

この Parcel から取得する時も直列的に取得していきます。

  1. 1回目のreadInt()num11 が入る
  2. 2回目の readInt()num22が入る。
  3. readString() で 初めの3, a, a, a を取得。サイズを取得してその後ろのサイズ分getする感じです。これが str に入る。

この場合はちゃんと動きますね。

では TestData(null, 2, "aaa") の時を考えましょう。

        num1?.let { writeInt(it) }
        num2?.let { writeInt(it) }
        writeString(str)

となっているので、初回の writeIntは走りません。そのため Parcel には 2, 3, a, a, a という値が積まれます。

これをgetするとどうなるでしょうか?

  1. 1回目の readInt()num12 が入る。(ここですでに間違ってますね)
  2. 2回目の readInt()num23 が入る。 (これも正しくない)
  3. 3回目の readString() 時にサイズが取れず、 IllegalExceptionが発生する (😇)

こうなります。ダメダメです。

では、これを修正してます。

    override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
        writeInt(num1 ?: 0)
        writeInt(num2 ?: 0)
        writeString(str)
    }

で、いいじゃんと思う方がいるかもしれませんが、これも厳密には間違っています。 TestData(null, null, "aaa")TestData(0, 0, "aaa") となってしまうからです。

修正方法は単純でnullかどうかのフラグもParcelに詰めてやります。(他にやり方あったら教えてください)

:
    constructor(source: Parcel) : this(
            if (source.readInt() == 1) source.readInt() else null,
            if (source.readInt() == 1) source.readInt() else null,
            source.readString()
    )

    override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
        writeInt(if (num1 != null) 1 else 0)
        if (num1 != null) {
            writeInt(num1)
        }
        writeInt(if (num2 != null) 1 else 0)
        if (num2 != null) {
            writeInt(num2)
        }
        writeString(str)
    }
:

こうすることで取得する時に対象がずれることがなくなります。

一件落着。

"テストには出ないエクササイズ" の回答&解説

回答は num=6, str="test" となります。

ここまでくれば解説もいらないと思います。

TestData(4, "hi") となっている時、 4, 2, h, i と積まれています。

data class TestData(
        val num: Int,
        val str: String = "test"
) : Parcelable {

    constructor(source: Parcel) : this(
            source.readInt() + source.readInt() // ← ここ注目
    )
   :
}

pos=0の4 と pos=1の2を足したものが numstr はデフォルト値となるので、TestData(4+2, "test") となるわけです。

結論

自分でParcelableを実装する時は値が積まれていくのをイメージすべし!

ちなみに、javaのときは int はプリミティブで null になることはなかったし、 Integer 使ってたとしてもその場合は writeValue / readValue(class loader) 使えば null 時もいい感じに動くので、こういうミスに遭遇する人は少なかったんでしょうね。

追記

修正コードについて、Int? に関しては writeValue を使うことでも問題なく動きます!なので、この方法でも良いです。

ただ個人的には、雑にやってるとランタイムにjava.lang.RuntimeException: Parcel: unable to marshal value で落ちるのでなるべく使わないほうがいいのかなという考えです。

くろかわさんから質問いただきました!

酒の席では思い出せなかった回答。

androidエンジニアとしてgoogle io2018での気になるところまとめ、あるいは帰ってからの宿題

帰ったらしっかり調べて会社ブログなどに書くとして、ひとまず雑に気になるところをまとめてみた。 何かしら一言コメントしてるところありますが、基本的に正しい保証ないです。

jet pack

ざざっとみる限り、指針やライブラリ群などまとめた物を総称してJetPackと呼んでいる?? とあるセッションでは下記のように説明されていた

  • guidance
  • recommended libraries and tools
  • has a cute logo

下記はdeveloper page

地味に、下記のようにさらっと書かれている。

The UI will consist of a fragment UserProfileFragment.java and its corresponding layout file user_profile_layout.xml

fragment_user_profileじゃないんですね、うぃす。

f:id:kgmyshin:20180510062359j:plain

Navigationまわり

developer page

navigation editorの話

migrate to the navigation arch componentとか読んでみるとといいかも

codelabのURL

Constraint Layout2.0まわり

ここのアニメーションのやつ。そもそもConstraint layoutのくだりだったか覚えてない

タイムラインで座標与えてアニメーションを作れたりするみたい。どういう感じでxmlに落ちるんだろうか?

セッションではMotionLayoutといってConstraintLayoutのサブクラスとのこと。 Motion Editorの動きは下から!

youtu.be

上記リンクはMotionEditorから再生になるけど、必見オブ必見なのでみてみるといいと思います。

app bundle

dynamic feature moduleごとにダイナミックにダウンロードできて、初回のapkのダウンロードサイズを減らすという話な認識。

もろもろ分割されるタイミングとか条件や、仕組みをしっかり知りたい。

今井さんがまとめてくれてるのがわかりやすい。

Android App Bundle/Dynamic feature modulesにみるモジュール化の未来 - tomoima525's blog

テストどうすんねんって今井さんと話してたんですが、ちょうど tnjさんが聞いてくれてたみたいでした!

work manager

あんまりまだ話に出てきてないけど新しいAPI

codelabはこっち

material components

githubのURL

toolsとか時間ある時に眺める

あと、普通にライブラリがあるらしく、 UIやStyleが用意されているみたい。

その説明をしているところが下記。

youtu.be

Slices

new approach for remote content.

  • templated
  • interactive
  • updatable

19+以上で使えるらしい。

jet pack並みに気になってるところ。

実装方法はdeveloperページに載ってる。 google searchとかに表示させる実装方法はセッションで言ってたけど早くて追いつけなかった。 動画が公開されたら見直す。(下記の動画)

Android Slices: build interactive results for Google Search (Google I/O '18) - YouTube

その他の気になるcodelabs

見直すセッション、見れなかったけどあとで見るセッション

技術書典4にて、KUGIBAKOとしてサークル参加します!

技術書典4とは

言わずもがなと思いますが、技術書オンリーの一大イベントです

techbookfest.org

Androidの設計の話をまとめました

Androidの設計にした理由は下記です

  • いろいろ設計方面の話を多めにしてきた割に、やっぱりLTや話すだけでは足りないなと感じていた
  • いろんなチームを外から支援する立場が多くなったときに、すっと差出せるまとまったものがほしかった

できたのか...?

一応、できました。 本当はもっともっと書きたいことが山積みなんですが、時間の都合もあって削らざるを得ない状況になりました。 それでも、一番重要なことたちは詰め込むことができました。 これらの経緯もあって、今回は 「~pocket~」というサブタイトルみたいなものが、タイトルの後ろにひっそりついています。

書きたかったけど書けなかったことは、別冊ではなくこの本に継ぎ足してつつ、また常に最新情報に改訂しながら、またどこかで出せたらなと思っています。 (つぎはきっと変なサブタイトルはつかないはず。~β~ とか ~golden master~とか決してつかないはず。)

目次 & 表紙

こちらが 尊い表紙 と目次です。貼らないですが裏表紙も尊いです。

f:id:kgmyshin:20180417191609p:plainf:id:kgmyshin:20180417191617p:plainf:id:kgmyshin:20180418011312p:plain

なにはともあれ

もし、興味ある方いらっしゃいましたら、立ち寄ってみてください!

お気に入り的にチェックできるので、下記からチェックしてもらえると当日楽かもしれません。

techbookfest.org

取り置きに関してです。取り置きしてほしいと、そんな酔狂な方がいるのかわからないのですが、同人誌に詳しい方からトリオキニというサービスを教えてもらいました。

こちら から取り置きを受け付けるようにしたので、酔狂な方よろしくお願いします。

リクルートマーケティングパートナーズを退職しました

二月末にリクルートマーケティングパートナーズ (以下RMPとします) を退職いたしました。

感謝ばかりです

そもそも大きな会社で働いたことのない自分をすんなり受けいれてくれたことから感謝しています。

環境の話

同じリクルートグループでも雰囲気だったりローカルルールだったりで多少異なるかもしれないのでRMPに限った話として。

勤務場所・時間

RMPではリモートワークが本当にフルで使えました。

満員電車を避けるために朝だけリモートワークしたり、また家でなくても東京各地にサテライトオフィスを借りてるので集中したいときはそっちを使ったりしていました。

コアタイムなしのフレックスだったので朝はゆっくりしたり、逆にすごく早くから始業したりと自分のパフォーマンスが発揮できる時間帯で仕事ができました。

評価制度がしっかりしている

リクルートらしい評価制度でなるほどと思うところがたくさんありました。 もともと営業の会社なので若干ミスマッチなところもあるのですが (エンジニアに向けて絶賛最適化中です) 、それでもしっかり評価されてるなぁと感じました。

2年9ヶ月いて、半年に1回の昇級タイミングではだいたい毎回給料が上がりました。 (もちろん人によります。言いたいのはちゃんと上がるってことです。)

給料が毎回上がったっていうのは実際は僕の初めの給料が低かったという可能性もあるのですが、それでも前職に比べたら全然もらえてた方でした。

本当に成長できた

0->1, 1->10 を経験できた

自分がもともとRMPに入ったのは 0->1, 1->10 を経験したかったというのが1番の理由です。

というのも、自分は個人でサービスを作って安定して売り上げがでるくらい使ってもらえるまで軌道に乗せる、というのを数スレッド回すというのが昨今の目標なんです。

スタートアップなども考えましたが、すでにノウハウを持ってるリクルートで新規事業に関われるのであれば...!と決めました。

実際入社してからはノウハウもなんとなく理解することができました。またリクルートのやり方は個人では再現性がないんだなことも理解できました。

イデア自体に突拍子やユニーク性はあんまりいらなくて、しっかりしたものをしっかり作って、ちゃんと広報して顧客に合わせて営業するという、言葉にすればシンプルですがそれぞれを高いレベルで実行できればビジネスは育っていくんだなぁと思いました。

思想が変わった

もともとエンジニア至上主義みたいな考えがちょっとありました。 企画もグロースも広報もデータ見て改善していくのも全部エンジニアがやればいいじゃんみたいな考えです。

前職がスタートアップで、その場所ではそれが正しかったんですが、大きな組織ではその限りではないなとすぐに悟りました。

RMPに入社してからは 優秀な企画の人 がいるチームで働き、「あぁ、企画もエンジニア並みに専門職じゃん」と考えるようになりました。数年以内に何億円何十億円のビジネスを作る約束をして実現する能力は、今のところ僕にはありません。

それ以降は一層リスペクトして接するようになりました。今では全部エンジニアがやるんじゃなくて お互い歩み寄っていくべきなんだ と考えています。

リードエンジニアや開発グループのマネージャーを務めさせてもらった

消去法かもなんですが、やらせていただきました。

グループマネージャーになってからは本当に血反吐を吐くほどにはいろんなことを考えて悩んで、ある程度実体験に基づいた考えみたいなものも持つようになりました。箇条書きで書くと下記みたいなところです。

  • どうやって強いグループにしていくべきなのか
  • チームの自立をどう促していけばいいか (自分がやってもしかたない)
  • どうやっていいエンジニアを採用していくか
  • エンジニアの働きやすい環境とは
  • エンジニアを評価するには
  • いいエンジニアの文化をどう醸成して行くか
  • エンジニアと非エンジニアの関係性(個人間)について
  • エンジニアと非エンジニアの関係性(グループ間)について
  • 他エンジニアグループとの関係性について
  • などなど

結局、任命してもらったにも関わらずグループマネージャとしては未熟なまま、8ヶ月間ほどで外れさせてもらいました。 外れた理由は複合的な理由が多くあるんですが、自分がメンバーだったら数ヶ月後に辞めるって言ってる人にいろいろ言われたくないなぁと思ったのが大きなところを占めます。

グループマネージャとしては微妙だった僕もリードエンジニアとしてはうまくやれたのかなと考えてます。 僕は リードエンジニアのあるべきゴールは自分が外れてもしっかり回るチームを作ること だと考えています。 2年9ヶ月でプロダクトを作り上げながら若手を育成し最後は引き継いで自分は次の燃えている所へ、というのを三度ほどやれたので (若手がめちゃくちゃ優秀だったというのが大きいですが) 自分の中ではここだけは満足しています。

じゃぁなんで辞めるのか

若手に引き継いで回った結果やることがなくなった (あんまりやりたくない仕事ばかりになった)というのが大きな理由です。 自分がやるよりは若手の成長機会や実績にした方がいいよなって思ってたら (いまでもそれは正しいと考えています ) 、いつの間にか雑務やマネージメントばかりになって技術者としての自分を維持できるかどうか不安になってしまったんですね。

もっと多くのプロダクトとエンジニアがいれば際限なく放牧民のようにプロジェクトを転々としていけたんでしょうが、まだそれほどでもない部署でした。

ミスマッチしてきたので辞めたということになります。

まとめ

結論、本当にお世話になりました!!! お疲れ様でした!!!

宣伝

また別途ブログに書くんですが、技術書典に出店します!

前職にて下記の記事を書きました。

tech.recruit-mp.co.jp

正直言うと、今見ると厳しいな...と感じる部分が多くあります。 また大枠でしかないので、これだけだとわからないことも多くあるでしょう。

この記事から、もう3年経とうとしています。

この3年間で、僕が使った新しい技術や考えを豊富に取り込んだものを、記事ではなくて一冊の本に仕立てあげます。 先日下記のツイートをしました。

僕の中では結構いいねとかしてもらえたんですが、こういうレベルの実体験からしか得られなかったような知見をふんだんにぶっ込みます!

まだ少し先ですが、転職祝いがてら技術書典当日にぜひ く-45に寄って もらえたら嬉しいです!

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

DroidKaigi 2018で「マルチモジュールのすヽめ」という内容で発表して来ました

DroidKaigi2018のDay1のラストセッションで「マルチモジュールのすヽめ」というタイトルで発表して来ました。

前回の2017年では「未熟なチーム開発」という技術というよりは、いわゆるエモい内容で発表したのですが今回はちゃんと技術的なお話で発表させていただきました。

スライド

speakerdeck.com

感想や反省点

人数

正直そんなに聞きに来る方いないのでは...と思ってて30人くらいいてくれれば和気藹々と話しながらやれるかなと思ってました。

蓋を開ければ、恐縮なのですが部屋が埋まる勢いだったので正直ちょっと怖かったです。

話すスピード

一人でリハをやってた時はなんどやっても27分くらいで終わってました。

実際本番ではもう少し早く終わってた記憶があるのでちょっと早口になってたかもしれないです。

よかったこと

聞いてくださってる中のツイートを見返して見たら、「発表が上手」というツイートがありまして、そこを褒められたのは初めてだったのでめちゃくちゃ嬉しかったです。

何をゴールにしていて、何を最低限使えればいいのかをあらかじめ設定していたのは効果があったのかもしれません。

セッションの内容について、聞いてくれてる方がどう感じるかをストーリー立ててagendaを組んでみました。

結果として、へんなもやもやみたいなツイートはあまり見られなかったので、うまくいったのかなと思います。

加えて、セッションの始まる前のみんなの移動時間に、セッションのタイトルだけでなく、「セッションのゴール」と「セッションで話さないこと」を写しておりました。

期待と違うセッションであれば、その時間であれば移動しやすい、、ということを狙ってました。

結果としては(見てないだけかもですが)、それによって出て行く人を観測はしてないんですが、共通認識をあらかじめ作ることはできたのではないかと考えてます。

また、たまたまなのですが最後のセッションということもあってホールに移動するため部屋に待機ということになり、その間の20分くらいずっと多くの方々に質問していただけたのが本当に良かった。

時間の都合上話せなかったほとんどのことをそこで話すことができました。

よかったかどうかわからないこと

事前にセッションの情報について公開しておりました。

motida-japan.hatenablog.com

ただ、これは正直効果があったかどうかわかってないです。その為にも「ブログ見てくれた方いますかー?」くらい聞けばよかったと反省。

上記のブログの記事を書くことによって、改めて個人的に整理できた点ではやってよかったです。

反省

リハを何度かやってここが終わったらだいたいn分経ってるみたいな感覚を掴んでいたんですが、テンパって肝心のストップウォッチを開始し忘れてしまったのは痛手でした。

開始前は掴み・どうやって話し始めていこうかなというところに頭を使ってしまってしまい、そこに意識が行き過ぎてたみたいです。

また、リハの時からボイスレコーダーに記録して見て気になってたのは「えっと」という言葉が多いなと感じていて気をつけようと思ってたんですが、本番ではその意識は飛んでたのできっと言ってしまってたと思います。

上記二つとも本番前の練習で解消できることではあるはずなので、もっと練習して場慣れして行きたいと思います。

残り1日、本日も気になるセッションがてんこ盛りなのでたくさん聞きに行きたいと思います!