この記事は、ユニファ Advent Calendar 2021の22日目の記事です。 adventar.org
こんにちは。人間中心設計(HCD)専門家のやまぐち(@hiro93n)です。主にプロダクトマネージャーやQAエンジニアを抱える部署のマネージャーをやっています。最近の主な悩みはイヤイヤ期への対応です。ひとつでもフラグを間違えるとゲームオーバーします。
さて本題ですが、少し前にAndroidエンジニアの一員として開発する機会がありました。今回はその時に感じたことのメモをご紹介します。新たにAndroidエンジニアになるぞ!と思った人が今後少しでも前に進みやすくなるきっかけになれば嬉しいです。
記事は長いので、先に求人募集を貼っておきます。ユニファでのお仕事やサービスにご興味があれば、募集フォームからでもDMからでもぜひカジュアル面談含めぜひご連絡ください。 unifa-e.com
2021年7月、スケジュールがこのままだと厳しいねと言う議論があった。エンジニア部門の副部長Kさん+Androidチームとの打ち合わせの中で「これ工数足りないから山口さんもコード書いた方がいいね!」とエンジニアSさんがジョークを飛ばす。
いつもなら「書けたらいいですよね」と言う話にしかならないのに、その時はなぜかそれを真に受けてしまった。結果、約2ヶ月にわたりAndroid Studioと向き合うことになったので後の人々のためにこれを残したい。
筆者のスキルセット
サーバー
- Node.js 12.x系でのアプリケーションを作ったことがある
フロント
- Vue.js 2.x系(ts)でのアプリケーションを作ったことがある
- GraphQLやNuxt.jsの経験はある
アプリ
- 手元でビルドできる
- stringでテキストファイル管理してる位は分かる
開発チームへの参加
チームへの参加で決めたルール
リリースが近いタイミングで業務委託エンジニアが新たに入ってきても教育もできないし厳しいと言う話をする中、Androidのコードを書いたことのないPdMがプロダクトコードを書きますと言う状況がこれだ。
客観的に見たら絶対にやらない方がいい打ち手なのは分かっていて、とはいえやるからにはいかに迷惑をかけずに価値を出すかを考えた。その結果、下記の方針で参加することにした。
- Androidエンジニアとしての経験がなくても大きくミスする可能性が低い案件に取り組む
- 既存の実装を参考にし、できる限り新しいコード、概念を持ちこまず学習コストを最小に抑える
- QAチケットに対しエラーログを貼り、Androidチームの検証の手間を減らす
- プルリクエストの動作チェックは率先して行う
なお、最終的には1は大きく変わることになった。
Androidエンジニアとしての経験がなくても大きくミスする可能性が低い案件に取り組む
テキストの修正や静的なリンクの張り替えに関連するものがこれにあたる。マスタを修正するだけでいいので、Androidにかかる専門知識はほとんど求められない。
Android開発にはAndroid StudioというIDEを利用するが、これの特徴として「フォルダ構成は実際のものと異なる」という点がある。
デフォルトで、Android Studio の [Android] ビューにはプロジェクト ファイルが表示されます。このビューは、ディスク上の実際のファイル階層を反映していません。プロジェクトの主なソースファイルに効率良くアクセスできるように、モジュールとファイルの種類別に整理され、あまり使用されない特定のファイルやディレクトリは非表示になっています。ディスク上の構造との違いは次のとおりです。 https://developer.android.com/guide/Navigation?hl=ja
finderのフォルダ構成と違うので、何がどこにあるのかを探ろうとすると混乱する。実際慣れるものだし特定のパスが知りたければ右クリックして「finderで開く」手があるが、しばらくはモヤモヤしていた。
*後にこの記事のレビュー時にProjectビューに切り替えると解決できるよと伺い、当時聞けば良かった案件ではあった
テキストの修正をするには元テキストの表示がどういう使われ方をしているかを見る必要がある。結果的に様々なファイル、フォルダ構成を見ることになる。VSCodeで検索→特定→フォルダ構成を見るという流れを繰り返すことで、自然にどこに何があるのかを理解することができるようになった。脳内にコードマップができていく感覚があった。
既存の実装を参考にし、できる限り新しいコード、概念を持ちこまず学習コストを最小に抑える
AndroidはKotlinで開発されており、オフィシャルのドキュメントもある。 kotlinlang.org
ちゃんとAndroid開発を学ぶならこれらのドキュメントを理解した上でアルゴリズムを設計しコードに落とすべきだ。しかし、リリースまで1ヶ月しかない中でこれらを全部やっている余裕はない。幸いPdMであれば現プロダクトの使用や挙動が理解できているので、「あれと同じ挙動をした機能を作りたい」と思ったら該当のコードを読んで流用することで、大きくは外さないそれっぽい実装を実現しやすい。
コードには設計思想があり、コードに落ちていないことでも「あえてやっていない」ことはある。
ど新人がやってきて聞きかじった知識で「これはオフィシャルドキュメントにあったので」と既存のルールに合っていないコードを書き始めてしまうとメンテ性が落ちたり、そもそもの解釈が誤っていて潜在的な不具合を抱えやすい。
場合によっては過剰実装になり得るデメリットがあったとしても、既存の動作から始めて転用した方が良かろうという判断をし、さすがにこれはダメだというものについてはコードレビューで指摘を受けて修正するサイクルとした。
機能とコードの話をしたが、自分の中でもう一つ参考にした知見としては「Vueのアレはkotlinでどう書くのか」と言うことだ。Webとアプリでプラットフォームは異なるが、フロントエンドと言うことには変わりなく設計についても似たような考え方で進められる部分が多い。
apiと通信し、ページをルーティングし、ページ間で値渡しをする。ログイン状況はapi通信時に拾い、トークンの有効期限が切れていれば更新する。typescriptの型決めで揺らぎで「?」で使うのでKotlinでも似たようなことはしているだろう。*1そういった想定の中で利用する関数、挙動を見ていったため、未経験から始めた割に既存のコードの理解がしやすかった部分が多い。
# typescript class Person { name: string; age?: number; }
# Kotlin class Person( val name: String?; val age: Int; )
そんな風に書いていたのでコードレビューで「なんでこのシーンでこのメソッド?」と突っ込まれる部分は多かった。後半になってやっと本来の使い方の理解が進んだくらいで、張りぼて感は否めなかった。
QAチケットに対しエラーログを貼り、Androidチームの検証の手間を減らす
実際どの関数でエラーしていて、apiは何を返してきているのか?についてはコードの中を突っ込んでみていかないと分からないことも多い。今回携わったアプリでは、サーバから受信しうるエラーをマッピングしている。
サーバの仕様変更などでそこに入らないなエラーが出てしまうと「不明なエラー」扱いでまとめられてしまい何が問題なのかが分からない。そういったエラーを展開してコメントをつけることをしていた。
PRの動作チェックは率先して行う
今の経験で「これはAndroidの書き方としては適さない」という判断ができるわけではない。まずは動作チェック、その上で関数名と実装に整合性があるかなど、一般的なコードレビューで見ていく部分に絞ってコメントしていた。
Androidの階段
これは持論だが、Androidを学ぼうとする時に上りやすい階段の順序はある。この階段を2,3段超えようとすると詰まって深夜の時間を溶かすことも多くあった。
自分が思うところの階段は下記の通り。
- テキスト変更
- Layout xml
- バリデーションとViewModelの概念
- FragmentとViewModelの役割分担
- Jobの概念
- Stateflowの概念
- バリデーション(複数)
- Intent
- apiは絡まない
- api通信(get)
- RepositoryとUseCaseの役割分担
- レイヤードアーキテクチャの概念
- uiとビジネスロジックの分割
- data, di, domainの役割分担
- api(post)
- paramが1個
- api(post)
- paramが2個以上でbodyオブジェクトを作成
- holdとsuccess, error
- CommonValueの使い方
- Navigationの操作
- Navigation Safe Argによる値渡し
- Navigation横断
- AndroidManifestの用途
- URLスキームとActivityの組み合わせ
- Fragmentの動的replace
- ViewModelにおけるinitブロック(初期化処理)
- Dialogの使い方
- AdapterとRecycleViewの関係性
- api値のセット
- itのリネーム
- DAOの操作
- Notification(push通知)の操作
- styleの作成、適用
- ViewModel経由での値渡し
大きなくくりで言うと下記の階段になる。
- ロジックの関係しない見た目の部分
- flow(vueで言うWatch)を使ったデータの扱い
- api通信を行う上でのRepository、UseCaseによる抽象化
- ナビゲーションの操作
- URLスキームからの起動
- もっと賢い動的UI(主にRecycleView)
- DAOによるデータベース操作
- その他周辺
まとめ
結果どうだったか
万人にはお勧めしない。キャッチアップのための時間はどんどん積み上がるし、リリース前に多忙な開発メンバーの負荷も高かったと思う。
ただ、自分が既存実装を流用している中で「これはこっちの書き方の方がいいのではないか」とコードレビューで議論するきっかけになっていたり(当時は「既存実装がこうなってるのだが今これ変えに行く…?」と思ったりも)、既存機能の横展開でなんとかなる系の機能のリリース時期が前倒れる、Androidに対する見積もり感覚が身につくなどメリットもあった。
何より「最悪自分がなんとかできる部分がある」というのは精神衛生上も良かった。自分がなんとかしないとリリースブロッカーになる、と言うプレッシャーも同時発生するが、それでもプラスだった。
0→1のスキルはもちろん1年以上の実戦経験があってと言うところだが、0→1ができる人のリソースを日々の修正で埋めてしまうくらいなら自分のように既存実装を元に修正、改修できる人がいるというのは今後開発効率を上げていく上でも可能性があるのかなと少し感じている。
Webフロントを少しでも触っていればアプリに行っても概念の近さからある程度はキャッチアップできるものなんだなーと言うのは良い気づきだった。これをきっかけにアプリを触る人が増えたらいいなと思う。未経験者を辛抱強くレビューしてくれたAndroidチームに感謝したい。
おまけ
Android開発を始める時に最初に知っておくといいルール
公式ドキュメントはやりたいことから逆引きできるわけではないのでご紹介。
- 開発環境と本番環境はどう切り替えるの。
- ビルド時に変数設定の切り替えができるのでそこからやる。
- build.gradle.kts の定義で分岐させるか書き換えるなどする。
- 画面遷移はどういうルールでやるの?いわゆるルーティングファイルはあるの
- Navigationとして管理する。Navigationについてはidで管理されており、それぞれどのFragmentが紐付くのか、どの画面からどの画面に遷移するのか、遷移させるアクションをなんと呼ぶか、値渡しをするのかなどを設定する
- ここで設定したアクションはFragment側で呼び出す。
- 公式ドキュメントで言うとこのあたり。 https://developer.android.com/guide/Navigation?hl=jadeveloper.android.com
- アクションはfindNavController()で操作する。単なるブラウザバックも指定できる。
- ブラウザバックするときに特定のステップは飛ばしたい場合、ナビゲーションファイルにその向け記載する(popToなど)
- ログインした後の画面からログイン画面に戻らせたくない場合など、このあたりの処理が必要
- ブラウザバックするときに特定のステップは飛ばしたい場合、ナビゲーションファイルにその向け記載する(popToなど)
- Navigationとして管理する。Navigationについてはidで管理されており、それぞれどのFragmentが紐付くのか、どの画面からどの画面に遷移するのか、遷移させるアクションをなんと呼ぶか、値渡しをするのかなどを設定する
- Fragmentって何。
- UI定義に関わるファイル。データ操作などはViewModelで行うためここに記載しないが、ViewModelで定義した操作をJobとして特定の条件で実行するなどはFragmentに記載する。
- サーバー開発であればコントローラーがFragment、モデルがViewModel位の概念。
- 画面を開いたときに何を実行するのか?等をvue同様に記載できる。 https://developer.android.com/guide/Navigation?hl=ja
- UI定義に関わるファイル。データ操作などはViewModelで行うためここに記載しないが、ViewModelで定義した操作をJobとして特定の条件で実行するなどはFragmentに記載する。
- Fragmentとレイアウトファイルの紐付けルールはあるの。
- 下記のような命名にしておくのが無難。FragmentではCamel caseだがlaypitはSnake case。他の場面でも下記の命名前提で自動的にクラスが作られたりする。
- Fragment
- SampleKotlinFragment.kt
- Layout
- Fragment_sample_kotlin.kt
- Activityに紐付ける場合はacitivity_xx_.ktのようになる。
- Fragment_sample_kotlin.kt
- Fragment
- 下記のような命名にしておくのが無難。FragmentではCamel caseだがlaypitはSnake case。他の場面でも下記の命名前提で自動的にクラスが作られたりする。
- Vuex的なことがしたいのだけど。
- ログイン状況はpreferenceStorage、ユーザー情報などのキャッシュはDAO経由でRoomに保存することが多い
- 値渡しはどうやるの。
- stringやintなど単純なものはナビゲーション経由のnavargs、オブジェクトなど複雑なものはViewModel経由で受け渡すことが多い。 https://developer.android.com/guide/Navigation?hl=jadeveloper.android.com
- URLスキームで起動するには。
- AndroidManifestファイルに定義する。特定のURLがきた場合にどのActivityを呼び出すかを決められる。細かなロジックについてはActivityを呼んだ後にFragmentやViewModelに引き継ぐ。
- UIレイアウトはxmlで作ると思うのだけど、該当のFragmentに紐付くxmlの中身に対しUIの要素が足りなそうに見える。
- UIを組み込んでいる場合。
- 別ファイルをincludeしている。
- UIを組み込んでいる場合。
- RecycleViewの場合。
- Fragmentで動的にUI生成している。具体的にはAdapterとして各要素を作り、まとめてUIに反映するような処理を記載する。
- 直接書いた方がラクではと思いそうだが、実質全部見出しと詳細など似たようなUIで済む場合はこちらで対応した方がUIにかかるリソースとしてもメリットがある。今のユーザーの持つ状態に合わせて入力欄を増やすなどする場合はこちらの方がおすすめ。
- RepositoryとUseCaseとServiceってどう使い分けるの。
- Repositoryはデータソースに対しどのようにアクセスし、データ処理するかを定義する。
- ひとつのRepositoryに対し複数のfunctionが紐付く。Serviceの操作はRepositoryが行う。
- api通信成功したらdbの値を更新するなどはRepositoryに記載する。
- UseCaseは特定のapi操作に合わせて引数やレスポンスの処理を定義する。
- 特定のRepositoryのfunctionを呼び出す。FragmentからはUseCaseを操作する。
- Serviceはapi通信をapi仕様通りに定義する。
- Repositoryはデータソースに対しどのようにアクセスし、データ処理するかを定義する。
- Dialogってどう出すの。
- 専用のナビゲーションを作成して表示する。vueのようにdisplay:noneを切り替えるというより、画面遷移の一種として考えた方がわかりやすい。
- DAOって何?なんだか怖い。
- 最初怖かった。ようはサーバー同様のDB操作。引数を使ってRoom用SQLクエリを実行するfunctionを作ることもできる。
*1:よくよく考えると使い方としては片方は任意キーで片方はnullableなので全然意味は違う