TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

こんにちは、delyのリテールカンパニーで新規サービスのAndroidアプリ開発を担当しているnozakingです。 今回もdelyのAndroidアプリエンジニアにインタビューした内容をお届けしたいと思います。 今回は第5弾として、わたくしnozakingのインタビュー内容をお届けします📣 第1弾のインタビュー(parayaさん)は こちら↗︎ 第2弾のインタビュー(Jさん)は こちら↗︎ 第3弾のインタビュー(kenzoさん)は こちら↗︎ 第4弾のインタビュー(umemoriさん)は こちら↗︎ インタビュー経緯 第5弾の内容の前に、インタビューに至った経緯を説明させてください。 私はAndroidアプリエンジニアの採用活動に携わらせていただいているのですが、面談や面接をしていくなかで、エンジニアとしてこの先どう歩んでいくかを決めかねている方の声を聞くことがよくありました。 そんなモヤモヤを晴らすような、いい話ができたらいいなと私は思いました。 そこで、せっかくdelyには複数のAndroidアプリエンジニアがいるので、インタビュー企画を始動しました🦸‍♂️!! 世の中のエンジニアの皆さんに、少しで もAndroidアプリ開発の魅力 (と、ついでにdelyで働くことの魅力)をお伝えできれば幸いです。 いざ、インタビュー 冒頭でもお伝えしましたが、第5弾は、リテールカンパニーでAndroidアプリ開発を担当する、 nozaking のインタビュー内容をお届けします! 自問自答しようと思いましたが、umemoriさんがインタビュワーを引き受けてくれました🙏感謝 第5弾:nozaking n :インタビュイーの nozaking の発言 u :インタビュワーの umemori さんの発言 delyに入社したのはいつですか? n 2020年7月です! u Androidアプリエンジニアとして採用したけど、最初はサーバーサイドのお仕事をしていましたよね💡 n そうですね! 入社して3〜4ヶ月ほどサーバーサイドエンジニアとして過ごしたのち、2020年の11月ごろにクラシルのAndroidチームにジョインしました。 当時はリアーキテクチャに取り組んでいましたね。 ↓リアーキテクチャのことが少し分かる記事(2年前です😲!) tech.dely.jp いまはどんなお仕事をしていますか? n 今はリテールカンパニーという事業部で新規事業のサービスを開発しています。 ↓新規事業や組織について軽く触れている記事です note.com どうしてAndroidアプリエンジニアになったのですか? n 私がAndroidアプリエンジニアになったのは、新卒1年目の時、入社した会社でのスマートフォンアプリ事業の新規立ち上げがきっかけでした。 ちょうど入社のタイミングで、その事業部への新卒受け入れもあり、「これから盛り上がっていきそうな事業だな」「裁量のある仕事ができそうだな」と思い、希望を出したところ配属が決まりました。 なぜそこでAndroidアプリ開発をやることになったのかというと、当時はiPhoneが広く普及し始めた時期だったのですが、その時の事業部長が「これからはAndroidの時代が来る!」と言ったからですね🤔(Honeycombがリリースされた頃です) ↓当時の最新バージョン developer.android.com u Androidアプリエンジニアとしてのキャリアを積んでどうでしたか? n うーん……良かったんじゃないでしょうか! 当時、Android端末なんて持ってる人はあんまりいなくて、「自分でアプリ作ったんだー」と知人に言ってもそんなに反応は良くなかったんです。ですが、最近だとみんな普通に持っているので、自分が作ったものを使ってくれている喜びを感じられますね。 u たしかに、昔に比べれば随分増えましたね。昔は周りはiOSしか持ってないみたいな感じでした😂 n そうなんですよね。人がちゃんと使ってくれるものを作っているというところで、やってて良かったと思いますね。 どうしてdelyでAndroidアプリを開発することにしたのですか? n まずdelyに入社した理由ですが、転職を考えていたとき「もっとユーザー目線で仕事したい」「ユーザーに価値を提供したい」と思っていました。ちょうどその時Twitterのタイムラインに「クラシルの開発のオンボーディング資料を公開します」という坪田さんのツイートが流れてきたんですよね。それを読んでみたら、「ユーザーファースト」というワードやチーム開発の方法とか、当時の私の求める働き方がそこには書かれていて「これじゃん!」と思いました。そして求人の応募ボタンをポチッと押しました! ↓その時のツイート クラシル開発チームのカルチャー言語化してみました! / ユーザーファーストであり続けるために開発チームオンボーディング資料を作ってみた|坪田 朋 @tsubotax https://t.co/vA81SOgPQV — 坪田 朋 / クラシル (@tsubotax) 2020年2月27日 n あと、他の会社もいくつか受けていたんですが、クラシルは自分も使っていて身近なサービスでしたし、採用面接をしてくれた梅森さんとか面白そうな人だなと思ったのでdelyにしました。 n 次に、Androidアプリ開発をすることになった理由ですね。転職活動していた当時の自分がユーザーにちゃんと価値を提供できるのはなにか?と考えた時、一番経歴が長いAndroidアプリ開発だなと思ったので応募しました。 ですが、いざ入社したらサーバーサイドエンジニアになってましたね。 それはそれで、当時のdely社では必要な役割でしたし、何か価値が提供できるなと思ったので、「サーバーサイドエンジニアでも良し!」と思ってお受けしました💪 u 入社時とかなり状況は変わっていますが、入った時と今で気持ちの変化みたいなものはありますか? n 最初はクラシルの開発をしていましたね。クラシルには既にたくさんユーザーがいたので、ユーザーを大事にしようというか、クラシルのユーザーって何考えてるんだろうっていうのを考えて開発していました。今はリテールカンパニーに異動して、新規サービスの立ち上げのために収益性みたいなところも気にしていますね。あと、toC、toBの面、両方ありまして、気にするところが結構増えました。そういうとことが変わったところかなと思います。 u toBの登場人物もまたユーザーともとらえられるので、ユーザーとして存在する人の種類が増えたと考えられますね。 n たしかに! u toCだけではなく、toBのことも視野に入れるという変化ははポジティブに楽しめていますか?辛いところもあれば聞いてみたいです。 n 対応に悩む瞬間があるところですね。関わる人が多いからこそ特殊な事情が入ってきてしまう場面もあるのですが、全員の求めるものを反映していると手が足りなくなってしまう。とは言え、ビジネスパートナーとして無視することはもちろんできないので、折衷案を考えたりそのバランスを取るのが難しくもあります。 その反面、toC/toBどちらも対象としてるといただく意見が多種多様なんですよね。「そんなふうに感じる面があるのだな」という新しい発見があるのはポジティブに感じています! Androidアプリ開発の魅力はどんなところだと思いますか? n 自分がAndroidアプリを開発しはじめたのが2011年からだったのですが、当時は動きがカクカクでUIがあまりかっこよくなかったんですよね。その後、Material Designが発表された時に「すごくかっこよくなってる!!」って思ったんですよね。 u Material Designはredさん(リテールカンパニーのデザイナー)が好きなやつですね。 n そうですね😁 ↓Material Designが好きなredさん(nozakingの同僚) twitter.com n Material Designが出た時に、「ちゃんとかっこよく作れる!」と思いましたし、それ以前からもOSとしてちゃんと進化してたなって思うんです。その進化を見てきたので、前回のインタビューでの梅森さんの言葉のまんまですが「まだ掘れるじゃん!」って思いました。それがすごく楽しいなって思っています。 まだ掘れますね。モバイルアプリ開発の世界は。 dely Androidアプリエンジニアインタビュー 第4弾 umemoriさん - dely Tech Blog u Material Designに関しては最近また大きなアップデートがありましたよね。こちらもすごい進化してますよね。 n そうですね。 iOSもHuman Interface Guidelines等がアップデートされたり、UIのトレンドが変わっていったりってのも楽しいんですけど。Androidを長くやっている身としては、Material Designの発表は衝撃的でしたね。 u それまでのAndroidってなんとなくビジネス向けという感じでかっこよくはなかったですよね。Material Designが登場したことによって、「ツールとして便利だから使ってる」的なものから「かっこいいから使ってる」という人がもしかしたら増えたかもしれないですね。 n 昔はアプリを作る上でのデザインシステムという概念がなかった気がするんですよね。好き勝手にボタン配置したり。 公式的にデザインガイドラインが登場したことで、アプリの中の世界だけじゃなくて、Androidを使ってる人向けならこういうふうに作ると使いやすいですよ、というのが明確に示されましたよね。例えば、ボタンの右がポジティブで左がネガティブ、のように。そういう部分がしっかりと整ってるからAndroidアプリ開発者もユーザーに優しいUIを実現しやすくなったなというのを感じます。(最初は私も無茶苦茶なUIのアプリを作っていたものです😅) u そういうのが整備されているっていうこと自体がプラットフォームの魅力なところありますよね。 n そうですね! delyでのAndroidアプリ開発にはどんな魅力があると思いますか? n 担当領域外のメンバーとの連携しやすさみたいなものがあって良いなと思っています。例えば、先ほど登場したデザイナーのredさんと一緒に仕事していますが、Material Designを理解した上でちゃんとしたデザインシステムを作ってくれているので実装しやすかったりしますし、UIについての議論もしやすいんですよね。以前の現場だと、一枚絵のデザインからエンジニアがよしなに解釈して実装しなきゃいけないなんてこともありましたので……。 u よしなに解釈しなくちゃいけない事情はAndroidアプリ開発あるあるですね😢 iOSのデザインだけが用意されて、それを見ながらAndroidに最適なUIを実装するという話はよく聞きますし。 n そうですね。 その辺がちゃんと整備されているので、エンジニアとしてすごく仕事しやすいですし、UIに関する議論もできるのですごく良いと思う部分ですね。 それはクラシルでもリテールでも同じく言えることです。 u デザイナーもエンジニアも、それぞれの領域を越えるような部分も勉強して取り組んでいますよね。良い相互作用が働いている環境だと思います。 n 私もそう思います! n あとは、自分以外のAndroidアプリエンジニアがいるという環境が私にとっては大きいです。前職では1人ぽっちだったので、実装面で誰にも相談できなかったんです。 例えばクラシルの時は、自分以外のAndroidアプリエンジニアが当時は4人いて、考え方もそれぞれで異なり、議論が白熱した時もありましたね。そういう環境に身を置いてるだけでも勉強した気になるし、「そんな考え方があるんだ!」という新たな発見もありました。 そういう状況を経てから新しくアプリを作ることになったので、「他の人が見た時も違和感がないようにしよう」というのを考えられるようになったと思います。 u いろんな人と議論した経験があるから、他者の視点を考えながらコードを書くことができるようになったっていうことですね。 n そうですね。とても良い経験でした。 delyのAndroidアプリ開発をする中で、やりがいや達成感を感じたエピソードを教えてください。 n Android開発ではないエピソードなのですが、クラシルで検索機能を改善して、良い検索体験を提供しよう、という目的をもつスクワッドにいた時、ユーザーの行動分析をしたり、ユーザーが何をしたらクラシルに価値を感じたと判断できるのか?というのをものすごく考えた時間があったんですよね。その時の取り組みはやりがいを感じたことの1つですね。 u 当時は開発だけではなく、結構いろいろやってましたよね。 n そうですね。開発したっていうよりは、分析した方が多かったと思います。 当時いたデータエンジニアやアナリストの方に分析の仕方を教えてもらったりしながら、本格的に取り組んだ記憶があります。タイミング的にも入社した直後に取り組んだことで、自分の中ですごく楽しかったし印象に残った出来事だったんですよね。 u アプリ開発って、コードを書くだけではなく、ユーザーやデータと向き合うというのも含まれると思うんですよね。コードを書く部分以外にも取り組めて達成感を感じられるというのはdelyのアプリ開発ならではだと思いますね。現場によってはぜんぜんアプリエンジニアがデータ見ない場合もあると思いますし。 n おっしゃる通りですね!delyならではの良いところです。 その他、世の中に発信したい思いがあればお願いします。 n 自分は「Androidアプリ開発やるなら絶対delyだぜ!」とは思っていないんですけど、delyだからある機会、例えばユーザーに向き合える等がありますよね。そこに惹かれた人にとっては良い場所だろうなと思うし、そういう人と一緒に働きたいですね。 あと、入社前はクラシルは出来上がったサービスだと思っていたんですが、全然そんなことはなかったです。まだまだやりたいことは多いし、1つ上の段階に進もうと思ったら結構大掛かりな挑戦が必要だったり……他にも自分がやっているように新しいサービスを立ち上げる、みたいなこともがっつりあるので、腰を据えたいというよりまだ荒波に揉まれたいという人にも良い環境だなって思います。 u これからも、「もうやることないよね、保守しかしないよ」みたいな状況は想像できない会社ではありますね。そういうところに魅力を感じる方はdelyに来るといいかもしれないですね。 n そうですね! さいごに 第5弾はいかがでしたでしょうか? nozakingが現在開発に携わっている新規事業についてはあまり詳細を語れなかったですが、興味を持っていただいた方はぜひお声かけください!カジュアルにお話しする場で詳しくお話しさせてください🙂 nozaking以外にも、delyのメンバーとお話してみたい!と思ってくださった方は、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
こんにちは、クラシルiOSのEMの @RyogaBarbie です。 2021年のSwiftUI 3、2022年のSwiftUI 4からSwiftUIを本番で使用するアプリも増えたのではないかと思いますが、 クラシルでも新規開発される画面では積極的にSwiftUIを使用していこうという流れになっています。 今回はSwiftUIを導入するに当たって、チーム内で採用してる実装方針について紹介していこうと思います。 主にWWDCのSwiftUI関連のセッションなどでAppleが公式で発表、推奨しているに則るようにしています。※おすすめのセッションは文末に記載しています。 クラシルの実装方針には Structural Identityを意識した実装 不活性修飾子やmodifier内で三項演算子などをうまく活用する Spacerと.paddingの使い分けの方法 データソースを持つViewを1つに限定する Viewで@State, @Bindingを使用しない etc. などがあります。 今回は、その中でも「データソースを持つViewを1つに限定する」、「Viewで@State, @Bindingを使用しない」の2つに関して、 どのような方針で実装しているのかを書いていこうと思います。 「データソースを持つViewを1つに限定する」 これはWWDCのSwiftUIのセッション内でも言及されている Single Source of Truth を実現するためのものになります。 クラシルiOSではSwiftConcurrencyベースで実装されている状態管理ライブラリの Actomaton を導入しています。(今回の主題とはあまり関係ないので詳細な説明は省きますが) stateを管理するActomatonのstoreを持つViewを画面ごとに1つに限定しており、 データソースを持つことを明確にするためにも、ScreenViewというsuffixを付ける運用にしています。 (ScreenViewはUIkitにおけるUIViewControllerのような位置付け) 「Viewで@State, @Bindingを使用しない」 「データソースを持つViewを1つに限定する」にも関連してくることですが、 Actomaton.storeでのデータソースの管理と@StateによるViewでのステート管理によって、複数箇所でのデータソースなどを管理をすることになりSingle Source of Truthを実現できなくなります。 SwiftUIのComponentの再利用性を考えた場合、@Stateを@Bindingという形で数珠繋ぎで引き渡していった時に、孫のComponentで親のScreenViewが知らない変更などが起きており、複数の画面で再利用する場合に特定の画面では不整合やバグに繋がるなどのケースも考えられます。 そこで、@Stateの使用を禁止することでActomatonで管理するStateを唯一のデータソースとして、Single Source of TruthとステートレスなViewを実現します。 (ここでいうステートレスは、WWDCで言及されてる「依存」がないということではないです) それらを実現するために、クラシルで定義&導入しているのが ViewData です。 ViewDataについて ViewDataはViewをレンダリングするのに必要な情報を構造化したもので、APIから取得したResponseやデータベースから取得した情報などから、Viewのレンダリングに必要な情報だけを抽出したものです。 クラシルはマルチモジュール設計にて運用されており、大きくCoreレイヤー、Featureレイヤー、Appレイヤーがあり、ViewDataはFeature(UI)レイヤーで定義&取り扱うという方針になっています。 画面遷移時に必要な情報などは、Feature間を跨ぐために必要な型を集めたFeatureInterfacesモジュールに定義されている型で行い、Viewのinit時などにViewDataに変換する形です。 (※クラシルでは画面遷移や、Navigation、Tabなどの管理はUIKitベースで行っています) 実装例 TwitterライクなUIでの使用方法をサンプルとして説明します。 自分のツイートが一覧して表示されるようなUIです。 ツイートをTweetItemView, ツイート一覧をTweetListViewとして定義し、TweetListViewでは内部にTweetItemViewを複数個持つような実装になっています。 リツイート、いいね、シェア等のリアクション、+ボタンのタップでモーダルの表示などができます。 ツイートごとにデータをどうやって扱っているか、+ボタンタップ時のモーダルの制御をどうやるか、ScreenViewでの定義を例にしていきます。 ツイート ツイート TweetItemViewの表示するのに必要なuserName、userId、tweetText、didLikeなどをTweetItemViewDataとして構造化し定義し、それらを基にViewを描画します。 struct TweetItemViewData : Sendable , Equatable { let tweetId : UUID let tweetText : String let tweetPostedAt : String var didLike : Bool var didRetweet : Bool let userId : String let userName : String let userImageName : String } struct TweetItemView : View { let viewData : TweetItemViewData let didTapHeartAction : ( String ) -> Void ... var body : some View { HStack(alignment : .top) { UserIconView( imageName : viewData.userImageName ) VStack(alignment : .leading) { UserSectionView( userName : viewData.userName , userId : viewData.userId , postedAt : viewData.tweetPostedAt ) Spacer() Text(viewData.tweetText) Spacer() ReactionSectionView( didRetweet : viewData.didRetweet , didLike : viewData.didLike , didTapHeartAction : { didTapHeartAction(viewData.tweetId) }, ... ) } } } struct TweetListView : View { let tweetItemViewDatas : [ TweetItemViewData ] let didTapHeartAction : ( String ) -> Void ... var body : some View { ForEach(tweetItemViewDatas, id : \.tweetId) { viewData in TweetItemView( viewData : viewData , didTapHeartAction : didTapHeartAction , ... ) } } } didTapHeartActionというハートアイコンをタップした時に行うClosureの中身には、データソースを更新するアクションを渡します。 didTapHeartAction : { viewModel.didTapHeartAction() } 呼び出したアクション内でViewDataが更新されると、更新されたViewDataに依存しているViewがSwiftUIによって再レンダリングされていきます。 ScreenViewData ScreenView内の全てのViewをレンダリングするのに必要な依存をScreenViewDataとして定義しています。 ScreenView内で表示されている子Viewのレンダリングに必要なViewDataも含めて定義され、ScreenViewをレンダリングするのに必要な全ての情報を構造化、階層化したものになります。 struct TimelineScreenViewData : Sendable , Equatable { // Tweetの一覧 let tweetItems : [ TweetItemViewData ] // apiの通信状態によってローディングを表示するか var isDisplayLoading : Bool = true // モーダルの表示の制御 var isPresentedTweetView : Bool = false } ViewModelなどで実装する場合は、 Viewのレンダリングに必要のない状態などはViewModelのpropertyなどで持たせ、ScreenViewDataのみ@Publishedで公開するような形になるかと思います。 前述の通り、クラシルiOSでは画面遷移などはUIKitベースで行っていますので、 isPresentedTweetViewなどの状態はViewDataでは管理していません。 モーダル SwiftUIでのモーダルは、.sheetの引数のisPresentedは、Binding によってモーダルの表示非表示を制御しています。 以下は、ViewModelのscreenViewDataにモーダルの表示状態を管理する変数としてisPresentedTweetViewを定義し、isPresentedTweetViewの状態によってモーダルの表示制御する例になります。 @State, @Bindingの使用を禁止しているため、.sheetやTextFieldなどでBinding<>を使用した双方向バインディングが実装する場合、 Binding.init(get:set:)を使用して実装する形になります。 struct TimelineScreenView : View { var body : some View { ScrollView { TweetListView( tweetItemViewDatas : vm.screenViewData.tweetItems , ... ) // ここに注目 .sheet(isPresented : Binding.init ( get : { vm.screenViewData.isPresentedTweetView }, set : { bool in vm.setIsPresentedTweetView(bool) })) { /// ツイート入力フォーム TweetView( ... ) } } } } get:に対しては、モーダルの表示非表示の状態を表しているisPresentedTweetViewの値を、 set:に対しては、モーダルを閉じるためのスワイプアクションなどを実行した時に呼ばれるboolの値を受け取り、viewModelなどでviewDataを更新します。 ※実際にはクラシルはActomatonベースで実装しています。 終わりに クラシルにおけるSwiftUIの実装方針について書かせてもらいました。 宣言的UIであるSwiftUIを用いた実装方針については色々ありますが、クラシルではViewDataを定義しSingle Source of Truthを実現しています。 SwiftUIの実装方針に関してはまだまだ手探りでやってますので、SwiftUIでの開発したいという方をお待ちしてます! !!採用ページはこちら!! https://careers.dely.jp/ careers.dely.jp おすすめのセッション developer.apple.com developer.apple.com
アバター
はじめに あけましておめでとうございます! クラシルバックエンドエンジニアの加藤です。 クラシルでは2022年4月から一部ユーザーに向けてパーソナライズされたフィードをリリースしました。 (以降、パーソナライズフィード) パーソナライズフィードではSnowflakeを活用してレコメンドをReverse ETLを行い実現しました。 今回はアーキテクチャの説明と課題・今後の展望について紹介します。 パーソナライズフィードについて ホーム画面に表示される一連のコンテンツのことを指します。 クラシルではこれまで複数のキュレーションされたコンテンツをまとめたリスト(画像左)を表示していましたが一部ユーザーに向けて単一のフィード(画像右)に変更しました。 アルゴリズムについて コンテンツ閲覧履歴などの行動ログを分析してルールベースでレコメンドを行っています。 機械学習の導入を検討していますが導入にかかるコストや効果が不明確であることに加えてクラシルにおいてどのようにコンテンツをレコメンドしていくことがユーザーへの価値提供につながるのかを検証しているフェーズであるため現状はルールベースを採用しています。 クラシルのデータ基盤について クラシルではDWHにSnowflakeを採用しておりユーザーの行動ログを管理しています。 ユーザーの行動ログは構築されたデータパイプラインを経由してニアリアルタイムに分析することが可能です。 また、dbtを採用しておりSnowflake上に取り込まれたデータをモデリングして各種KPIやレコメンド時に使用するデータを整備しています。 詳しくはこちらの記事で紹介しています。 クラシルでのSnowflakeデータパイプラインのお話&活用Tips Reverse ETLとは? アプリケーションなどから生成されたデータを抽出(Extract)・適切な形にデータを変換(Transform)・データ基盤にデータを格納(Load)する一連の処理をETLと呼びます。 Reverse ETLとはETLの逆でデータ基盤に格納されたデータを抽出・変換してアプリケーション側に格納する処理を指します。 アーキテクチャ 処理の流れ 大まかなレコメンドまでの処理は以下の通りです。 アプリ内の行動ログをKinesis Data Firehoseを経由してS3に配置 一定の時間・ログサイズになるとS3にログファイルが配置されます。 配置されるS3にはSnowflakeの外部ステージが設定されておりS3からSnowflakeにデータロードすることができます。 S3からSnowflake側にログを格納 S3(外部ステージ)にログファイルが配置されたことをトリガーにSnowpipeを使用してマイクロバッチ的にログをSnowflake上にロードします。 これによりニアリアルタイムにログの分析をすることが可能です。 レコメンドに必要なデータを作成・更新してレコメンドタスクを実行 Snowflakeにロードされたログからレコメンドに使用するためのデータ作成・更新・レコメンドをSnowflakeのタスク・サーバレスタスクを使い実行します。 タスク・サーバレスタスクにはDAGを設定することができるので処理に依存関係をもたせることでレコメンドに必要なデータの作成からレコメンドまでを一連の流れで実行することができます。 アプリケーションのDBへレコメンド結果をロード レコメンドタスクの後続タスクで外部関数を使用してアプリケーションが利用するDB(DynamoDB)へ結果を書き出します。 外部関数はSnowflakeから外部システムのコードを呼び出すことができる機能です。 外部関数を使用することでログの格納からレコメンドをイベント駆動で実現できたことやサーバサイドからのレコメンド結果の取得が不要になりSnowflake上で処理を完結させることができます。 また、処理する件数が多い場合は効率的にさばけるようにSnowflake側で分割されて外部関数が実行されることで効率的にレコメンド結果を書き出すことできます。 課題と今後の展望 エラー・障害の監視強化 これまでデータ基盤はデータを分析するための基盤であったためユーザーに影響があることはありませんでした。 今回のパーソナライズフィードの取り組みによって状況は変化しユーザーに影響のあるデータ基盤に状況が変わりました。 それによりこれまで以上にエラー・障害検知やSLOの定義などの重要性が上がりました。 これまで大きな障害等なく運用できていますが改めてアラートの設定などを見直していく必要があると考えています。 チューニング 現在パーソナライズフィードは一部のユーザーに向けて展開しているため全展開となればデータ基盤に対するコスト・負荷やレコメンドの処理時間の増加などが見込まれるため現在の設計や今後の追加実装においても負荷に耐えうる設計になっているかを考えて実装する必要があると考えています。 さいごに クラシルではデータエンジニア・MLエンジニア・データアナリストを募集しています! 今後は機械学習モデルの導入を進めてよりユーザーへの価値提供を加速させていきたいと思っていますので ご興味のある方はぜひカジュアルにお声かけください!! dely.jp
アバター
こんにちは、クラシルAndroidエンジニアのもとはしです! 最近はひたすら広告周りの改善をしてます。 今回はGoogle Mobile Ads SDKで用意されている広告インスペクタを使って、メディエーションとの接続を確認できるようにしてみたいと思います! なぜ導入したのか? 新たにメディエーションを追加したとき、皆さんはどのように接続確認をしているでしょうか? 自分のチームでは広告のAdChoiceアイコンを確認したり、管理画面上で追加したメディエーションからの配信を確認することで接続を担保しています。 とはいえ、新しくメディエーションを追加する度に配信設定者 / 実装者間でコミュニケーションを取るのは少々手間ですので、サクッとアプリ内で接続を確認したいなと思いました。 そこで広告インスペクタの出番です 広告インスペクタとは? 公式ドキュメント より 広告インスペクタは、承認済みのデバイスにおいて、モバイルアプリ内で広告リクエストのテストを直接分析できるアプリ内オーバーレイです。 上記ドキュメントの記載通り、メディエーションの接続確認だけでなく、エラーの特定にも使えたり入札結果を覗き見できたりします。便利😻 導入手順 テスト広告モードに切り替える 広告インスペクタを使用するには、デバイスをテストデバイスとして追加する必要があります。 しかしながら、管理画面上で担当者のデバイスを追加するのは面倒ですし、テストモードの切り替えもスムーズにやりたいところです。少し調べたところ、ANDROID IDをmd5ハッシュ化してuppercase()することで同一の文字列が得られました🙌 @SuppressLint ( "HardwareIds" ) private fun getTestAdDeviceId(): String { val deviceId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) val digest = MessageDigest.getInstance( "MD5" ) digest.update(deviceId.toByteArray()) val messageDigest = digest.digest() val hexString = StringBuffer() for (i in messageDigest.indices) { var hex = Integer.toHexString( 0xFF and messageDigest[i].toInt()) while (hex.length < 2 ) hex = "0 $hex " hexString.append(hex) } hexString.toString().uppercase() } 続いてsetTestDeviceId()を使用することで、テストモードに切り替わります。 if (isYourAppDebug) { val testDeviceId = getTestAdDeviceId() val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(testDeviceId)).build() MobileAds.setRequestConfiguration(configuration) } テストモードにすると、以下のようなテスト広告が配信されるようになります。 広告インスペクタを開く テストモードにしたら、あとはMobileAds.openAdInspectorを呼ぶだけです。 MobileAds.openAdInspector(context) { error -> error?.let { // show snack bar or do something } } ラムダ内にはエラー発生時の処理を記述できます。 テストモードが有効になっていないと、エラーが発生しインスペクタを開けませんでした。 終わりに 今回は広告インスペクタの表示方法を解説してみました! クラシルではテストモードの切り替えと広告インスペクタを社内配布用のアプリに組み込み、実装者だけでなく配信設定者でもメディエーションの接続確認ができるようにしています。 メディエーションの接続確認だけでなく入札結果の確認色々な機能が用意されているので、導入されていない方はぜひ導入してみてはいかがでしょうか? 最後に クラシルではAndroidエンジニアを大募集しております! クラシルAndroidメンバーってどんな人がいるの?と気になる方は以下のタグからインタビュー記事が見られますので、ぜひご覧ください! #クラシルAndroidチーム 実際にメンバーと話してみたいと思ってくださった方にはカジュアルにお話しする場も設けられますので、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
こんにちは、delyのリテールカンパニーで新規サービスのAndroidアプリ開発を担当しているnozakingです。 今回もdelyのAndroidアプリエンジニアにインタビューした内容をお届けしたいと思います。 今回は第4弾として、クラシルカンパニーのumemoriさんにインタビューしました! 第1弾のインタビュー(parayaさん)は こちら↗︎ 第2弾のインタビュー(Jさん)は こちら↗︎ 第3弾のインタビュー(kenzoさん)は こちら↗︎ インタビュー経緯 第4弾の内容の前に、インタビューに至った経緯を説明させてください。 私はAndroidアプリエンジニアの採用活動に携わらせていただいているのですが、面談や面接をしていくなかで、エンジニアとしてこの先どう歩んでいくかを決めかねている方の声を聞くことがよくありました。 そんなモヤモヤを晴らすような、いい話ができたらいいなと私は思いました。 そこで、せっかくdelyには複数のAndroidアプリエンジニアがいるので、インタビュー企画を始動しました🦸‍♂️!! 世の中のエンジニアの皆さんに、少しで もAndroidアプリ開発の魅力 (と、ついでにdelyで働くことの魅力)をお伝えできれば幸いです。 delyに在籍するAndroidアプリエンジニア全員にインタビューしていくつもりで、月一を目標に発信していきます🚀 いざ、インタビュー 冒頭でもお伝えしましたが、第4弾は、クラシルカンパニーでクラシルのAndroidアプリ開発を担当する、 umemori さんにインタビューしました! 第4弾:umemoriさん u :インタビュイーの umemoriさん の発言 n :インタビュワーの nozaking の発言 delyに入社したのはいつですか? u 2017年5月です。 いまはどんなお仕事をしていますか? u いろんなことやってきたんですが、いまはAndroidチームのテックリード的なことをやっています。 どうしてAndroidアプリエンジニアになったのですか? u 10年くらい前(Androidの4系のOSが出た頃だったと思います)、フリーランスとしてある会社に出向していました。 ある時、その会社に1人しかいないAndroidアプリエンジニアが退職してしまい、Androidアプリを開発する人がいなくなってしまったんですよね。 当時僕はPHPを書いていたんですが、昔Javaをやっていた経験から「Androidアプリ開発をやりませんか?」と話が来て、引き受けたのが始まりでした。 Javaの経験があったとはいえ、Androidアプリ開発に関しては何も知らないところから参加しました。 n その時は、どうやってAndroidアプリ開発のことを勉強したりスキルを高めていったのですか? u 当時、Androidアプリ開発の情報はあまりなかった気がするんですよね。 そんな中、yanzmさん(Y.A.M の 雑記帳)とか、Tech Boosterさんの記事がすごく参考になった記憶があります。 y-anz-m.blogspot.com techbooster.org u あとは体当たりで覚えていきましたね。改修の案件が多かったことから、ソースコードを読む機会が多く、良いコードも悪いコードも読みつつ、動かしつつ、学んでいった感じです。 公式リファレンスは(今ほど親切ではなかったのですが)割と参考にしていましたね。 n 昔って結構英語しかない感じでしたよね。 u 大抵そうでしたよね。 なのでyanzmさんをはじめとして、解説してくれているブログはとても参考になったし助かりましたね。 どうしてdelyでAndroidアプリを開発することにしたのですか? u 前回のkenzoさんの記事 を見たんですが、僕の回答もそのまんまですね。 u 僕もエージェントきっかけでdelyを知ったんですけど、大竹さんと堀江さんと話して「若いし伸びていきそうだな」と思いました。 あと大竹さんが熱心に喋っていたのが印象的で、面白い会社だなーと感じて入社を決めました。 ※大竹さんについては是非下記をチェックしてみてください👀 mobile.twitter.com note.com n umemoriさんも大竹さんと一緒に仕事がしたいと思って入ったんですか? u そりゃそうですね! n そのころってAndroidアプリエンジニアは他にいたんですか? u 1人いましたよ。僕が入ってすぐ辞めちゃいましたけど(´·ω·`) n なるほど。当時の大変さがうかがえますね。 Androidアプリ開発の魅力はどんなところだと思いますか? u いろいろあると思うんですけど、モバイルアプリ開発の楽しさでいうと 手元で動くアプリが作れるところ ですね。これはモバイルアプリを開発している人は割と思っていることなんじゃないかなと思います。僕もそれが楽しくてAndroidアプリ開発に結構のめり込んでいきました。 u あと、モバイルアプリ特有の難しさっていうのがありますよね。 例えばスマートフォンってスリープしたりするし、ユーザーの使い方に沿った形で状態がどんどん変わっていきますが、それに合わせてちゃんとアプリを作ってあげないといけなかったり・・・。 それってサーバーサイド(僕はそれまでPHPをやっていました)とは全然違う世界だなと感じていて、そこが難しいところでもあり面白いところでもあると思います。 u 魅力って、デメリットというか、人によっては面白くないところの裏表だと思っています。 モバイルアプリではプラットフォーム自体がアップデートされていくから、何もしないとどんどんアプリが古くなってしまうというのがありますが、それを「作ったものがどんどん陳腐化していっちゃうから嫌だ!」っていう人もいると思います。ですが、 ちゃんと勉強してキャッチアップして合わせていく楽しさ がAndroidアプリ開発の魅力のひとつなんじゃないかなと思います。これはiOSも同様だと思います。 u Androidならではの魅力でいうと、ガラッと変わってきた面白さもありますよね。プラットフォーム自体もそうですけど、言語も大きく変わったじゃないですか。例えば 2017年にAndroid公式開発言語としてKotlinが追加されたり、ここ最近だとJetpackComposeが新しいUIツールキットとして発表されたりとか。 そういうパラダイム自体がどんどん変わっていく開発っていうのは、必要性がないとなかなか経験しにくいと思うんですよね。そういう場面がないと技術力って活かせないので、自分の力の奮いどころとしては非常に楽しい世界なんじゃないかなと思います。 なので、そういう エキサイティングな仕事 をしたい人はぜひAndroidアプリ開発をしましょうと言いたいですね。 n Jさんも同様に、RxがCoroutineに変わったり、JetpackComposeが出てきたりとか、そういうのが楽しいと言っていましたね。(第2弾インタビューの "Androidアプリ開発の魅力はどんなところだと思いますか?" にて) u その中でもう少し付け加えるとしたら、その中で変わらないものを考えながら技術的な意思決定をしていけるのが楽しいと思います。 変化したものを表面だけ追いかけていてもしょうがないんですよね。RxがCoroutineになったときになんで変わったのか(変わったというより共存したというイメージなんですけど)、Rxってなんだったのかをちゃんと理解していないと、表面を追いかけるだけになっちゃうと思うんですよね。 「Rxを使っていたらCoroutineが出てきた。」「なんか新しいのがでてきたからやってみよう。」だけじゃなくて、ちゃんとRxってどういうものだったのか、何を提供していたのか、じゃあCoroutineはそことどう違うのかというところをちゃんと理解しながら移行する、しないならしない、という意思決定をやっていけるのは楽しいところだなと思いますね。 n umemoriさんならではの視点ですね。他にもつついたらいっぱいでてきそうですw u あとは、サーバーサイドとの比較になるのですが、リリースしたものがユーザーの端末に届いてインストールされる楽しさもありますね。だからこそ気をつけなきゃいけないことが結構あります。例えばアプリが大きすぎちゃいけないなど、そのためにいろんなことを工夫するんですよね。 Androidアプリエンジニアの頭痛の種でもあるんですけど、 いろんな端末に対して工夫して対応してどこでも動くようにできた、みたいな楽しさ は魅力なんじゃないかなと思います。 n そこはiOSと比べてもAndrodの方がいろいろあるってのもありますしね u そうなんですよね。結局、困難じゃないですか。 困難こそが工夫の種 だと思っていて、そこに対して頭を使うのが僕は結構好きなんです。 そういうのが好きな人にはAndroidアプリ開発は結構おすすめです。 特にdelyでのAndroidアプリ開発にはどんな魅力があると思いますか? u どういう人にこの言葉を向けるかによって変わってきますね。 u 例えば受託でアプリを作ってきた方向けには、dely(クラシル)のようにずっと同じプロダクトに向き合ってアプリを作っていくということの魅力がお伝えできると思います。 受託でアプリを作る場合、創意工夫の余地はあるんですが、アプリをリリースするまでだったりするんですよね。だからいろんな制約によって技術トレンドを追えないことが多いと思うんです。とりあえず動くものを作れて、それで楽しいっていう欲求は満たせると思いますが。 dely(クラシル)では同じアプリが5年も存在し続けているのですが、その中でトレンドが変わっていきます。そのトレンドが変わっていくのを指をくわえて見ているだけかというとそうじゃなくて、キャッチアップしていかなきゃいけないし、なんでキャッチアップしていくのかも考えながら実際の機能開発と一緒に進めていく必要がありますよね。 これは先ほども言ったように1つの困難だと思うんです。そこをどうするのか頭を使っていくのは、 ずっとひとつのプロダクトに向き合うってことでしか得られない ことなのかなと思っています。 u 例えば今JetpackComposeが出てきていずれは移行しなきゃいけないけど、今あるソースコードを考慮してどうやるのかっていろいろ工夫する必要があると思うんですよね。全部書き変えちゃうのか、ちょっとずつ移行するのかとか。ちょっとずつ移行するっていうのはものすごく難易度が高くて・・・でも設計によるかな・・・。うちはちょっと特有の事情で難しいところはあるんですけど。😢 n うーむ、全部まるっと変えちゃう方が楽なのはたしかですよね。 u 考えなくていいからね。共存するんだったら、共存するなりの橋渡しのコードを書いてから移行していかなきゃいけないので、難しいです。JetpackCompose書きたいなっていう気持ちもあるけど、そこを ちゃんと考えてから移行するっていうのをやれる、やらなきゃいけないって言うのが面白いところ なんじゃないかな。 u あと自社サービスを開発している上では、ユーザーの声に向き合いやすい環境ではあると思います。ユーザーの声って直接聞けるわけじゃないですけど、例えばユーザーの行動ログとかレビューなどを大切にする文化があります。だからその中でAndroidアプリを作っていくっていうのは、「リリースして終わり」みたいな仕事と比べると、フィードバックを受けてじゃあどう改善するのか、しないのか、どう取り組んでいこう、っていうのを考えられる場に身を置けるっていう楽しさはありますよね。 n SlackにもTwitterの感想つぶやきや、ストアのレビューの内容が流れてきたり、割と見えやすいですよね。 u そうですね。思わぬことが起きますからね。お問合せでも、なんでそんなことが起きるんだって思って調べると、「ああ、こういう事か!」っていう発見につながったりしますね。 delyのAndroidアプリ開発をする中で、やりがいや達成感を感じたエピソードを教えてください。 u だいたい今までの話で網羅されてる気がしないこともないですけどw n より具体的なエピソードがあれば、お願いします! u ひとつあげるとしたらAndroidアプリのリアーキテクチャですね。 u リアーキテクチャを決意した当時(約2年前)はだいぶ古い作りを引きずって開発してしまっていたのもあって、いろいろ作りにくい部分がありました。そこを綺麗にして後の開発効率を上げていきましょうよって言ってリアーキテクチャしたんですよね。 u その時に、最初は1人でプロトタイプを作って、みんなにこんな感じですって説明して、何回か議論もしたかな・・・。最終的にはみんなを巻き込んでいって全員野球みたいな感じでやり遂げました。あと、全部QA回してしっかりやったというところにも達成感がありましたね。 必要性があればそういうことができる組織にいて、実際にそういう仕事ができる のはやりがいと言えるんじゃないかなと思いますね。 n リアーキテクチャやりたいですって言っても、難しい顔をされるイメージしかないですよね。この規模でできるってのはすごいなって思いますね。ユーザー数もかなり多いですし。 n あと、リアーキテクチャの対応時期はちょうど私がクラシルのAndroidアプリ開発チームに参加したタイミングだったんですけど、すごい勉強になるなって思ってました。クラシルを開発する上で理解を深めるってのもあったんですけど、「あ、そういう書き方もあるんだな〜」とかも知れたりして。 u リアーキテクチャをやった時は、深いところまでアーキテクチャを分解して説明してってのを結構丁寧にしたので、みんなの勉強にもなったのかなと思いましたね。 n なりましたなりました。 u リアーキテクチャには、JetpackComposeに移行しやすくするという目的も実はあったので、そっちもいずれちゃんとやりたいなと思っていますね。 n 楽しみにしております😁 その他、世の中に発信したい思いがあればお願いします。 u 改めて言いますが、モバイルアプリ開発はおもしろいですよ。やっていて思います。 もう10年くらいやってることになるんですが、たまには違うこともやってみないなっていう気持ちも湧くけど、それだけやってても楽しいですし、まだ掘れますね。モバイルアプリ開発の世界は。 u 最近、AndroidアプリはJetpackComposeも出てきたしAndroidの公式ドキュメントもすごい整ってきて、作り方が固まって、それを踏襲していけばいいみたいな雰囲気があるようなないような感がありますが・・・(世間の声はわからないですけど)、全然そんなことはないぞと思います。 本当のベストプラクティスってちゃんと見つけられていないんですよね。モバイルアプリ特有の事情も多いですし。例えば、スリープってのは結構難しい気がしています。いつでも中断されうるし、いつでもメモリがどっかいっちゃって、そのあと帰ってこなきゃいけないっていうモバイルアプリ特有の事情な気がするんですよね。他にもセンサーとかカメラとかが色々ついてたり・・・(活用しないアプリだったら関係ないけど)。そういう泥臭い事情などを綺麗に整理して「この通りに作ればいいんですよ」って言うことってまだできていない気がするんですよ。 u Googleの公式ドキュメントもあくまでガイドラインですよ、みたいなふわっとさせて予防線を引いているんだけど、本当にこう作ったらいいんですよ、ってのがまだ見つけられていない気がします。 ・・・というところがまだ掘れる気がしていて、まだ遊べるなって思います! ネイティブアプリはまだまだ形は変わるかも知れないですけどね。いまKotlinで書いてますけど、5年後Kotlinじゃなくなってるかも知れないですし、あるいはAndroidじゃない何かが出てきている可能性もありますし、AndroidのKotlinだけやっていればいいですよってことは全然ないですね。でも「 モバイルアプリは楽しいぜ! 」ということを言いたいです! n umemoriさん、熱く語っていただき、ありがとうございます!!🙌 さいごに 第4弾はいかがでしたでしょうか? クラシルのAndroidチームのテックリードである、umemoriさんならではのお話が聞けのではないでしょうか💡 インタビューしていて、Androidアプリ開発を楽しんでいる雰囲気を感じられました😁 前回の第3弾 のkenzoさんへのインタビュー記事も公開しておりますので、よかったらご覧ください📄 記事中で、umemoriさんについても触れられています! tech.dely.jp 次回は第5弾として、いままでインタビュワーだったnozakingが回答者として実施したいと思っております🎤ドキドキ 引き続きよろしくお願いします! もし、delyのメンバーとお話してみたい!と思ってくださった方は、カジュアルにお話しする場も設けられますので、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
どうもクラシルAndroidエンジニアの @MeilCli です。今回はAndroidのちょっとした便利テクの紹介です 序文 Androidの開発をしていると極稀に標準のTextViewやImageViewを独自の実装に置き換えたくなることがありますよね *1 たとえばすべての画面で使うほど重要な処理や、なんらかの不具合に対処するワークアラウンドをすべての画面に一括で反映したいなど。androidx.appcompatはこれと同様な状況と言え、実際に LayoutInflater.Factory を使用して一括でViewを置き換えています。具体的な挙動を申し上げると、layout.xmlで <TextView /> を書いたときは自動で AppCompatTextView に置き換えられてViewが生成されています クラシルAndroidではTextView/Button/EditTextを独自のものに置き換える必要が生まれ、LayoutInflater.Factoryを使わずにAppCompatViewInflaterを使う方法をとったので紹介します AppCompatViewInflaterについて androidxのリポジトリー でLayoutInflaterを扱っている箇所を検索すると AppCompatDelegateImpl が見つかります。ここでLayoutInflater.Factory(正確にはLayoutInflater.Factory2)をLayoutInflaterに設定することで、LayoutInflaterを使ったViewの生成の際にxml上のタグをAppCompat系のViewに置き換えて生成しています 実装を追っていくと createView(View, String, Context, AttributeSet) においてAppCompatViewInflaterを生成して、それを利用して実際のViewを生成しています。また、AppCopatViewInflaterを生成の際にはThemeから viewInflaterClass を取得してリフレクションで生成するクラスを置き換えれるようになっています if (mAppCompatViewInflater == null ) { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass); if (viewInflaterClassName == null ) { // Set to null (the default in all AppCompat themes). Create the base inflater // (no reflection) mAppCompatViewInflater = new AppCompatViewInflater(); } else { try { Class<?> viewInflaterClass = mContext.getClassLoader().loadClass(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() .newInstance(); } catch (Throwable t) { Log.i(TAG, "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default." , t); mAppCompatViewInflater = new AppCompatViewInflater(); } } } ※ AppCompatDelegateImpl.createView から抜粋 また、 AppCompatViewInflater では各Viewの生成メソッドがprotectedになっているため、AppCompatViewInflaterを継承してメソッドをオーバーライドすれば独自に作成したViewに一括で置き換えることができます クラシルでは以下のようにクラスを作り、Themeに設定しました import android.content.Context import android.util.AttributeSet import androidx. annotation .Keep import androidx.appcompat.app.AppCompatViewInflater import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView @Keep class KurashiruViewInflater : AppCompatViewInflater() { override fun createTextView(context: Context?, attrs: AttributeSet?): AppCompatTextView { return ContentTextView(context, attrs) } override fun createButton(context: Context?, attrs: AttributeSet?): AppCompatButton { return ContentButton(checkNotNull(context), attrs) } override fun createEditText(context: Context?, attrs: AttributeSet?): AppCompatEditText { return ContentEditText(checkNotNull(context), attrs) } } <style name = "BaseTheme" parent = "Theme.MaterialComponents.Light.NoActionBar.Bridge" > <item name = "viewInflaterClass" > path.to.KurashiruViewInflater </item> </style> TextViewとかを一括で置き換えたいことってあまりないんじゃ? UI定義によってはそうかもしれません。クラシルAndroidではTypographyの設定でlineHeight *2 やletterSpacingなどのTextAppearanceでは設定できない値を一括で反映させたい需要がありました どういうことかというと、TextViewに対する属性の一括設定には style を指定する方法と android:textAppearance を指定する方法があります。 style はすべての属性を指定できますが、画面ごとやパーツごとにstyleを指定したい場面があるので、Typographyの指定では android:textAppearance で指定する方法のほうが何かと便利です。しかし、 android:textAppearance ではTypographyとして設定したい属性すべてに対応していません *3 。そのためクラシルAndroidで使うTypography向けにTextViewなどを拡張する必要があったのです 実装 すべてを書くと長いので結構省きますがだいたい以下の感じです <resources> <declare-styleable name = "ContentTextAppearance" parent = "TextAppearance" > <attr name = "android:textSize" /> <attr name = "android:lineHeight" /> <attr name = "lineHeight" /> <attr name = "letterSpacingCompat" /> <attr name = "fakeBold" /> </declare-styleable> </resources> import android.content.Context import androidx. annotation .Px import androidx. annotation .StyleRes object ContentTextAppearance { @Px fun getLineHeight(context: Context, @StyleRes textAppearance: Int , @Px defaultValue: Int ): Int { val typedArray = context.obtainStyledAttributes(textAppearance, R.styleable.ContentTextAppearance) var result = defaultValue if (typedArray.hasValue(R.styleable.ContentTextAppearance_android_lineHeight)) { result = typedArray.getDimensionPixelSize(R.styleable.ContentTextAppearance_android_lineHeight, defaultValue) } if (typedArray.hasValue(R.styleable.ContentTextAppearance_lineHeight)) { result = typedArray.getDimensionPixelSize(R.styleable.ContentTextAppearance_lineHeight, defaultValue) } typedArray.recycle() return result } fun getLetterSpacing(context: Context, @StyleRes textAppearance: Int ): Float { val typedArray = context.obtainStyledAttributes(textAppearance, R.styleable.ContentTextAppearance) val letterSpacing = typedArray.getDimension(R.styleable.ContentTextAppearance_letterSpacingCompat, 0f ) val textSize = typedArray.getDimensionPixelSize(R.styleable.ContentTextAppearance_android_textSize, 0 ) typedArray.recycle() return if (textSize == 0 ) 0f else letterSpacing / textSize } fun getFakeBoldEnabled(context: Context, @StyleRes textAppearance: Int ): Boolean { val typedArray = context.obtainStyledAttributes(textAppearance, R.styleable.ContentTextAppearance) return try { typedArray.getBoolean(R.styleable.ContentTextAppearance_fakeBold, false ) } finally { typedArray.recycle() } } } import android.content.Context import android.graphics.Paint import android.util.AttributeSet import androidx.core.widget.TextViewCompat import androidx.emoji.widget.EmojiAppCompatTextView open class ContentTextView : EmojiAppCompatTextView { constructor (context: Context?) : super (context) { initialize( null , android.R.attr.textViewStyle) } constructor (context: Context?, attrs: AttributeSet?) : super (context, attrs) { initialize(attrs, android.R.attr.textViewStyle) } constructor (context: Context?, attrs: AttributeSet?, defStyleAttr: Int ) : super (context, attrs, defStyleAttr) { initialize(attrs, defStyleAttr) } private fun initialize(attrs: AttributeSet?, defStyleAttr: Int ) { initializeLineHeight(attrs, defStyleAttr) initializeLetterSpacing(attrs, defStyleAttr) initializeFakeBold(attrs, defStyleAttr) } private fun initializeLineHeight(attrs: AttributeSet?, defStyleAttr: Int ) { val textAppearanceResourceId = findTextAppearanceResourceId(attrs, defStyleAttr) val textAppearanceLineHeight = if ( 0 <= textAppearanceResourceId) ContentTextAppearance.getLineHeight(context, textAppearanceResourceId, - 1 ) else - 1 if ( 0 <= textAppearanceLineHeight) { TextViewCompat.setLineHeight( this , textAppearanceLineHeight) } } private fun initializeLetterSpacing(attrs: AttributeSet?, defStyleAttr: Int ) { if (textSize == 0f ) { return } val textAppearanceResourceId = findTextAppearanceResourceId(attrs, defStyleAttr) val textAppearanceLetterSpacing = if ( 0 <= textAppearanceResourceId) ContentTextAppearance.getLetterSpacing(context, textAppearanceResourceId) else - 1f if ( 0 <= textAppearanceLetterSpacing) { letterSpacing = textAppearanceLetterSpacing } } private fun initializeFakeBold(attrs: AttributeSet?, defStyleAttr: Int ) { val textAppearanceResourceId = findTextAppearanceResourceId(attrs, defStyleAttr) val fakeBoldEnabled = ContentTextAppearance.getFakeBoldEnabled(context, textAppearanceResourceId) paintFlags = if (fakeBoldEnabled) { paintFlags or Paint.FAKE_BOLD_TEXT_FLAG } else { paintFlags and Paint.FAKE_BOLD_TEXT_FLAG.inv() } } @Deprecated ( "" ) override fun setTextAppearance(context: Context, resId: Int ) { @Suppress ( "DEPRECATION" ) super .setTextAppearance(context, resId) val textAppearanceLineHeight = if ( 0 <= resId) ContentTextAppearance.getLineHeight(context, resId, - 1 ) else - 1 if ( 0 <= textAppearanceLineHeight) { TextViewCompat.setLineHeight( this , textAppearanceLineHeight) } val textAppearanceLetterSpacing = if ( 0 <= resId) ContentTextAppearance.getLetterSpacing(context, resId) else - 1f if ( 0 <= textAppearanceLetterSpacing) { letterSpacing = textAppearanceLetterSpacing } val fakeBoldEnabled = ContentTextAppearance.getFakeBoldEnabled(context, resId) paintFlags = if (fakeBoldEnabled) { paintFlags or Paint.FAKE_BOLD_TEXT_FLAG } else { paintFlags and Paint.FAKE_BOLD_TEXT_FLAG.inv() } } private fun findTextAppearanceResourceId(attrs: AttributeSet?, defStyleAttr: Int ): Int { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ContentTextView, defStyleAttr, 0 ) val result = typedArray.getResourceId(R.styleable.ContentTextView_android_textAppearance, - 1 ) typedArray.recycle() return result } } ContentTextViewを使うことでTextAppearanceで lineHeight や fakeBold の指定を行えたり、 letterSpacing をsp単位で指定できたりします。これをすべての画面で使いたかったということですね ちなみにですが、ContentTextViewの由来は名前が思いつかなかったからというのが大きいですが、ユーザーコンテンツの部分で特にTypographyを頑張りたかったからユーザーコンテンツのコンテンツから取ってContentTextViewです。基底クラスであればあるほど名付けにくい現象があるのでMyTextViewとかKurashiruTextViewとかでも良かったかもしれませんね(笑) 終わりに 今回クラシルAndroidではAppCompat系のViewの置き換えとして独自のViewを使いたかったのでAppCopatViewInflaterを使用しました。しかし、AppCompat系以外のものはこの方法ではできません。その場合は頑張ってLayoutInflater.Factoryを作りましょう *1 : なければブラウザバックしてどうぞ *2 : MaterialTextViewを使うとTextAppearanceでlineHeightを設定できます *3 : https://developer.android.com/guide/topics/ui/look-and-feel/themes?hl=ja#textappearance
アバター
こんにちは、delyのリテールカンパニーで新規サービスのAndroidアプリ開発を担当しているnozakingです。 今回もdelyのAndroidアプリエンジニアにインタビューした内容をお届けしたいと思います。 今回は第3弾として、TRILLカンパニーのkenzoさんにインタビューしました! 第1弾のインタビュー(parayaさん)は こちら↗︎ 第2弾のインタビュー(Jさん)は こちら↗︎ インタビュー経緯 第3弾の内容の前に、インタビューに至った経緯を説明させてください。 私はAndroidアプリエンジニアの採用活動に携わらせていただいているのですが、面談や面接をしていくなかで、エンジニアとしてこの先どう歩んでいくかを決めかねている方の声を聞くことがよくありました。 そんなモヤモヤを晴らすような、いい話ができたらいいなと私は思いました。 そこで、せっかくdelyには複数のAndroidアプリエンジニアがいるので、インタビュー企画を始動しました🦸‍♂️!! 世の中のエンジニアの皆さんに、少しで もAndroidアプリ開発の魅力 (と、ついでにdelyで働くことの魅力)をお伝えできれば幸いです。 delyに在籍するAndroidアプリエンジニア全員にインタビューしていくつもりで、月一を目標に発信していきます🚀 いざ、インタビュー 冒頭でもお伝えしましたが、第3弾は、TRILLカンパニーでTRILLのAndroidアプリ開発を担当する、 kenzo さんにインタビューしました! 第3弾:kenzoさん k :インタビュイーの kenzoさん の発言 n :インタビュワーの nozaking の発言 delyに入社したのはいつですか? k 2018年1月1日です。 いまはどんなお仕事をしていますか? k 主にTRILLのAndroidアプリに関わることすべてです。 開発はもちろんですが、Androidアプリ開発チームのメンバーのマネージメントやサポートもしています。 あと、採用業務(書類選考や面接など)もやっていますね。 n 今この機能を実装している!みたいなものはありますか? k TRILLの検索機能のリニューアルです! 以前は機能実装をガッツリやっていたのですが、最近は割合を減らして、チームのマネージメントとサポートの割合を多くしています。 現在Androidアプリ開発チームは(自分を含めて)3人のメンバーがいるのですが、メンバーが多ければその分進捗も人数倍になるわけじゃないですよね。今はチームで最大の進捗感を出せることを目指して取り組んでいます。 Android版 TRILLはこちら https://play.google.com/store/apps/details?id=jp.trilltrill.trill play.google.com どうしてAndroidアプリエンジニアになったのですか? k 新卒で入社した会社ではアプリは作っていなかったけれど、Javaを利用した開発の経験がありました。 次の転職先はスタートアップで、何をやるかと考えた時、Javaを書けるならAndroidアプリをやろうとなりました。 その時はAndroidアプリだけではなかったのですが、他にAndroidアプリを開発できるメンバーがいなかったので、僕はそこを担当する割合が多かったです。ちなみに当時はバックエンドも担当していました。 n 転職先のスタートアップで「Androidアプリ開発やるぞ!」となった時、そこで初めてAndroidアプリ開発をしたのですか? k そうですね、そこではWebサービスとアプリを開発していたのですが、そこが初めてでした。 n 結構前の話ですよね…… k 10年ほど前の話ですね。 n その頃って、今ほどAndroidアプリ開発のドキュメントって親切じゃなかったですよねw k たしかにw ただ当時はとにかく作る!という感じで、Androidアプリを開発する力をつけるには社外の活動に参加していくなどする必要があるような環境でした。当時は頑張っていたつもりだけど、今考えるともっとやりようがあったよな……と思っています。 n なるほど……そしてその次にdelyにジョインして、めきめきパワーアップしていくんですね! どうしてdelyでAndroidアプリを開発することにしたのですか? k delyを知ったきっかけはエージェントに紹介されたことでした。当時はdelyのことはまったく知りませんでした。 当時、大竹さん(現在はクラシルマート事業を統括するdely執行役員/コマースカンパニーGM)と話して、 「この人と一緒に仕事がしたい!」 と感じました。 n おお、人に惹かれたのですね。 k そうですね、完全に大竹さんに惹かれましたね。 僕と同じ時期に入社した人は同じような理由の人が多いんじゃないかなと思いますw 大竹さんが今開発しているサービスへの想いをひたすら語っているのが印象に残っています。 そしてdelyで働くことに決めました。 当時一番経歴やスキルがあったのがAndroidアプリ開発だったので、Androidアプリエンジニアとして入社することになりました。 ※大竹さんについては是非下記をチェックしてみてください👀 mobile.twitter.com note.com Androidアプリ開発の魅力はどんなところだと思いますか? k やっぱりユーザーが直接触るっていうところが良いですよね。 あとWeb上のサービスと違って、触ってもらうハードルが高いと思うんですが、一度使ってもらって、そのとき 良いと感じてもらえたら使い続けてもらえるもの だと思うんです。なので、WebよりもUIが良くないといけないと思いますし、ユーザーにとっての使いやすさみたいなのを追い求めていけるところが魅力だと思います。もちろんWebにおいても必要な部分ですが、アプリだとより必要な部分だと思います。 特にdelyでのAndroidアプリ開発にはどんな魅力があると思いますか? k 入社してから現在まで、配属の変更を何度か経験しているのですが、それぞれ異なる魅力がありました。 k クラシルで開発していた時は一緒に働く人がいる、というのが良かったと思います。前職では、みんなAndroidアプリは作れるけれど、専門的にやっている感じではなかったので、クラシルに入社してそこにすごく違いを感じました。 入社した当時は僕と梅森さん(現在もクラシルのAndroidアプリ開発を担当している人)の2人でしたが、梅森さんがすごくできる人だなっていう印象を感じました。僕がAndroidアプリエンジニアとして一番成長したのは、delyに入社してからだなって思うので、 優秀な人と一緒に働ける のは魅力のひとつだなと思います。 当時は2人で、僕と梅森さんにレベル差があったので、梅森さんが言っていることをすんなり受け入れることが多かったのですが、その後メンバーが増えることでいろんな考え方がぶつかりあい議論できるようになりました。 ※梅森さんの考え方を垣間見るれるかもしれない、最近書いた記事もぜひご覧ください👀 tech.dely.jp k 人が増えてくると、組織の体制も変わっていきました。言われたものをただ作るのではなく、 エンジニアもデザイナーもみんなで一緒に考えて作る 、という開発の仕方になっていて、それがとても良いなと感じていました。 その後、坪田さん(delyのCXO)が入社されたというのも転機になりました。 坪田さんって作ったプロダクトに対する目が厳しいんですよね。大変ではあったのですが、それを頑張ってやることで自分の能力的にも上げることができた気がしますし、より良いものが作れたというのはすごく良い環境になったなと思いました。 作ってるものに対して「いや〜、これで許してよ〜」って思うことってあると思うんですけど、坪田さんはちゃんとした基準を持っているので「これはダメ」というのがはっきりするんですよね。それが良かったと思います。 ※坪田さんについては是非下記をチェックしてみてください👀 Tweets by tsubotax twitter.com blog.tsubotax.com k その後TRILLに異動しましたが、当時はAndroidアプリエンジニアは僕一人だったし、何していいか分からない状態でした。それまではクラシルでチームで開発していて、いろいろ分担している状態だったのですが、状況が変わり、全部自分が見る必要になりました。自分が見てなかった部分もやる必要が出てきたので知識も増えたな、と思います。 しかも、TRILLカンパニーはグローバル化が進み、 Androidチームの会話は全て英語になりました。 ……という感じで、フェーズがどんどん変わっていって、 自分のいろんな部分の成長にも繋がるし、シンプルにいろんなシーンが見れて楽しい 、みたいな魅力がありますね。 n 組織としても成長過程にあるから、自分もいろんなポジションにいれるチャンスがある、みたいなのありますよね。 k そうですね。やることが変わるので、その時々は最初しんどいんですが、それが楽しかったり、自分が今まで使ったことない部分を使わなきゃいけない、みたいな感じでレベルアップしていってるような感覚がありますね。いろんな力がついているように思います。 delyのAndroidアプリ開発をする中で、やりがいや達成感を感じたエピソードを教えてください。 k さまざまな部署にいたので、これだ、っていうのは難しいですね……。環境が変わった最初は大変なんですけど、その大変さを乗り越えてちゃんと成り立ってきたな、と感じられた時は達成感がありますね。まあ、またすぐに大変な波はやってきますがw あとは……あまり自分で「やりきったな」とは感じてないんですよね。ずっと環境は変わっていきますし、課題は常に出てきますし、それの連続ですね。山を乗り越えても 「やりきった」と感じる間も無く次の課題がやってくるみたいなw n 息つく暇もなくって感じなんですね k そうですね。平らなところにはあんまりいない感じです。でもその方が良くないですか? n わかりますね、平らなところにいるよりも、険しい山登ってる方が楽しいですよね。 例えば最近登った山ってどんなのがあるんですか? k 大きめの山ですと、最近ではないのですが、自分がTRILLに異動した時ですかね。(2021年7月) 当時TRILLのAndroidアプリは既にリリース済みでしたが、担当エンジニアが不在な状況で、そこへ自分が入っていきました。引き継ぎなどはなかったので、既存コードを読み解きながら仕様を把握しつつ、新しい機能を実装する必要がありましたが、それをやり遂げた時は達成感がありましたね。 k といいつつも、当時のソースコードは長い間手を加えられていないような大変な状況で、その上に新しい機能を実装したので、やりきったとは言いつつもまだまだ直す部分があるなーと思っていました。そこは引き続き改善していっているところですね。(やっぱり息つけてないw) この 息つく間もない感じが好きな人にはもってこいな環境 ですね。 その他、世の中に発信したい思いがあればお願いします。 k 結構delyで働く魅力みたいなのは話したなあと思いますし、それが言いたかったことかなあ。 息つく暇もなくやることがたくさんあって、それが良いと思う人には良いと思いますし、環境が変わることが多いのでチャレンジする機会も多いですし、いろんな成長の機会があるよなと思います。 だって、前はTRILLの開発は日本人4人のチームだったのに、1年経って20人弱のチーム、しかも半分は外国籍のメンバーですからねw n 確かに、TRILLはここ最近ものすごく変わった組織ですね。 k 特に今年の2月には一気に人数が増えたので、環境の変わり方はすごいですね。 それを楽しめる方に是非きて欲しいですね。 k TRILL開発の現場にいて思うのは、 変化の多く難しいけどやりがいのある環境 があります。特にTRILLはグローバル化が激しかったりしますし。 今後もグローバルな環境で働くような状況っていうのは増えていくんじゃないかなと思うので、いち早くそれを取り入れている環境だと思いますし、会社としても力を入れているところなので、そこが良いなと思っています。 ↓先日公開したTRILLの開発に関するnoteの記事です。こちらでもグローバル化への挑戦について触れていますのでぜひご覧ください🙌 note.com kenzoさん、ありがとうございました🙌!! さいごに 第3弾はいかがでしたでしょうか? kenzoさんはdelyのメンバーとして長くいる方なので、delyの過去から現在までの貴重な話も聞けて、nozaking(kenzoさんより2年半後に入社)としてはとても興味深かったです。 今回はdelyで働くことの魅力についてたっぷり語っていただいたので、たくさんの方に伝わると嬉しいです😊 前回の第2弾 Jさんへのインタビュー記事も公開しておりますので、よかったらご覧ください📄 Jさんは今年入社したので、また違った魅力を語ってくれています🎤 tech.dely.jp 引き続き、第4弾も実施したいと思っておりますのでよろしくお願いします🙇‍♂️ もし、delyのメンバーとお話してみたい!と思ってくださった方は、カジュアルにお話しする場も設けられますので、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
こんにちは、delyのリテールカンパニーで新規サービスのAndroidアプリ開発を担当しているnozakingです。 今回もdelyのAndroidアプリエンジニアにインタビューした内容をお届けしたいと思います。 今回は第2弾として、クラシルカンパニーのJさんにインタビューしました! (第1弾のインタビューは こちら↗︎ ) インタビュー経緯 第2弾に入る前に、インタビューに至った経緯を説明させてください。 私はAndroidアプリエンジニアの採用活動に携わらせていただいているのですが、面談や面接をしていくなかで、エンジニアとしてこの先どう歩んでいくかを決めかねている方の声を聞くことがよくありました。 そんなモヤモヤを晴らすような、いい話ができたらいいなと私は思いました。 そこで、せっかくdelyには複数のAndroidアプリエンジニアがいるので、インタビュー企画を始動しました🦸‍♂️!! 世の中のエンジニアの皆さんに、少しで もAndroidアプリ開発の魅力 (と、ついでにdelyで働くことの魅力)をお伝えできれば幸いです。 delyに在籍するAndroidアプリエンジニア全員にインタビューしていくつもりで、月一を目標に発信していきます🚀 いざ、インタビュー 冒頭でもお伝えしましたが、第2弾は、クラシルカンパニーでクラシルのAndroidアプリ開発を担当する、 J さんにインタビューしました! 第二弾:Jさん J :インタビュイーの Jさん の発言 n :インタビュワーの nozaking の発言 delyに入社したのはいつですか? J 2022年2月15日です。 いまはどんなお仕事をしていますか? J クラシルのAndroidエンジニアとして開発に取り組んでいて、いまは主に広告周りの改善や、クライアントさん(※)向けに営業が提案する用の機能を担当しています。 ※ クラシルは、食品メーカーやブランド等とタイアップやコラボレーションをさせていただく事があります 👩‍🍳 n セールススクワッドに所属しているんですよね? J そうですね!他のスクワッドではフィードを改善するスクワッドや、投稿機能を改善するスクワッドがありますね。 (スクワッド体制については下記の記事に詳しく書いているので、ぜひご覧ください!) blog.tsubotax.com どうしてAndroidアプリエンジニアになったのですか? J 最初はエンジニアとしてスタートしたわけではなくて、例えばベンダーさんの管理など、いわゆるSE的なことをやっていました。 このまま手を動かさなくてこの先大丈夫かな?と不安に思い、プライベートでRuby on RailsやAndroidを勉強しました。 その勉強の活動(GitHubにて成果物を公開して、求人サイトのプロフィールに載せていた)を見てくださった企業が、「うちでAndroidアプリ開発をやりませんか?」と声をかけてくれたことがきっかけで、Androidアプリ開発の仕事をはじめました。 n 最初の勉強のときにAndroidもやってみようと思ったのは何か理由があるのですか? J シンプルに自分の興味があるからでした。最初はWebのアプリを作ってみていて、データベースをいじったりHTMLを書くなど一通りやってみたのですが、「そういえばAndroidアプリってどう作るんだろう?」と思ったんです。 どうしてdelyでAndroidアプリを開発することにしたのですか? J 前職では受託がメインの業務だったのですが、一部自社アプリもやっていました。そのプロジェクトにはPdM担当のような方がいたものの、自分が提案してもあまり受け入れられず、その人の要求に応えるというスタイルだったんですよね。 n 自社開発でそのスタイルは少しモヤっとしてしまうかもしれませんね。 J そうですね。不満があったわけでは無いのですが、そんな中でdelyからのスカウトメッセージをきっかけにカジュアル面談を受ました。毎日アプリを触って検証しているなど、チームの取り組みを聞いていいなぁと思いましたし、自分もクラシルを使っていたので興味を持ちました。 Androidアプリ開発の魅力はどんなところだと思いますか? J いろんな技術が新しく出てくるのが魅力だと思います。例えば最近はJetpack Composeが話題だと思いますし、非同期処理がRxからCoroutineに変わるタイミングがあったり、そうやってて絶えず新しいネタが生まれてきて飽きないというか、常に刺激を与えられますね。そしてそれを自分で触りながら学んでいけるのが楽しいです! あと、Webのフレームワークほどルールがガチガチではないと思っていて、一応Googleからベストプラクティス的なものがでているけれど、企業やアプリごとに考えた適切な設計があるよねという感じで、ある程度自由に自分の設計思想を反映できるっていうのがいいなと思います。 n たしかに、ガイドラインにも、「必要に応じて各自の条件に適応させる必要があります。」みたいな注意書きが書いてありますもんねw developer.android.com ↑(参考)アプリ アーキテクチャ ガイド 「アプリの推奨アーキテクチャ」の部分 特にdelyでのAndroidアプリ開発にはどんな魅力があると思いますか? J 技術面とチーム面で魅力があるかなと思ってますね。 J 技術面では、Androidアプリを開発するだけではなく、設計に沿ってどうやってコードを記述していくかっていうところを僕に限らずみんなが大事にしているように思います。 そんな中で開発をしているとプログラミングの基礎的な能力が磨かれていくように感じるんですよね。例えば思想として副作用が分かっていても、それを実際にコードにうまく落とし込むというのが実務ではなかなか難しいと思っているのですが、今それができていて楽しいなと思いますね。 J チームの面では、全員でアプリを触ってチェックするという取り組みが良いなと思っています。実装者が気づかない変な表示などに気づけるし、実装してみたけどあんまり思っていた動きと違うなあみたいなところもみんなで話し合えるのが楽しいし、良いコミュニケーションの場になっていると思います。 n わかりますねー。クラシルのAndroidチームは複数人で開発しているので、それならではの良さがありますよね。 J そうですね。クラシルのAndroidチームでの開発は、 アプリを作る力というより、コードを書く力が磨かれてる っていう感じがしますね。前職とかだと結構密結合なコードを書いてしまっていたりしたんですが、今思うとここはこうした方がよかったなっていうことを気づけるようになったので、普段の業務を通して、コードを書く力とか設計をする力が磨かれていくっていうのが、クラシルでAndroid開発をしている魅力かなと思います。 delyのAndroidアプリ開発をする中で、やりがいや達成感を感じたエピソードを教えてください。 J 2つあって、ひとつは、2月に入社してから3月末まではSquadに仮配属のような形で、そのとき余っていたタスクをやるスタイルだったんですが、4月から正式にSquadに配属になり、早速プロフィール改修というでかい案件の担当になったんです。 そこで、いろいろアドバイスをもらいつつ、自分の力だけでクラシルの設計に則りつつ開発できたというのが、やっとクラシル開発になれてきたなって感じたエピソードですね。 J もうひとつはエピソードって感じではないのですが、いま広告周りのことをやっていて、コードだけではなくてGoogleアドマネージャの管理画面でちゃんと設定しないとうまく広告が出なかったりとかがあるのですが、スクワッドのメンバーと毎日コミュニケーションとりつつ、「ここの広告のサイズがおかしいからこうしないとダメなんだ」とか話したりしながら改善していって、それがimp数の向上などにつながるというのが楽しく感じますね。 n 何かをするだけじゃなくて、その効果を実感できるっていうのはいいっすね。 その他、世の中に発信したい思いがあればお願いします。 J エンジニアになると、サーバー側をまずやってみるという方が多いのかなと思いますが、フロント特有の状態管理などは触れてみると面白い部分があるんじゃないかなと思うので、ぜひAndroidアプリ開発を通してやってみてはいかがでしょうか! n そうですね!ぜひ!! Jさん、ありがとうございました🕺!! さいごに 第2弾はいかがでしたでしょうか? Androidアプリエンジニアとして働くこと、delyで働くことについてイメージできたでしょうか? 前回の第1弾 parayaさんへのインタビュー記事も公開しておりますので、よかったらご覧ください📄 tech.dely.jp 第3弾も実施したいと思っておりますので、引き続きよろしくお願いします🙇‍♂️ もし、お話してみたい!と思ってくださった方は、カジュアルにお話しする場も設けられますので、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
こんにちは、delyのリテールカンパニーで新規サービスのAndroidアプリ開発を担当しているnozakingです。 今回はdelyのAndroidアプリエンジニアにインタビューした内容をお届けしたいと思います。 インタビュー経緯 私はAndroidアプリエンジニアの採用活動に携わらせていただいているのですが、面談や面接をしていくなかで、エンジニアとしてこの先どう歩んでいくかを決めかねている方の声を聞くことがよくありました。 そんなモヤモヤを晴らすような、いい話ができたらいいなと私は思いました。 そこで、せっかくdelyには複数のAndroidアプリエンジニアがいるので、インタビュー企画を始動しました🦸‍♂️!! 世の中のエンジニアの皆さんに、少しで もAndroidアプリ開発の魅力 (と、ついでにdelyで働くことの魅力)をお伝えできれば幸いです。 delyに在籍するAndroidアプリエンジニア全員にインタビューしていくつもりで、月一を目標に発信していきます🚀 いざ、インタビュー 第1弾となる今回は、クラシルカンパニーでクラシルのAndroidアプリ開発を担当する、parayaさんにインタビューしました! 第1弾:parayaさん p :インタビュイーの parayaさん の発言 n :インタビュワーの nozaking の発言 delyに入社したのはいつですか? p 2021年7月15日です。 いまはどんなお仕事をしていますか? p クラシルでAndroidエンジニアとして開発業務をしています。 n 具体的にはどんなことをしていますか? p クラシルショートの投稿機能まわりの拡充に注力しています。 どうしてAndroidアプリエンジニアになったのですか? p まずAndroidとの出会いなんですが、当時在籍していた会社の技術共有会で先輩からAndroidの話を聞いたのがきっかけでした。当時はAndroidが発表されたばかりの時で、先輩からAndroidの理念を聞いてる内に良さそうだなと思ったのがはじまりでした。 p 例えば当時のOSは有料だったりすることが多いと思うんですが…… n むむ、OSが有料? p たとえばWindowsとかもそうだし、当時はモバイルOSも有料だったり、各社が独自で作ったりしていて、BREWとか、ドコモ系のOSとかDoJaとか……。各社それぞれOSやアプリ開発環境やプログラミング言語が異なる状況で、アプリ開発者も端末メーカーもコストや時間が掛かっていて四苦八苦していたんですよね。 そんな時Androidが発表されて、オープンソースのOSとして公開していて、それを使えば端末メーカーはOSに使うコストを減らせるし、開発者はJava(当時)でリッチなアプリを作れますよって言うところに惹かれて、そこが好きだな〜と思ったし普及するだろうなと思いました。そこから自分で勉強して、Androidアプリエンジニアになったという流れでした。 n 勉強したのは自らだったのですか? p そうですね。当時は画期的というか衝撃的で、本当に浸透したじゃないですか。 当時僕はBREWを扱っていたのですが、今ではガラケーと共に全部無くなって、iPhoneを発売していない携帯会社はみんなAndroidの携帯を出すっていう流れでしたよね。その広がりがすごかったよなと思っています。 そんな流れで自ら勉強しようと思ってやってみました。 どうしてdelyでAndroidアプリを開発することにしたのですか? p ひとつは僕自身が興味のある事業だったところですね。自分自身も料理をしているので。 以前は事業というより、どちらかというと働き方重視で転職先を選んでいました。 前職まではUnityやiOSアプリ開発もしていたのですが、やはりAndroidアプリ開発が一番好きなので、そこに集中したいと思いました。 n 好きなポイントってどのへんでしょうか? p AndroidStudioが使いやすいところですかね。Kotlinが好きな言語というのもあります。 あと、もともとAndroidを調べて勉強した背景があるので好きですね。 Androidアプリ開発の魅力はどんなところだと思いますか? p 普段使っている端末で自分の作ったものが触れるっていうところは魅力的かなとおもいます。 サーバーサイドと違ってUIをしっかり作り込んでいきたいとか、そういう所に注力したい思いがありましたね。 クライアントでがりがりアニメーションとかやるのも面白いですし、そういうところかな、と思いますね。 p あとはGoogle主導で開発サポートがあるところは魅力だなと思いますね。 過去に経験した中で言うと、iOSは今はどうかわからないですが、Unityだと公式からこういうふうに作るといいです、みたいなものがあまりないんですよね。命名規則とか、アーキテクチャとか、書き方の統一みたいなものもないので。 なのでAndroidやってると開発のサポートの良さについては感じますね。 developer.android.com ↑(参考)アプリ デベロッパー向けのドキュメント 特にdelyでのAndroidアプリ開発にはどんな魅力があると思いますか? p delyだとAndroidエンジニアの人数がまだまだ少ないので、Androidエンジニアとしてやろうと思ったことをやりやすいのは魅力かと思います。 新卒や中途入社で上に人が居るからできないとか、役割分担してるから本来自分がやりたいことができない、というのはないですね。 やりたいと思ったことがやれるというのが魅力だと思います 。 たとえばCIをいじりたいとか、採用活動に関わりたい、など。前職だと採用活動にも携わりたかったのですがあまりできなかったんですよね。 自分の進みたいキャリアに向けて行動しやすいっていうところも魅力かなと思います。 (Androidの魅力ではないんですがw) delyのAndroidアプリ開発をする中で、やりがいや達成感を感じたエピソードを教えてください。 p 一番はコンテストページを作ったところですね。 parayaさんが担当したクラシルショートのコンテスト機能 p 始めた当初は簡単な画面でそこまで思い入れもなかったんですが、すごい使われてる機能なんですよね。コンテスト企画をしているチームが社内にあって、その人たちが考えた企画を、僕たちが作った機能を通じて運用してもらっていて、クリエーターさんたちもそこを使って見てくれている。 運営者も、ユーザーさんもたくさん使ってくれていて、僕が作った中で一番使ってくれているなと思う機能です。 ドキュメントも僕が精査して、使ってください!って案内したら、すごい使われる機能になって良かったですね。 その他、世の中に発信したい思いがあればお願いします。 p 学生さんに向けてありますね、エンジニアになるならAndroidアプリ開発を是非選んでください!w 弊社じゃなくてもいいんで、業界全体でAndroidアプリエンジニアを求めていますのでw 絶対活躍の場があります! もしiOSとAndroidでなやんでるならぜひAndroidを! parayaさん、ありがとうございました🙌!! さいごに いかがでしたでしょうか? Androidアプリエンジニアとして働くこと、delyで働くことについてイメージできたでしょうか? 第2弾も来月実施したいと思っておりますので、引き続きよろしくお願いします🙇‍♂️ もし、もっとじっくり話したい!と思ってくださった方は、カジュアルにお話しする場も設けられますので、どうぞお気軽にお声かけください 🙌 下記の採用リンクから応募いただけますのでお願いします! dely.jp
アバター
6月末になって梅雨も本格的になってきましたね。クラシルでAndroid開発をしているエンジニアのみうらです。 今回は社会人の皆さんにとって馴染み深い、「目標設定と振り返り」について書きたいと思っています。 目標設定と振り返りについて delyでは1年をQ(四半期)毎に区切っていて、部などの組織もそうですが個人の目標設定や振り返りなどもQ毎に行われています。 1Qが4~6月なので、6月末の今頃はそろそろ1Qの振り返りと来Qの目標設定を行う時期なんですね。 目標設定は悩みがち ところで皆さんは個人目標の設定は得意でしょうか?正直に言って私は苦手です笑 目標は会社としては売上に紐付けられると一番わかりやすくていいと思うのですが、開発業務の全てを売上に紐づけるのって難しいんですよね。 開発業務の場合は新規開発なら明確な目標と貢献度が見えやすいですが、運用開発業務だと直接的に売上に紐づいたタスクではない場合も多いと思います。 他にも部門目標に沿った目標を立てるとしても自分の業務でどう部門や会社に貢献すればいいのかわからない…今後のキャリアを踏まえてといっても自分のキャリアどうしようか決まってない… こんな感じで悩む方もいらっしゃるのではないでしょうか。私はよく悩みます🤔 目標設定は悩むとは思いますが、結局は上長と認識や期待値すり合わせができていればOK、であると思います。自分のやりたいことが会社の方針とあっており、その上で評価されることであるということの認識が上長とあっていれば成立すると思います。 delyでは目標設定を1on1で上長orメンターとすりあわせて決定しています。 しかし、1on1には時間制限もあり、上長とのすり合わせが十分ではないということもありました。 そのため、Androidチームでは目標設定面談の前に上長やチームメンバーと認識や期待値をすり合わせるために、振り返りワークショップを行っています。 クラシルAndroidチームでの振り返り 振り返りはKPT+ドラッカー風ワークショップに近い形でやっています。 ワークショップは以下の問いに対して自分の思っていることを書き出して進めています。 今Qで良かったこと&課題に思ったこと チームメンバーは来Q自分にどんな成果を期待しているかor自分は来Qどんなことをやろうとしているか 他メンバーに来Q期待することはなにか ドラッカー風ワークショップはGMOペパボさんのこちらの記事を参考にしています。 tech.pepabo.com   なぜドラッカー風ワークショップを取り入れているかというと、KPTもいいのですが、KPTはProblemとTryに集中しがちなのと、ProblemとTryをまとめてもチームの課題と目標にはなるが、個人目標に落とし込みづらいことがあるのを課題に思ってました。 チームの課題と個人目標は一致しないこともありますし、課題だけが目標ではないですからね。 その点、ドラッカー風ワークショップでは個人のTryに集中できるのと、そのTryにチームメンバーからのフィードバックや、どう期待されているのかを具体的に言ってもらえる点が気に入ってます。 チームメンバーからのフィードバックは目標設定としても妥当なことも多く、個人目標設定の良い材料にもなります。 使用ツール ワークショップのツールは以下を使っていました。ホワイトボードのように使えるものであれば何でもいいと思います。 出社時 ポストイット リモート Google Jamboard FigJam まとめ いかがだったでしょうか? 目標設定は多くのビジネス書でも語られているように物事を進める上でとても大事なものです。 より良い目標設定を行うため、Androidチームでは個人目標設定のサポートとしてチームでの振り返りワークショップを行っています。  目標設定でお悩みの方はチーム内で導入を検討してみるのはどうでしょうか? 最後に クラシルではAndroidエンジニアを絶賛大募集しています🙌   テックリード的なシニアエンジニアはもちろん、料理好きなスキルアップしたい若手エンジニアも大歓迎です🤝   カジュアル面談も行っていますし、どういった仕事をしているのかエンジニアと話してみたいという方でもOKです。   下記の採用リンクから応募いただけますのでよければ気軽にお願いします😄  dely.jp
アバター
そろそろ梅雨も近づき雨の多い季節になってきましたね。みなさんいかがお過ごしでしょうか? クラシルでAndroidエンジニアをやっているみうらです。 今回はエンジニアには馴染みの深い勉強会について、社内での簡単な開催方法について書いてみようと思います。 いきなりですが皆さんの会社では社内で何かしら勉強会をやっているでしょうか? 読書会やLT会、もくもく会…やっている所もあればやっていないところもあるとは思うのですが、やりたいけどやってないという話を聞いたこともあります。 外部の勉強会に参加したことはあるが勉強会を開催したことはない…といった方もいらっしゃると思います。 そこで今回は勉強会の簡単な開催方法を書いてみようと思います。 勉強会こんな感じでやってます 実はクラシルAndroidチームでは私が入社した時点では勉強会をやっていませんでした。 やってはいたものの長く続かなかったようです。 しかし、多くの企業では勉強会をしている通り、勉強会をすることにはメリットが色々あります。 勉強会のメリットと手法をいくつか紹介します。 勉強会のメリット 勉強会のメリットは色々ありますが、概ね技術的議論をスムーズにすることと、実装とコードレビューの効率化だと思っています。(個人の意見です) 共通認識の構築による議論のスムーズ化 お互いのバックボーンが違う中での議論は、議論ではなくバックボーンの説明になりがち こういう設計にしたい→個人の意見ではなく設計手法の説明になったりする バックボーンを知った上でどうするか?を話したい 共通認識の構築による実装/レビューの効率化 お互いに注視すべきポイントが同じであれば似た実装or納得できる実装になってくる レビューと実装速度の高速化が見込める 単純にスキルアップを目指せる 新入/中途社員のフォローなどにも繋げられそう 勉強会の手法 勉強会の手法ですが色々あります。 その中では読書会の宿題方式は時間とコストのバランスがいいのでオススメです。 読書会 読み合わせ式 その場で読み合わせていき、議論をする時間を設ける 勉強会で読む時間も必要なので時間がかかる 宿題式(オススメ) 事前に読んでおいて当日議論だけする 勉強会にあまり時間を割きたくない場合や、勉強会のペースを早めたい場合おすすめ 宿題忘れるorやらない人が出てくる ただ忘れた人が少ないなら勉強会は問題なく進む事が多い 担当持ち回り式 担当が読んできてその章を他の人に紹介/説明する 担当になった場合説明資料の作成など大変 LT会 発表苦手な人も居るので聞き専の人が出てくる 情報発信が一方通行になりがちなので議論には向いてない。個人の学習にはなる。 外部の勉強会で登壇したい人向けの練習になる もくもく会 もくもくと自分のやりたいことを進める 個人の学習にはなる。 自由にできるので実装ベースの勉強をしやすい。 発表することで他の人からのフィードバックを受けられる。 次に読書会形式の開催方法についてまとめてみます。 読書会形式の勉強会の開催の仕方 基本的には 何か題材を読む それについて分からなかった所について質問してみる、感想を言う。 これだけで勉強会は成立します。 クラシルAndroidチームでは直近 アプリアーキテクチャガイド について読書勉強会を開催していました。 進行は以下のように大きく章ごとに分けて進めていました。 Architecture Overview UI Layer Data Layer Domain Layer Handling UI events そして各章を宿題として毎週開催しました。 実際の勉強会では宿題として読み込んでいる章の節毎に疑問や感想があるかを聞いて、それについて議論を進めていきました。 例えばドキュメントにはUseCaseは1UseCaseにつき1振る舞いのみのようだが、クラシルAndroidではどうするか? ビジネス指向のオペレーションはクラシルではどこにあたるのか? などの問いかけをきっかけに議論につなげていきました。 1章1回では終わらないので、大体1章3〜4回に分けて開催していたと思います。 簡単に説明しましたが、勉強会はこんな感じで題材を読んで感想や疑問をぶつけ合うだけでも成立します。 まとめ いかがだったでしょうか? 外部の勉強会に参加したことはあるが勉強会を開催したことはないという方もいらっしゃると思います。 もし社内勉強会やりたいなと思っている方がいらっしゃったら、宿題形式の読書会から試してみるのはいかがでしょうか? 最後に クラシルでは勉強大好きなAndroidエンジニアを絶賛大募集しています🙌 テックリード的なシニアなエンジニアはもちろん、料理好きなスキルアップしたい若手エンジニアも大歓迎です🤝 カジュアル面談も行っていますし、どういった仕事をしているのかエンジニアと話してみたいという方でもOKです。 下記の採用リンクから応募いただけますのでよければ気軽にお願いします😄 dely.jp
アバター
はじめに android-developers.googleblog.com 12/14に新しいアプリアーキテクチャガイドがAndroid公式からアナウンスされました。読まれた方もいらっしゃると思いますが、非常によくまとまったアーキテクチャガイドであり、新しくアプリを作る際も、既存のアプリのアーキテクチャを整理する際にも役に立ちそうな文章です。 クラシルのAndroidチームは去年の2月にAndroidアプリをリアーキテクチャしたのですが、そのアーキテクチャがアプリアーキテクチャガイドと似通った個所が多く、クラシルのアプリアーキテクチャを説明するのにもちょうど良さそうな文章だと思いました。 ですので、今回は新しいアプリアーキテクチャガイドとほとんど同じ構成で、クラシルのアプリアーキテクチャについて解説してみようと思います。なお、元の文章は一般的なアーキテクチャについてのTipsや考え方について説明している個所もあり、そういった個所については記述を割愛しますので、是非元の文章を読んでみてください。 この記事のタイトルは、アプリアーキテクチャガイドの Guide to app architecture から拝借して Guide to "kurashiru android" app architecture とさせていただいてます。 この記事は tech.dely.jp の続きの記事(vol.2)です。(vol.1から5カ月くらい経ってしまった…) はじめに UI layer 概要 基本的なケーススタディ UI layerのアーキテクチャー UI Stateを定義する イミュータビリティ このガイドの命名規則 単方向データフローで状態を管理する State holders ロジックの種類 何故単方向データフローを使うのか? UI StateをViewに適用する その他の考慮事項についてのクラシルの方針について 処理中の状態を表示する UIイベント UIイベント決定木 ユーザーイベントをハンドリングする RecyclerView内のユーザーイベント Domainイベントをハンドリングする おわりに UI layer 概要 UIの役割は、アプリケーションのデータを画面に表示することと、ユーザーとのインタラクションの主な窓口となることです。ユーザーの操作(ボタンを押すなど)や外部からの入力(ネットワークのレスポンスなど)によってデータが変更されるたびに、UIはそれらの変更を反映して更新されなければなりません。実質的には、UIはData layerから取得されたアプリケーションの状態を視覚的に表現したものです。 しかし、Data layerから取得するアプリケーションデータは、表示に必要な情報とは異なる形式であることがほとんどです。例えば、UIに必要なのはデータの一部だけかもしれませんし、2つの異なるデータソースをマージしてユーザーに関連する情報を提示する必要があるかもしれません。どのようなロジックを適用するかに関わらず、UIを完全にレンダリングするために必要なすべての情報を渡す必要があります。UIレイヤーは、アプリケーションのデータの変化をUIが表現できる形に変換し、表示するパイプラインです。 基本的なケーススタディ ユーザーが料理を作るためのレシピを取得するためのアプリを考えてみましょう。このアプリはレシピ一覧画面があり、見ることができるレシピが表示され、サインインしているユーザーは、気に入ったレシピをブックマークすることが出来ます。また、レシピの数が多い場合は、カテゴリー別にレシピを閲覧することも出来ます。まとめると、このアプリでは次のようなことが出来ます。 レシピを表示する。 カテゴリー別にレシピを閲覧する。 サインインし、気に入った記事をブックマークする。 プレミアム機能を利用する。 次のセクションでは、この例をケーススタディとして、単方向データフロー (UDF) の原則を紹介し、この原則がUI layerのアーキテクチャという文脈で解決できる問題を説明します。 UI layerのアーキテクチャー クラシルのUI layerは、以下の責務を果たすためにあります。現在はAndroid Viewsを使っていますが、Jetpack Composeに移行してもその責務は変わりません。Data layerの役割が、アプリケーションのデータを保持、管理し、アクセスのインターフェースを提供することから、UI layerは以下のステップを実行する必要があります。 アプリケーションのデータを受け取り、UIがレンダリングしやすい形に変換する。 UIがレンダリングしやすい形のデータをユーザーに表示するためのUI要素に変換する。 構築されたUI要素から入力イベントを受け取り、必要に応じてUIデータに反映させる。 1から3のステップを必要なだけ繰り返す。 このセクションの残りの部分では、いくつかのステップに分けてUI layerをどのように実装するかについて説明します。特に、以下のタスクやコンセプトについて説明します。 UI Stateをどのように定義するか UI Stateを生成・管理する手段としての単方向データフロー (UDF) UDFの原則に従って、observable data typesとしてUI Stateを扱う方法 observable UI Stateを使ってどのようにUIを実装するか その中で最も基本的なことが、UI Stateを定義することです。 UI Stateを定義する 前述のケーススタディを参照してください。一言で言えば、UIはレシピのリストと各レシピのメタデータを表示しています。この、アプリがユーザーに見せる情報が UI State です。 言い換えるなら、UIがユーザーが見るものであるならば、UI Stateはアプリがユーザーに見せる べき ものです。同じコインの裏表のように、UIはUI Stateを視覚的に表現したものです。UI Stateに変更があれば、すぐにUIに反映されます。 ケーススタディを考えてみましょう。Recipeアプリの要件を満たすために、UIを完全にレンダリングするために必要な情報は、次のように定義されたRecipeState data classに定義することが出来ます。 data class RecipeState( val isSignedIn: Boolean = false , val isPremium: Boolean = false , val recipeItems: List <RecipeItem> = listOf(), val userMessages: List <Message> = listOf() ) data class RecipeItem( val title: String , val body: String , val bookmarked: Boolean = false , ... ) イミュータビリティ 上の例で定義されたUI Stateの定義はイミュータブルです。このことの重要な利点は、イミュータブルオブジェクトがある時点でのアプリケーションの状態に関する保証を提供することです。これにより、UIはStateを読み取り、それにしたがってUI要素を更新するという単一の役割に集中することが出来ます。そのため、UI自身がデータの唯一のデータソースである場合を除き、UI StateをUIで直接変更するべきではありません。この原則に反すると、同じ情報に対して複数の情報源(sources of truth)が存在することになり、データの不整合や厄介なバグが発生する原因になります。 例えば、ケーススタディのRecipeItemオブジェクトのbookmarkedフラグがUI Componentで更新されると、データソースとして利用しているData layerのブックマーク状態と競合することになります。Immutable data classは、このようなアンチパターンを防ぐのに非常に有効です。 このガイドの命名規則 クラシルでは、UI Stateクラスは記述される画面に基づいて命名されます。規約は以下の通りです。 (機能) + State です。 例えば、レシピを表示する画面の状態はRecipeStateとなります。 画面の一部をあらわすUI Stateについて(例えばリストのアイテムなど)は、 (機能)+ Argument と名付けられています。 クラシルでは原則、画面の一部を示すStateを管理するView HolderがState自体を更新することはなく、Argumentといった形の方が呼び方として適切だからという理由があります。ただ、この区別は将来的には廃止される可能性があります。 単方向データフローで状態を管理する 以前のセクションで、UI StateはUIのレンダリングに必要な詳細の、イミュータブルなスナップショットであることを説明しました。しかし、アプリケーションデータは動的で、時間の経過とともにStateが変化する可能性があります。これは、ユーザーの操作やその他のイベントによって、アプリケーションを構成している基礎的なデータが変更されてしまうことによって引き起こされます。 こういった処理はクラシルではMVI Modelに格納され、イベントに応じてDomain layerのUseCaseを呼び出す形や、UI Stateを即座に更新する処理を書く形で処理を定義することになります。基本的にUI ModelはUI ElementとDomain layerの仲立ちを行うような処理に徹するべきで、それ以上の責務を負うべきではありません。UI Modelが多くの責務を負ってしまうと、コードが密結合になってしまい、テストのしやすさにも影響してくることになります。UIの負担を減らし、あくまでUI Stateを表示する責任のみを負う形で実装するべきです。 このセクションでは、健全な責務分離を実現するためのアーキテクチャーパターンである、単方向データフロー (UDF) について説明します。 State holders UI Stateの生成を担当し、そのタスクに必要なロジックを含むクラスをState holderと呼びます。State holderには、対応するUI Elementのスコープに応じて様々なサイズがあり、Bottom Navigation Barのような単一のウィジェットから、画面全体やナビゲーション先まで多岐にわたります。 クラシルでは、対応するUI要素のスコープ(画面、ページ、あるいはリストの一要素)に合わせてUI Componentというものを作成するようにしており、それがState holderにあたります。UI Componentは、UI Stateの生成や、UIイベントや外部イベントに応じてUI Stateを更新する責務、UI StateをUI Elementに反映する責務を負っており、UI ElementからUIイベントを発火させる責務をIntent、UIイベントや外部イベントに応じてUI Stateの生成/更新を行う責務をModel、UI StateをUI Elementに反映する責務をViewが負うMVIアーキテクチャーを採用しています。例えば、ケーススタディのRecipeアプリでは、RecipeListComponentクラスをState holderとして使用し、そのセクションで表示される画面のUI Stateを生成しています。 クラシルにおける、UIとUI Componentクラス間の相互作用は、UIイベントの入力とStateの出力を中心に整理すると以下の図のようにあらわすことが出来ます。 Stateが下に、イベントが上に流れていくパターンを単方向データフロー (UDF)と呼びます。このパターンをクラシルアプリアーキテクチャーで説明すると、次のようになります。 UI ComponentはUIが扱うStateを保持し、公開します。UI StateはUI Componentによって変換されたアプリケーションデータです。 UIは、ユーザーイベントをMVI Intentに通知します。 MVI Modelは、MVI Intentが発火したユーザーのActionを処理し、Stateを更新します。 更新されたStateは、MVI Viewを通してUIにフィードバックされてレンダリングされます。 以上のことを、Stateを変化させる全てのイベントが起きるたびに繰り返します。 画面の場合、UI ComponentはDomain Featureと連携してデータを取得し、UIイベントによる状態の変更を取り入れながらUI Stateに変換します。先ほどの事例では、レシピのリストがあり、それぞれにタイトル、サムネイル、ブックマークされているかどうかが表示されています。各レシピアイテムのUIは次のようになっています。 ユーザーがレシピのブックマークを要求することは、状態変化を引き起こす可能性のあるイベントの一例です。UI Stateを提供するComponentは、UI Stateの全てのフィールドに情報を入力し、UIが完全にレンダリングされるために必要なすべてのイベントを処理するロジックを定義する責務があります。 次のセクションでは、状態変化を引き起こすイベントと、それをUDFでどのように処理するかについて詳しく説明します。 ロジックの種類 レシピをお気に入りすることは、アプリに価値を与えることなので、ビジネスロジックの一例です。この点については、Data layerのセクションを参照してください。しかし、ここで一旦定義しておくべき二つのロジックの種類があります。 ビジネスロジック とは 状態の変化 を用いて 何(what) をしたいかということです。既に述べたように、レシピアプリでのレシピのお気に入りがその一例です。ビジネスロジックは通常、Domain layerやData layerに置かれますが、UI layerに置かれることはありません。 UI behaviorロジック または UIロジック とは、 状態の変化 を どのように(how) 画面に表示するかということです。例えば、Androidリソースを使って画面に表示する適切なテキストを取得したり、ユーザーがボタンをクリックすると特定の画面に移動したり、トーストやスナックバーを使ってユーザーのメッセージを画面に表示したりします。 UIロジック、特にContextのようなUI関連の型に依存するロジックは、クラシルの場合は現状Componentに配置するべきです。ComponentはUIのライフサイクルに従うため、Android SDKに依存することが出来ますし、MVIアーキテクチャーによって関心の分離を行うこともできますが、将来的にはUI関連の型に依存するロジックを分離できるように、UI Stateをラップする薄いState Holderクラスを導入する予定があります。 何故単方向データフローを使うのか? UDFは、図4に示すように、State生成のサイクルを設計しています。また、Stateの変化が発生する場所、変換される場所、最終的に消費される場所を分離しています。この分離により、UIはその名の通り、状態の変化をObserveすることで情報を表示し、ユーザーの意図をイベントとしてComponentに伝えることが出来ます。 つまり、UDFでは次のようなことを可能にします。 データの一貫性。UIには Single Source of Truth があります。 テスト可能性。Stateの生成、変換はUIから分離されており、UIとは独立してテスト可能です。 メンテナンス性。Stateの変化は、ユーザーのイベントとデータソースの両方に起因する、明確に定められたパターンに従って引き起こされます。 UI StateをViewに適用する UI Stateを定義し、UI Componentを定義したら、次のステップは生成されたStateを使ってUIを表示することです。Stateの生成を管理するためのUDFを私用しているので、生成されるStateはストリームであると考えることが出来ます。つまり、Stateの複数のバージョンが時間経過によって生成されます。クラシルアーキテクチャーでは、UI ComponentのModelからUI Stateをdispatchすると、MVI View関数に順番に最新のUI Stateが通知されます。UI Stateはdispatchされる度にキャッシュされるので、UI Elementの状態復元時にも前回のUI Stateを使って即座に状態復元を来なうことが出来ます。 class RecipeView(...) { fun view(state: State, updater: ViewUpdater,...) { updater.update(state.title) { layout, title -> layout.titleLabel.text = title } } } クラシルではUI Componentは単一のUI Stateを保持するようになっています。Modelで取得したデータは、UI StateでwrapすることでUI Elementや画面からアクセスできるようになっています。以下のようなコードでModelからUI Stateをdispatchします。 class RecipeModel( val recipeFeature: RecipeFeature ) { private val recipeListUseCase = recipeFeature.recipeListUseCase fun model(dispatcher: StateDispatcher,...) { recipeListUseCase.fetchRecipeItems().subscribe { recipeItems -> dispatcher.dispatchState( copy( recipeItems = recipeItems ) ) } } } 上の例では、RecipeComponentModelクラスが特定のカテゴリのレシピを取得しようと試み、その結果(成功か失敗か)をUIの状態に反映させ、UIが適切に反応できるようにしています。 その他の考慮事項についてのクラシルの方針について UI Stateは画面、及びUI Elementごとに単一のUI Stateを使う設計を採用しており、Modelからdispatchされる際は必ず全ての情報が一度にdispatchされます。これにより、互いに関連したStateが同時に更新されることが保証されており、全ての情報が最新の情報に保たれます。さらに、ビジネスロジックによっては、複数の情報ソースを組み合わせる必要がある場合もあります。例えば、ユーザーがサインインしていて、そのユーザーがプレミアムレシピの購読者である場合にのみ、お気に入りボタンを表示する必要があるかもしれません。複数の情報ソースから取得した情報を組み合わせる必要がある場合は、次のようにUI Stateクラスを定義することが出来ます。 class RecipeState( val isSignedIn: Boolean = false , val isPremium: Boolean = false , ... ) { val shouldShowBookmarkButton = isSignedIn && isPremium } クラシルアーキテクチャーでは単一のUI Stateストリームを採用しています。これは常に最新の情報をMVI View関数から参照するためであり、実装を単純にするのが目的ですが、次にあげるような副作用を回避できる仕組みを別に用意しているからです。 関連性のないデータをまとめることによるデメリットの回避: 単一のUI Stateストリームを使うことで、関連性のないデータを一か所にまとめることになりますが、それを束ねたことによるデメリットはクラシルアーキテクチャーでは大きくありません。UI Stateのdispatchは軽量な動作であり、頻度高く更新されてもアプリケーションのパフォーマンスへの影響は微々たるものです。そして、MVI View関数では、単一のUI Stateから関連性のないデータを簡単に個別に取り出せるようになっています。 UI Stateを使った差分更新の採用: クラシルアーキテクチャーでは、MVI View関数内でUI Stateのフィールドを使った差分更新を容易に行うことが出来るようになっています。以下のように書くことで、distinctUntilChanged & combineLatestを行うことが出来るため、実装者は特別なことを意識せずに単一のUI Stateからそれぞれ別の情報を使った差分更新を行うことが出来ます。 class RecipeItemView(...) { fun view(context: Context, argument: Argument, updater: ViewUpdater,...) { updater.update(argument.title, argument.shouldShowBookmarkButton) { layout, title, shouldShowBookmarkButton -> layout.bookmarkButton.isVisible = shouldShowBookmarkButton layout.bookmarkButton.text = context.getString(R.string.bookmark_button_message, title) } updater.update(argument.title) { layout, title -> layout.titleLabel.text = title } } } 処理中の状態を表示する UI Stateで処理中の状態を表現するもっとも単純な方法は、booleanのフィールドを作ることです。 class RecipeState( val isFetchingRecipes: Boolean = false ... ) この値は、UIにおけるプログレスバーの有無をあらわします。 class RecipeModel( val recipeFeature: RecipeFeature ) { private val recipeListUseCase = recipeFeature.recipeListUseCase fun model(action: Action, dispatcher: StateDispatcher,...) { when (action) { is FetchAction -> { recipeListUseCase.fetchRecipeItems() .doOnSubscribe { dispatcher.dispatchState( copy( isFetchingRecipes = true ) ) } .doFinally { dispatcher.dispatchState( copy( isFetchingRecipes = false ) ) } .subscribe { recipeItems -> dispatcher.dispatchState( copy( recipeItems = recipeItems ) ) } } } } } class RecipeView(...) { fun view(context: Context, state: State, updater: ViewUpdater,...) { updater.update(state.isFetchingRecipes) { layout, isFetchingRecipes -> layout.progressBar.isVisible = isFetchingRecipes } } } UIイベント UIイベントは、UI layerでMVI Intentによって処理されるべきアクションです。もっとも一般的なイベントの種類は、ユーザーイベントです。ユーザーは例えば画面をタップしたり、ジェスチャー操作を行うことでユーザーイベントを発生させます。そして、UIは onClick() といったイベントリスナーを使ってイベントを受け取ります。 キーワード: UI: ViewかComposeで書かれ、ユーザーインターフェースをハンドリングする。 UIイベント: UI layerによってハンドリングされるべきアクション。 ユーザーイベント: ユーザーがアプリを操作することによって発生するイベント。 MVI Modelは通常、特定のユーザーイベント(例えば、ユーザーがボタンをクリックしてデータを更新する)のビジネスロジックを処理する責任を持ちます。クラシルでは、MVI ModelがMVI Intentがユーザーイベントを処理して発火したイベントに対する処理を記述することにより、ユーザーイベントを処理します。ユーザーイベントは、UIが直接処理することが出来るUI behaviorロジックを持つ場合もあります。例えば、別の画面へ移動したりSnackbarを表示したりする場合です。 ビジネスロジックは、同じアプリが違うモバイルプラットフォームやフォームファクターで提供されても変わることはありませんが、UI behaviorロジックはプラットフォームやフォームファクターで変わってくる可能性がある実装の詳細のことをあらわしています。UI layerのページでは、これらのタイプを次のように定義しています。 ビジネスロジックとは、例えば決済やユーザー設定の保存など、状態の変化をどのように処理するかということを指します。通常、Domain layerとData layerがこのロジックを処理します。クラシルでは、基本的にビジネスロジックを処理するクラスとして、UseCaseクラスが使用されます。 UI behaviorロジックまたはUIロジックとは、状態の変化をどのように表示するか、例えば画面遷移をどのように行うかやユーザーにどのようにメッセージを見せるかなどを指します。UIがこのロジックを処理します。 UIイベント決定木 次の図は、特定のイベントのユースケースを処理するための最適なアプローチを見つけるための決定木を表しています。このガイドの残りの部分では、これらのアプローチについて詳しく説明します。 ユーザーイベントをハンドリングする UI Elementの状態変更に関連するイベントであれば、UIが直接ユーザーイベントを処理します。(例えば、開閉できるアイテムの状態など)そのイベントが、画面上のデータをリフレッシュするなどのビジネスロジックを実行する必要がある場合は、UseCaseによって処理されるべきです。 次の例では、UIの開閉(UIロジック)と画面上のデータのリフレッシュ(ビジネスロジック)のために、異なるボタンがどのように使用されるかを示しています。 class RecipeIntent { fun intent(layout: RecipeBinding, actionDispatcher: ActionDispatcher) { layout.expandButton.setOnClickListener { actionDispatcher.dispatch(ExpandAction()) } layout.refreshButton.setOnClickListener { actionDispatcher.dispatch(RefreshAction()) } } } class RecipeModel( val recipeFeature: RecipeFeature ) { private val recipeListUseCase = recipeFeature.recipeListUseCase fun model(action: Action, dispatcher: StateDispatcher,...) { when (action) { is ExpandAction -> { dispatcher.dispatchState { copy(expanded = true ) } } is RefreshAction -> { recipeListUseCase.fetchRecipeItems() .subscribe { recipeItems -> dispatcher.dispatchState( copy( recipeItems = recipeItems ) ) } } } } } RecyclerView内のユーザーイベント Actionが、RecyclerViewのアイテムや子UI Componentのように、UIツリーの下流から生成される場合においても、MVI Modelは依然としてユーザーイベントを処理するものであるべきです。 例えば、RecipeComponentの全てのレシピアイテムがブックマークボタンを含んでいるとします。MVI Modelは、ブックマークされたレシピアイテムのIDを知る必要があります。ユーザーがレシピアイテムをブックマークする時、RecyclerViewのアイテム用のComponentはブックマーク用のActionを発火し、親ComponentへレシピアイテムのIDを通知します。その結果、子Componentは親ComponentのMVI Modelに直接依存することなく、ユーザーイベントを移譲することが出来ています。 class RecipeItemIntent { fun intent(layout: RecipeItemBinding, actionDispatcher: ActionDispatcher) { layout.bookmarkButton.setOnClickListener { actionDispatcher.dispatch { argument -> BookmarkAction(recipeId = argument.recipeId) } } } } class RecipeModel( val recipeFeature: RecipeFeature ) { private val bookmarkUseCase = recipeFeature.bookmarkUseCase fun model(action: Action, dispatcher: StateDispatcher,...) { when (action) { is BookmarkAction -> { bookmarkUseCase.bookmarkRecipe(action.recipeId) } } } } この方法では、RecyclerViewのアイテム用のComponentは必要なデータ、つまりRecipeItemオブジェクトのリストのみを扱います。子Componntは親Componentにアクセスできないので、親ComponentがアクセスしているUseCaseが公開する機能を悪用する可能性は低くなります。画面のMVI ModelのみがUseCaseを操作できるようにすると、責任を分離できるようになります。クラシルではViewやRecyclerViewのAdapterなど、UI固有のオブジェクトはUseCaseを直接操作できないようになっています。 Domainイベントをハンドリングする Domain layerに由来するUIアクション、Domainイベントは常にUI stateを更新する必要があります。これは、UnidirectionalData Flowの原則に従ったものです。これは、設定変更のイベントに再現性を持たせ、UIアクションが必ず処理されることを保証します。クラシルでは、UI stateは必ずState bundleに保存されるので、プロセスが終了した後でもDomainイベントの副作用を再現できるようにすることが出来ます。 class RecipeModel( val recipeFeature: RecipeFeature ) { private val bookmarkUseCase = recipeFeature.bookmarkUseCase private val recipeListUseCase = recipeFeature.recipeListUseCase fun model(action: Action, dispatcher: StateDispatcher,...) { when (action) { is BookmarkAction -> { bookmarkUseCase.bookmarkRecipe(action.recipeId) .flatMap { recipeListUseCase.fetchRecipeItems() } .subscribe { recipeItems -> dispatcher.dispatchState( copy( recipeItems = recipeItems ) ) } } } } } おわりに 実は、今回この記事を書くにあたって、既存のクラシルのアーキテクチャをAndroid公式のものに沿っていくつか再解釈したところがあります。今まで作ってきたアプリケーションの構造としては変わらないものの、今まで明確にそういった役割を持たせていなかった部分に対して名前を付け、整理するのに、アプリアーキテクチャガイドはとても役に立ちました。皆さんも是非、Guide to app architectureを使って自分たちのアーキテクチャを見直してみてはいかがでしょうか。 クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂いてもOKです! dely.jp twitter.com
アバター
I'm Ishida, iOS engineer in TRILL. In this article I introduce Redux architecture and implementation for iOS. TRILL engineering team has 13 engineers, about half of workers are global engineers (April 2022). We hold Study Meetup every week and a speaker talks about something technical (for example, docker, AWS, scrum, etc.) in English. I talked about Redux architecture there, so I also introduce that in this article. I introduce basic and abstract features because the meetup has various engineers (iOS, Android, backend, etc). delyではグローバル採用を進めており、日本人に限らず外国籍の方も積極的に採用しています。 もちろん全員が英語が堪能ということはなく、バイリンガルのメンバーや翻訳ツール、グローバルチーム (通訳できるメンバーが在籍) の力を借りながら進めています。 週1で行っている勉強会で発表した内容をこの記事にまとめたので、なんとなくTRILLのグローバル化を感じてもらえればと思います。 折角なので日本語で書いて英語に自動翻訳、ということはせず英語で書きました (とはいえ多少調べました…) 。 Background In this decade, iOS application become bigger and architecture is more important. We have many architectures for iOS app, MVC, MVP, MVVM, Clean architecture, VIPER, etc. This article introduces Redux architecture. About Redux Redux architecture is from web front-end technology. Originally, web front-end was simple and clear because it didn't have state and API connection was simple. Recently web front-end has become bigger and more complex, so we've needed architecture against that. In the situation, Redux architecture was created. And Redux try to solve those problems by two concepts: mutation and asynchronicity. redux.js.org says "Redux can be described in three fundamental principles". Three fundamental principles are: Single source of truth State is read-only Changes are made with pure functions Roles and data flow of Redux is like below figure. reference source Redux has 4 roles: action, state, reducer, store. The app has one store that has states and reducers. UI dispatches an action, and the reducer receives action and state. The state is updated, the store notifies the UI that. In next section, I introduce the implementation by ReSwift . Implementation I implemented a simple counter app. This app has 2 tabs, one tab (counter tab) has counter label and button, the other tab (result tab) has counter label. When I tap a button in counter tab, 2 label in each 2 tabs change. App's screenshots are below: CounterViewController ResultViewController CounterViewController has 1 label and 1 button, and ResultViewController has 1 label. First we define each roles, action, state, reducer, store. Each roles are defined below. struct AppState { var counter : Int = 0 } struct CounterActionIncrease : Action {} func counterReducer (action : Action , state : AppState? ) -> AppState { var state = state ?? AppState() switch action { case _ as CounterActionIncrease : state.counter += 1 default : break } return state } let mainStore = Store < AppState > ( reducer : counterReducer , state : nil ) In this app, state has only one parameter (counter). Store has a reducer and the reducer receives action and state. Action detail is described in reducer (increment counter). Next CounterViewController is below. final class CounterViewController : UIViewController , StoreSubscriber { @IBOutlet private weak var counterLabel : UILabel! override func viewDidLoad () { super .viewDidLoad() mainStore.subscribe( self ) } func newState (state : AppState ) { counterLabel.text = " \( state.counter ) " } @IBAction func increment(_ sender : UIButton ) { mainStore.dispatch(CounterActionIncrease()) } } For simplicity, this view controller subscribes in viewDidLoad() When users tap increment button, increment(_ sender:) function is called. This function sends CounterActionIncrease() to dispatch of mainStore. This is just Redux data flow. When state changes, this class receives in newState(state:) function. And this function sets new data to counterLabel . ResultViewController is below. final class ResultViewController : UIViewController , StoreSubscriber { @IBOutlet private weak var resultLabel : UILabel! override func viewDidLoad () { super .viewDidLoad() mainStore.subscribe( self ) } func newState (state : AppState ) { resultLabel.text = " \( state.counter ) " } } ResultViewController just subscribes state. When state changes, newState(state:) function is called and set to label. The label changes without manual update. This app is too simple, so we can implement without Redux architecture. But this architecture makes easier to read codes by defining some roles. Conclusion I introduced Redux architecture. Apple announced SwiftUI and iOS app architectures have changed recently. I'd like to catch up architectures in the future. We're hiring engineers and designers. If you are interested in our company, please contact us. dely.jp
アバター
こんにちは。クラシルサーバサイドのエンジニアをしておりますnegiです。 クラシルサーバサイドでは2021年10月から2022年3月にかけてRailsのバージョンアップ(5.0 → 6.1)を行なったので記事にしました。 クラシルでは2019年にRails5.0にバージョンアップして以降、バージョンアップができていませんでした。Rails5.0がEOLとなってからサポート対象外の状態になっていましたが、今回のバージョンアップによってサポート内にすることができました。 リリースは以下のように3回に分割して行いました。 5.0 → 5.1 5.1 → 6.0 6.0 → 6.1 5.0 → 6.1のように一気にバージョンアップを行うと、影響範囲が大きすぎるため想定外の問題が発生する可能性が高く、問題の切り分けも難しくなるため分割してリリースしました。 5.1 → 5.2のリリースはマイナーバージョンアップということもあり、テスト工数の削減のためにスキップしましたが、結果的にこのリリースをスキップしたことにより発生した問題もあったのでマイナーバージョンアップでもリリースのスキップすることはしない方がよかったと感じています。 やったこと 調査・修正 Railsと関連するgemのアップデート changelogを確認し、影響のある箇所を調査・修正 rspecでエラーとなった箇所の修正 DEPRECATIONエラーが発生している箇所の修正 これらの修正のPRは合計で87件でした。PRにはどのバージョンに対する修正なのかをわかりやすくするためにバージョン毎のLabelを付与するようにしました。 コード管理 クラシルでは機能開発も並行して進んでいるため、バージョンアップ専用のブランチを作成して作業を行うとconflictが発生する可能性が高く、マージミスなどによる不具合も発生しやすくなります。また、今回のバージョンアップでは複数のバージョンを一気に上げるため、バージョン毎のコード管理が複雑になる可能性もありました。不具合によるロールバックなども考慮すると管理し切れない程複雑になると予想していました。 そこで私たちは以下のようにして対策を行いました。 Gemfileをバージョン毎に作成し、環境変数 BUNDLE_GEMFILE で切り替えを行うようにする バージョンアップに伴うコード修正はバージョン毎に分岐させるようにする if Kurashiru.rails_v5_0_7_1? # rails 5.0.7.1 code else # another rails version code end これにより、既存のコードを変更することなくバージョンアップ関連のコードをmasterブランチにマージすることが可能となりconflictの発生を防ぐことができます。 また、環境変数 BUNDLE_GEMFILE の変更のみでRailsのバージョンを切り替えることが可能となりました。実際、不具合によるロールバックも何度か発生しましたがこの方式のおかげでコードの管理はかなり簡易的となりスムーズにロールバックすることが可能でした。 検証 rspecがエラーとならないこと 既存バージョンと新バージョンで動作の差分を比較 ステージング環境にて機能の動作確認 クラシルサーバサイドではCoverageを95%にキープできているので、ある程度の問題はrspecでカバーできるはずであると考えています。開発環境での確認では全機能を細かく確認することはせず、重要な機能のみの確認を行いました。 (Coverageをどのようにキープしているのかは別途記事にできればと思います。) リリース方法 リリースは段階的に行いました。 まずはサーバ1台のみにデプロイをして丸1日稼働させ、これで問題がなければ全サーバにリリースしました。想定外のエラーが発生した場合はロールバックを行い、調査→修正→検証→再リリースのサイクルを行いました。これによりサービス全体への影響を最小限に抑えつつ、新バージョンのリリースを行うことができました。 まとめ 今回のRailsバージョンアップを通して感じたことは↓です。 リリースはマイナーバージョン毎に分けてした方が良い カバレッジは高い水準でキープしておいた方が良い リリース時の問題はある程度許容することが必要 問題が発生することを想定して、ロールバックがしやすい環境・体制にしておくことが重要 クラシルでは過去にRailsバージョンアップを行なった実績はありましたが、今回のように複数バージョンを一気に上げるということはなかったので、Railsバージョンアップが行えたこと、サポート内にすることができてよかったです。 リリースは不具合によるロールバックを何度か行なったので決してスムーズではなかったですが、チームメンバーに調査やリリースに協力してもらいながら進めることができました。感謝です! 最後に クラシルで行なったRailsバージョンアップについて紹介しました。Railsバージョンアップはシステムへの影響も大きいので苦労される方々も多いかと思います。参考にして頂けると幸いです。 delyではエンジニアを募集しています!少しでも興味ある方、ご応募お待ちしています! dely.jp
アバター
はじめに こんにちは!クラシル開発部でデータエンジニアをしておりますharry( @gappy50 )です。 先日、Snowflakeの「the 2022 Data Superheroes」に選出いただきました! www.snowflake.com 世界で48名、日本で7名のSuperherosが選出されており、そのうちの1人に選ばれたことは嬉しい反面、とても恐れ多い気持ちもいっぱいではあります! 今回は、Data Superheroについてのご紹介や、今後の活動について簡単に書かせていただければと思います! Snowflake Data Superheroって? こちら に詳細書いてありますが、Snowflakeコニュニティメンバーの中でもコミュニティでの発信や、Snowflakeのエキスパート、熱意のある方が選出される制度です! 私はSnowflakeのエキスパートまではいかないですが、これまでもSnowflakeのJapan User Groupの SnowVillage - YouTube でお話をさせていただいたり、 Snowday Japan - Snowflake でもクラシルでの採用事例を紹介させていただいたりしました! クラシルでのSnowflake活用事例についてはブログやスライドでも紹介しております↓ tech.dely.jp speakerdeck.com Data Superheroになってよかったことは? 引き続きData SuperheroとしてSnowflakeに関する情報を発信できることは嬉しいのはもちろんですが、Snowflake認定試験のバウチャーや新機能の早期アクセス権などの様々な特典をいただけるのもメリットです! あとは、世界中のData Superhero達が集うSlackのプライベートチャンネルもあり、Snowflakeを通して世界のデータの潮流を肌で感じられたり、カンファレンスでみたことある方が普通にいらっしゃるみたいなこともかなり刺激を受けています。 個人的にはアメコミ風のわたしのキャラクターを作ってもらえるのが一番うれしかったりします!!! 今後について コミュニティの活動としては #SnowVillage LIVE Season3というコミュニティメンバーで企画したコンテンツがどんどん配信されていくと思うので、楽しみにしていてください!!! 内容はまだ秘密にしておきますが、Snowflakeを通して最近のデータの潮流を感じられるようなコンテンツが盛りだくさんなので、是非チャンネル登録をお願いします!(Youtuber気分) www.youtube.com クラシルでも、上記のブログを書いて以降でまたSnowflakeに関する面白い取り組みをし始めているのでどこかのタイミングで発信できればと思っています! それと、個人的な活動は最近なかなかできていませんが、 Twitter や個人ブログでも自分が気になった機能や情報もちょくちょく発信していたりしますので是非コミュニケーションさせてください! 最後に 簡単ではございますがSnowflake Data Superheroとは何なのか、そしてData SuperheroとしてこれからSnowflakeに関する情報発信していきますという意気込みを書かせていただきました! クラシルでもSnowflakeの活用がこれからますます重要になっていくフェーズになっているので、これからこのブログでも色々と技術的な知見をご紹介できればと思います。 最後に、Snowflakeを使い倒してクラシルのデータからユーザーへの価値提供に貢献してみたい!と思うデータエンジニアやMLエンジニアの方々に関わらず、delyではエンジニア、デザイナー、PdMを積極採用しています。 ご応募お待ちしております!パワー! dely.jp
アバター
こんにちは、クラシルAndroidエンジニアの @MeilCli です。前回、クラシル内のレシピ保存機能の開発に際してページングに関して考慮した理想のUXについての考え方について紹介しました tech.dely.jp 今回はそれの後編にあたり、Android側の実際の実装に関して深ぼって紹介しようと思います 設計 前回の記事において、サーバー側は時刻ベースのCursorを用いたページングAPIの実装、クライアント側は要素の追加・移動などのユーザー操作を記録し、その差分反映をリストに対して行うという実装方針を紹介しました これをクラシルAndroidの設計に基づいた構造に落とし込んでいきます クラシルAndroidの設計については先日別記事にて紹介したのでよかったらそちらも読んでいただければと思います tech.dely.jp 保存する・保存解除する・保存状態を確認する まず保存に関する基本的なCreate/Read/Deleteに関する操作ですが、クラシルAndroidの設計を図示すると画像のような感じになります ※保存は内部的にはBookmarkと呼ばれています BookmarkRecipeUseCaseはおおよそ以下のようなinterfaceをしています interface BookmarkRecipeUseCase { val bookmarkingRecipeIds: Flowable< Collection < String >> fun requestBookmarkStatus(targetRecipeIds: List < String >) fun bookmarkRecipe(recipeId: String ) fun unBookmarkRecipe(recipeId: String ) } 保存する・保存解除するの動作でRestClientを呼び出し、結果に応じてDb/Cacheの値を更新する。その際にPublishProcessorに変動した保存状態を通知しUI Layerに反映する。保存状態の確認をする場合にはDb/Cacheの値を第一報として通知し、RestClientで正確な値を後から通知するという流れです ユーザー操作を記録する 画像の通り、ユーザー操作を記録するのは簡単です。記録したい操作を行うUseCaseがイベントの記録を行うUseCaseに対してイベントを教えてあげればよいだけだからです 保存する・保存解除する・保存状態を確認する ではDb/Cacheの2つのData Source、つまりSQLiteへの保存とインメモリーキャッシュを行っていました。ユーザー操作に関しては保存内容が多くなりそうなこと・インメモリーキャッシュを要求するほど速度を求めてないことからSQLiteへの保存のみを行うことにしました またその際に保存している内容としては、ID・オブジェクトJSON文字列化したもの・イベントが起きた時刻という感じです ユーザー操作を反映した最新のリストを構築する リストを構築する上で、まずページングの基盤が必要です。ページングの基盤については以前の記事で紹介したのでよかったら見ていただければと思います tech.dely.jp 基本方針としては、ページングの基盤から取ってきたリストにページングを開始した時刻以降のユーザー操作を反映させていくという感じです 画像にするとこのような感じですね ページング基盤ではシンプルに Single<List<T>> のような返り値なので、それに対してmapやflatMapを行い、ユーザー操作に応じてaddFirst, remove, moveFirstなどのオペレーションを加えるという形になります そしてUI Layer側はOnStartやPullToRefreshなどのタイミングでUseCase側に最新のリストを要求するということになります。これにてめでたしめでたし、ということでは終わりません。この設計・実装ではリストを表示している他画面で行われたユーザー操作を反映することができますが、同じ画面内で行われたユーザー操作を反映することはできません。そのような場面になったら、BookmarkUseCaseがBookmarkEventUseCaseに対してイベントの購読を行い、イベントが起こったタイミングで最新のリストを計算し、それをUI Layerに通知するといった追加の処理が必要になります 終わりに 以上にてクラシルAndroidにおいてのユーザー操作を反映したリスト表示の設計と実装についての紹介を終わりにしたいと思います クラシルAndroidで採用した手法は数ある手法の内の1つであります。古典的な方法で行くとEventBusのような仕組みもありえますし、ユーザー操作が行われた時点で画面の表示情報をリフレッシュするという手もあると思います。クラシルAndroidで採用した手法よりももっといい手法があればコメントなどで教えていただければと思います。また、詳細な設計や実装について気になればカジュアル面談でお話することもできると思うのでぜひお声がけいただければと思います meety.net
アバター
こんにちは、クラシルAndroidエンジニアの @MeilCli です。近々、クラシル内のレシピ保存機能において クラシルショート とレシピカードも保存できるようにするという変更が入ります。それの開発に際して、ページングのあるAPIにおいて更新されうるコンテンツをどう表示していくかを開発チーム内で話し合い、理想と思うものを実装したのでそれの共有を行います 当記事は前後編の前編にあたり、どう表示していくかの考え方についてご説明します 更新されうるコンテンツの理想的なUX まず、どういうことが問題になっているのか説明します ユーザーが保存タブを開いたときの表示、つまり自分が保存しているレシピ一覧についてですが、保存タブからそのレシピの詳細画面を開き保存を解除したとします。この時点で保存タブを開いていたときから保存しているレシピ一覧が変わっていることになります。そして保存タブに戻ってきた時の表示はどのようなものが理想的なUXとなるのか、という問題です 画像にした図 開発チームで話したときに出たアイデアとそれのメリット・デメリットは以下のようなものでした アイデア メリット・デメリット 画面表示時にAPIを叩き、コンテンツを再表示する ページングAPIなのでコンテンツを再表示すると2ページ目以降だった場合にスクロール位置を維持できない アプリ側は表示していたコンテンツを変えずに、ユーザーのPullToRefreshによる更新に頼る 実装は楽だが、PullToRefreshはユーザーの能動的な操作に頼ってしまっている アプリ側は表示すべきコンテンツが変わったことを検知し、ユーザーに更新がありますよと教えてあげる 実装は重くなるが、更新を通知することでユーザーの意図したタイミングで変更を適用できる、しかし能動的な操作を行ってもらう必要がある アプリ側は表示すべきコンテンツが変わったことを検知し、自動で変更を適用する 実装は重くなるが、自動で更新される、それがユーザーの意図した挙動なのかが焦点 いろんなアプリを触りながら理想的なUXを検討したところ、 変更を自動で適用するというUXが一番良いのではないかという結論になりました。 自動で変更されるアプリは多く、特にSNS系のアプリでは自動で変更が適用されることがユーザーの意図した挙動でありそうだったからです ページング方式 我々が理想とするUXが決まったら次はそれを実現するための設計です。そこで最初に出てきた問題としてページングの問題でした このようなリスト要素と1ページあたり10件のページングを考えます。クラシルでよく使用されていた手法としては1ページあたりの件数とページ番号を指定してリクエストする方式でした。今までユーザー操作による要素の増減があまりなかったのでこの手法では問題にならなかったのですが、ページングを完了する前にユーザー操作が行われると問題になる場面があります 画像のように、1ページ目をリクエストしたあとに要素の削除が行われると2ページ目を取得したときの結果に含まれない要素が出てきてしまいます。また、リストの先頭に要素を追加するなどした場合には2ページ目を取得したときの結果に1ページ目を取得したときの結果が含まれるようにもなってしまいます。そのため、1ページあたりの件数とページ番号を指定してリクエストする方式では問題のあることがわかりました では、他に取れるページング方式には何があるでしょうか。多様な方式があると思いますが、ページの起点となる要素に着目すると以下の2パターンがよくあるものかなと思います 要素のIDを指定し、それ以降・以前の要素を返す 要素の作成・更新された時刻を指定し、それ以降・以前の要素を返す どちらの方式も1ページ目を取得した結果の末尾の要素を基準点とし、次の2ページ目のリクエストに指定するという形になります そうすると画像のように要素の減少に対応したページングを行うことができます そして、どちらの方式を採用するかですが、UI上の話をすると、保存機能は保存順や閲覧順といったソートができるUIを提供する予定です。そのため、実際のレスポンスはID順とはならないため、時刻ベースのページングである必要がありました。最終的な仕様としては時刻ベースのページングを行うCursorをレスポンスに追加し、クライアント側はCursorを用いて次ページのリクエストをするという形になりました 要素の追加・移動があった場合の対応 前述のページング手法では主に要素の削除に対応した方式について話しました。しかし、ユーザー操作は要素の削除だけではありません。要素の追加や移動といった操作がありえるのです これについてはクライアント側でユーザー操作に応じた差分反映をリストに対して行うしかありません しかし、設計上の難しいところはありません。なぜならクライアント側でユーザー操作が行われるのですから、ユーザーがどのような操作をしたかを記録し、それに応じて差分を反映すればよいからです まとめ クラシルでは更新されうるコンテンツについては変更を自動で適用するというUXが理想形ではないかということになりました サーバー側は時刻ベースのCursorを用いたページングAPIの実装、クライアント側は要素の追加・移動などのユーザー操作を記録し、その差分反映をリストに対して行うという実装を行うことになりました 次回はAndroid側の実際の実装に関して掘り下げていこうと思います
アバター
こんにちは、クラシルAndroidエンジニアの @MeilCli です。先日ページングの基盤を実装したので紹介します なぜページングの基盤を実装することになったのか クラシルAndroidにはもともとFeedListContainerというページングに関する実装がありました。インターフェースとして表現するとUI Layerからは以下のような見た目です interface FeedListContainer<TId, TValue> { fun getUpdateFlowable(): Flowable<FeedState<TId, TValue>> fun getErrorFlowable(): Flowable< Throwable > fun requestNext() fun requestRefresh() fun restore(state: FeedState<TId, TValue>) } ページングリスト本体となるインスタンスはUI Layer側のStateとして保持し、Domain Layer側が提供するFeedListContainerがページング後のStateをUI Layerに提供するという形式です ここで、先日投稿したクラシルAndroidの次世代アーキテクチャー設計を御覧ください tech.dely.jp 先日紹介したクラシルAndroidのこれからのアーキテクチャ設計では、データの加工などの処理はDomain LayerのUseCaseが担当することになっています。現状のFeedListContainerではDomain Layerが公開したFeedListContainerをUI Layerが参照し、FeedStateの更新をUI Layer側で観測するという形になっています。そのため、FeedStateの加工をしたい場合にはUI Layerで行うのが慣習化していました そこで、Data LayerやDomain Layerといったレイヤー定義に従ったページングを実装する運びになりました 新ページング基盤の設計 新しいページング基盤を作成するにあたって以下のような使い勝手にしようと考えました request関数の返り値にresponseを付ける そのため、呼び出し順によって問題が発生しないようにresponseは最新のrequestの結果によるものを返す *1 基盤側でDBにページングリストを保存するが、利用者側はページングAPIを用意すればページングを実装できるようにする ページングを始めた時刻(SessionStart)を保持する DomainLayerでのページングリストの加工ができる形にする 上述のことを実現するにあたってこれからのアーキテクチャー設計に照らし合わせると以下のような構造になります 新ページング基盤の簡略図 また、PagingCollectionProviderは以下のようなinterfaceを持つことになります sealed class PagingLink { // offsetベースやcursorベースのページング用にそれぞれ、次のリクエストのための情報を保持する abstract val hasNext: Boolean abstract val total: Int ? } interface PagingCollectionProvider<TParameter, TLink, TElement> where TLink : PagingLink { fun request(request: PagingRequest<TParameter>): Single<PagingCollection<TElement>> } 新ページング基盤の実装 すべてのコードをお見せしようとするととても長くなるため、一部抜粋で紹介させていただきます まず基本となるクラスを用意します class PagingCollection<T>( val metaData: PagingMetaData, val session: PagingSession, val latestRequestSegment: List <T>, private val source: List <T> ) : List <T> by source class PagingCollectionSegment<TLink : PagingLink, TElement>( val link: TLink, val source: List <TElement> ) : List <TElement> by source PagingCollectionがページングリストの本体を表します。また、それぞれのページング時にAPIなどに問い合わせた結果ををページングリストの一部=Segmentとして用意します また、APIとの通信部分は各利用者のRestClientが提供することになります。その通信部分をPagingCollectionProviderに渡しやすくするために関数として提供する形式にしました typealias PagingApi<TParameter, TLink, TElement> = (parameter: TParameter, nextLink: TLink) -> Single<PagingCollectionSegment<TLink, TElement>> // 提供側 class ExampleRestClient( private val exampleApi: ExampleApi // Retrofit側のinterface ) { // この関数をPagingCollectionProviderに渡す fun fetchExample( parameter: ExampleRequestParameter, nextLink: PagingLink.KeyBase ): Single<PagingCollectionSegment<PagingLink.KeyBase, Example>> { return exampleApi.fetchExample .map { PagingCollectionSegment( PagingLink.KeyBase(it.meta.nextPageKey != null , it.meta.nextPageKey, it.meta.totalCount), it.data ) } } 取得したPagingCollection/PagingCollectionSegmentをローカルに保存しておく必要もあります。AndroidにおいてIn-memoryなオブジェクトはアプリケーションのライフサイクルより短いスパンで開放される可能性があります。アプリケーションのライフサイクルに合わせるにはBundle以上の寿命をもたせる必要があるのですが、今回はDBに保存しました class PagingState<TLink, TElement>( val elements: List <TElement>, val nextLink: TLink ) where TLink : PagingLink interface PagingStateDb<TLink, TElement> where TLink : PagingLink { fun update(componentPath: String , state: PagingState<TLink, TElement>): Completable fun get (componentPath: String ): Maybe<PagingState<TLink, TElement>> } 上記のようなインターフェースを実装していくという感じです。ページングリストの要素についてですが、ばか正直にDBのTableを用意すると変更がしづらいため、Jsonの文字列として保存するようにしました。一定のパフォーマンスペナルティが出ますが、In-memory cacheである程度の場面でDBを参照しなくてよいこと、RecyclerViewの構築は非同期スレッドで要素の差分を計算したり、もともとのページング処理が非同期であること、実際に動作させたところ大きな遅延が見られなかったことなどからページングリストの要素はそれぞれJsonの文字列に変換して保存することにしました あとはPagingCollectionProviderが各DataSourceを結びつけてページング処理を実行するだけです class PagingCollectionProvider<TParameter, TLink, TElement>( private val currentDateTime: CurrentDateTime, private val api: PagingApi<TParameter, TLink, TElement>, private val stateCache: PagingStateCache<TLink, TElement>, private val stateDb: PagingStateDb<TLink, TElement>, private val sessionCache: PagingSessionCache, private val sessionDb: PagingSessionDb, private val linkProvider: PagingLinkProvider<TLink>, private val applicationExecutors: ApplicationExecutors ) where TLink : PagingLink { private val resultPublisher = PublishProcessor.create< Pair < String , Result <PagingCollection<TElement>>>>() private var requestDisposable: Disposable? = null private var latestRequests: MutableMap < String , PagingRequest<TParameter>> = mutableMapOf() private val requestLock = ReentrantReadWriteLock() fun request(request: PagingRequest<TParameter>): Single<PagingCollection<TElement>> { return resultPublisher .filter { it.first == request.componentPath } // 最初に通知される要素のみを返り値とする .take( 1 ) .singleOrError() .flatMap { (_, result) -> val value = result.getOrNull() val error = result.exceptionOrNull() when { value != null -> { Single.just(value) } error != null -> { Single.error(error) } else -> { Single.error( Exception ()) } } } .doAfterSubscribe { applicationExecutors.background() .submit { // requestの各処理内でthread lockをしているのでcurrent threadをlockさせないためにbackground threadに投げている when (request) { is PagingRequest.Refresh -> { requestRefresh(request) } is PagingRequest.LoadFirst -> { requestLoadFirst(request) } is PagingRequest.LoadMore -> { requestLoadMore(request) } is PagingRequest.GetCollection -> { requestGetCollection(request) } } } } } private fun requestApi( request: PagingRequest<TParameter>, session: PagingSession, nextLink: TLink, newStateCreator: (PagingCollectionSegment<TLink, TElement>) -> PagingState<TLink, TElement> ): Disposable { return api.invoke(request.parameter, nextLink) .flatMapCompletable { segment -> val newState = newStateCreator(segment) stateCache.update(request.componentPath, newState) stateDb.update(request.componentPath, newState) .doOnComplete { resultPublisher.offer( Pair ( request.componentPath, Result .success( PagingCollection( metaData = newState.nextLink.toMetaData(), session = session, latestRequestSegment = segment, source = newState.elements ) ) ) ) } } .andThen(sessionDb.update(request.componentPath, session)) .doOnComplete { sessionCache.update(request.componentPath, session) } .doOnError { resultPublisher.offer( Pair (request.componentPath, Result .failure(it))) } .doFinally { requestLock.write { latestRequests.remove(request.componentPath) } } .subscribe() } private fun requestRefresh(request: PagingRequest.Refresh<TParameter>) { requestLock.read { val latestRequest = latestRequests[request.componentPath] if (latestRequest is PagingRequest.Refresh) { return } } requestLock.write { requestDisposable?.dispose() requestDisposable = null latestRequests[request.componentPath] = request val nextLink = linkProvider.initialLink val session = PagingSession(currentDateTime.nowUnixLong()) requestDisposable = requestApi(request, session, nextLink) { PagingState(it, it.link) } } } // requestLoadFirstやrequestLoadMoreも同様にDataSourceを使い分けて処理します // ただしrequestLoadMoreやrequestLoadFirstよりもrequestRefreshを優先的に処理するようにしたりなどの場合分けが必要になります } ちなみにですが途中に出てくる Single<T> に対する doAfterSubscribe 拡張関数は以下のような実装です fun <T> Single<T>.doAfterSubscribe(action: () -> Unit ): Single<T> { return SingleDoAfterSubscribe( this , action) } class SingleDoAfterSubscribe<T>( private val source: SingleSource<T>, private val action: () -> Unit ) : Single<T>() { override fun subscribeActual(observer: SingleObserver< in T>) { source.subscribe(observer) action() } } 注意点 クラシルAndroidのDomain LayerやData LayerのUseCaseやDataSourceはSingletonになっています。ページングリストは画面ごとに必要になるオブジェクトですので、画面ごとのページングリストを判別する識別子が必要になります。今回はUI LayerのComponentIdが各画面ごとにユニークな値として存在しているのでそれを活用しました 終わりに クラシルAndroidの基盤に乗っける形式でページングの基盤を作成しました。そのため、他のアプリケーションでは一部噛み合わない箇所があるかもしれません。ページングについてはPaging Libraryを利用するなどの方法がありますが、アプリケーションの理想的な挙動を実現するには最終的には自作しかないんじゃないかなと思います。そのための助けになれば幸いです もしクラシルAndroidのページング基盤の実装について聞きたいことがあればカジュアル面談で話をすることもできるので気軽にお声がけくださいm( )m meety.net *1 : ここについては異論が出るところだと思う。なぜならrequestした通りの結果が返ってくるのではなく、ページングリストとしての最新の値が返ってくるのだから。ただ、そのような違和感のある動作をすることによる弊害はなく、むしろエラーハンドリングやローディングハンドリングがしやすくなるというメリットがあるのでこの方針にしました
アバター
こんにちは、クラシルAndroidエンジニアの @MeilCli です。先日Androidチームで設計についてお互いの認識を合わせ、今後のクラシルAndroidのアーキテクチャー設計をどうするか決めたので共有します 基本的な考えについてはテックリードのうめもりさんが書いた記事にありますのでよかったら読んでください *1 tech.dely.jp レイヤー構成 レイヤー構成 クラシルAndroidには3つのレイヤーが存在します UI Layer Viewの描画・ユーザ操作のハンドリング・ViewにまつわるStateの管理 Domain Layer データの加工・UI Layerへの公開 Data Layer データ操作を提供 それぞれのレイヤーにおいて複数のクラスが関わってきますが、今の所Domain LayerにはUseCase、Data Layerにはプリミティブなデータ操作を提供するDataSourceしか定義されていません。先日うめもりさんが書いた記事やGoogleのドキュメントにはRepositoryの存在が予見されていました。しかし、チームでの話し合いの元、今はUseCaseとDataSourceの中間の存在を定義しないという結論になりました 要約すると以下の感じです JSON色付け係のようになっている今のクラシルAndroidでは、RepositoryがただDataSourceのメンバーを公開するだけの存在になってしまう Data LayerとDomain Layerで利用するEntityを別々にし、変換を挟むというのであればRepositoryのような中間の存在の必要性が出てくるが、現状のクラシルAndroidはそれを必須にして行う規模感ではない DataSourceの安全なアクセスを提供するための中間の存在にはメリットもあるが、UseCaseがそれの代替を行うことが可能 仮に変換ロジックを挟むなどの場面が出たとしても、それはRepositoryという名称で行わずにもっと狭い名前で行った方が良い 現状では中間の存在を切り出すほどの場面がないので必要に応じて定義する 事実上のレイヤー構成 そのため、事実上は画像のような構成になっているという解釈で問題ありません UI Layer UI LayerはJetpack Composeにおける役割とほとんど変わらないと思います Viewを描画する ViewのStateを管理する 画面遷移などの遷移処理 Domain Layerとの通信 UI Layerは主にこれらの役割を担っています。また、現在はAndroidViewを利用する実装ですが、将来的にJetpack Composeを利用する形に変更していきたいという思惑があります。そのため今は移行期間であり、UIの書き方を少しずつ変えていっている段階なので具体的なコードは省かせていただきます なんにしろ、UI LayerはDomain Layerから受け取ったデータをただ表示するだけという形を目指していくことになります Data Layer(DataSource) Domain Layerの前にData Layerの説明を行います DataSourceには様々な種類に対応する名前が用意されています。そのためコード上ではDataSourceという名前ではなくそれぞれの名前が用いられています Rest Client: サーバ側との通信を行う Preferences: SharedPreferencesへの保存を行う Db: SQLiteへの保存を行う Cache: In-memory cacheを行う etc... これらのDataSourceの役割は非常にシンプルです プリミティブなData functionを提供 プリミティブなData functionの整合性を保証 プリミティブなData functionというのはバックエンド側の用語で言うところのCRUD(Create, Read, Update, Delete)のような単純な操作のことを指しています。そしてそれらの操作の整合性(たとえばThread safe)を保証するのもDataSourceの役割です たとえばですが、In-memory cacheにuserを保管するとしたら以下のような感じになります class ExampleUserCache { // 実際にはthread safeな実装にする private val users = mutableListOf<User>() // クラスの責務内のデータ群に関する単一の操作(get, create, update, delete, etc...)関数を提供する fun addUser(user: User) fun removeUser(user: User) fun getUsers(): List <User> } Domain Layer(UseCase) 最後にDomain Layerです。なぜ最後に説明を持ってきたのかというと、実際のチームでの話し合いにおいてもDomain Layerで何をすべきなのかが決めにくかったためです。そこで、まず最初にData Layerを決め、次にUI Layerを決め、最後にDomain Layerについて話すことにしました。そうすることでData LayerとUI Layerで行わないことすべてがDomain Layerで行うことという考えができたためです *2 そして、アプリケーション開発のコーディングにおいて必要な部分の残り物がUseCaseの役割となったわけですが、おおまかに以下のものになりました Data Layerからデータを取得・操作する データを組み合わせ・加工・フィルタリング・バリデーションする Domain Layerの操作を組み合わせる データをUI Layerが見えるかたち(見ても良いかたち)に変換する UI Layerにバリデーションロジックを公開する 一方で、特定の画面でしか使わないような処理の命名難しいよねという問題もありました。理想的には画面によらない名前付けをしたいところですが、我々は未来を見通す力を完全に持ち合わせているわけではないので、その場で将来に渡って適切な名前を付けることは非常に難しいです。そのため特定の画面でしか使わないような処理には画面名を付けても良いということになりました 具体的には以下の運用です 画面によらない共通処理: UseCase 画面固有の処理: ScreenUseCase 複数の画面で使い回すときにはScreenUseCaseのまま使わずにRenameを行う 一方で、悪く言うと余り物で構成された役割をUseCaseが持ってしまっているので、いつかのタイミングで、処理がFatになってるよね・この処理を設計レベルで切り離せるよねということがあるんじゃないかなと思います。前述のRepositoryの話のように、そういうタイミングが訪れればあらたな存在・役割を定義するということになると思います 終わりに クラシルAndroidはリライトしてから約1年が経ちました。最初の1年はざっくりとした感覚でそれぞれがコーディングしていたので、設計がバラバラであったり、コーディング手法が確立されていなかった箇所で記述が複雑になっていたりしました また、レシピカードや今後登場する新保存機能においてこの新しいアーキテクチャー設計に沿った実装を実際に行ってみました。感触としてはUseCaseを適切に分割できていればFatな存在になってしまうのは避けれそうという感じでした。今後はこれからの設計が確立したのでそれに沿った開発を行っていく所存です *1 : 続きは催促しておきます *2 : はさみうちの原理に似てますね
アバター
皆さんこんにちは。クラシルAndroidチームのparayaです。 少しずつ梅や桜も咲き始めて春を感じる季節になってきましたね🌸 2月に入り、クラシルでは新たにAndroidエンジニアが1名ジョインしました🎉 Jさん入社おめでとうございます!🎉🎂🎉 クラシルAndroidチームでは新メンバーとの交流を深めるために、Wevox values cardを利用しています。 Wevox values cardについては過去に私がジョインした時の記事もあるので良かったら見てくださいね。 tech.dely.jp そこで、チームで恒例となっているWevox values cardでコミュニケーションを実施したいのですが… なんとクラシルではコロナ・オミクロン株対策として、1月からフルリモートしていたのでした😷 出社メンバーが少ないので対面でのコミュニケーションが取りづらいのなんの😥 何かリモートでできないかなーと探していた所、そもそもWevoxにonline版があることに気づきました💡 アカウント登録は必要ですが無料なんです。素敵ですね✨ values-card.wevox.io online版でのWevoxの模様をお伝えしていこうと思います。 チャット機能は無いのでGoogle meetで繋ぎつついざスタート! スタート画面 始まるとこのような画面でスタートしました。 リアルでやるよりも手持ちカードと場のカードが見やすいので思っていたよりプレイしやすい印象です。 ぬーさんが情熱を捨てた所 カードを捨てる時に声で何々捨てたよ、と発言しつつカードを捨てるのですが、情熱を捨てたぬーさんという状況が面白くてみんなで笑ってしまいましたw ゲーム感覚で楽しみつつコミュニケーションが取れるのがWevox values cardのいいところですね♪ 結果発表 新しく入社されたJさんの結果はこちらです。 違いをつくる、躍動的な人生を選んでいるところにJさんの個性がとても出てると思います👍 Androidチームリーダーのumemoriさんの結果はこちら 愛と遊び心が入ってるところがとても個性的ですね💝 ぬーさんはいつも周囲をしっかりみているので、利他的が入ってるところにぬーさんらしさをとても感じます🤝 寛容と多様性と個性が入ってるところにMeilCliさんらしさが出てる気がします💭 私ことparayaは振り返ってみるとほんわかしてるの選んでる気がしますね🧚🏻‍♂️ まとめ いかがだったでしょうか? リモート期間中でもオンラインツール、今回ではWevox values card onlineを使うことで、新しいメンバーと良いコミュニケーションを取ることができました。 リモートで話す機会が少なくなることが増えてきましたが、話してみないと伝わらないことだったり、抱えてることもあるのでコミュニケーションの機会は増やしていきたいところです。 最後に クラシルではコミュニケーションを取って協力プレーができるAndroidエンジニアを絶賛大募集しています🙌 テックリード的なシニアなエンジニアはもちろん、料理好きなスキルアップしたい若手エンジニアも大歓迎です🤝 カジュアル面談も行っていますし、どういった仕事をしているのかエンジニアと話してみたいという方でもOKです。 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! お待ちしております😄 dely.jp Tweets by dely_developers twitter.com
アバター