Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Kotlin Fest 2019に参加しました

こんにちは、Androidチームの若宮 (id:D_R_1009)です。

Kotlin Fest 2019(2019年8月24日)にAndroidチームの3名(若宮、中島、隅山)で参加してきました。

kotlin.connpass.com

昨年度に参加した時のブログはこちらです。昨年に引き続き 勉強会・カンファレンス参加補助 を利用し参加させていただきました。

tech.studyplus.co.jp

当日の様子

会場が品川駅港南口方面だったので、品川駅の某所で待ち合わせて会場に入りました。 Peatixを利用したチケットの確認も大変スムーズで、待ち時間なく会場入りすることができました。スタッフの皆様に感謝。

blog.jetbrains.com

JetBrainsさんのブログの通り、会はたろうさんの挨拶から始まりSvetlana Isakovaさんの「Kotlin Fest 2019基調講演」となりました。 朝一番にこんなに面白いものを聞いてしまっていいの!? と驚きつつ、Kotlin Festが始まったんだなーと感じていたのを覚えています。

たろうさん

twitter.com

Svetlana Isakovaさん

twitter.com

当日の思い出

以下、3名それぞれの当日参加感想となります。

若宮(id:D_R_1009)

昨年参加できず、今年は参加できることが決まった日から楽しみにしていました。

KotlinはGoogle I/O17でAndroidの正式採用言語になることが決まってから触り出しているので、おおよそ2年ほど触っていることになります。 最初は varval の使い分けに混乱するほどでしたが、関数を引数とするなど徐々にKotlinに慣れ今ではなくてはならない言語となっています。

今回のKotlin FestではContractsが業務に生かせそうだな、と感じました。 アプリのモジュール化が進むにつれ、Utilクラスや拡張関数にContractsの利用するべきシーンが出てき始めたのかな、と感じています。 資料をもとに勉強し、活用していきたいと思います。

個人的な関心としては、Reactを使ったSPAの開発に取り組んでいるので、Kotlin MPPを利用したReactアプリ開発にチャレンジしてみたいなと。 Kotlin JSは発表や資料を読んでも、まだまだ開発途中っぽいなと感じました。ですが、今から開発に飛び込むとそれはそれで楽しそうだなとも!

全体的に「Kotlinを使うのは楽しいな」と感じる瞬間が多かったです。 懇親会も含めて、Kotlin Festは想像の数倍楽しかったです! また来年も参加したい!

中島

結論から言いまして、とても実りの多い体験となりました。

オープニングセッションであるSvetlana Isakovaさんの基調講演から、Kotlinの開発体制や精神だけでなく今後の新機能についてまで盛りだくさんの情報量でした。 特に、幅広いユースケースで使われそうな Contracts および ImmutableCollections について興味をそそられました。

午後からのセッションも 佐藤 隼さんの「Kotlinの型実践入門」富田健二さんの「改めて学ぶContract」 といった関連の強いものを聴講することで、より理解を深められたと感じられました。 特に Contracts は既に公式(1.3.50)拡張関数の内26メソッドに利用されているとのことで、自分でも積極的に使っていこうという気概が生まれました。

また「Kotlinの型実践入門」ではNothing/Nothing?型のユースケース、恥ずかしながら不勉強のまま放置していたジェネリクスなどについても色々勉強できたと思います。 セッションの最後にちらっと触れられていた、SAM変換問題に対する新しい型変換の開発についても、今後もKotlinの動向からは目を離せないと改めて思いました。

懇親会では普段あまりお話しできない、Android以外でKotlinを使っている方々との情報交換が色々とできてこれもとても有意義な時間だったと感じました。 Androidをやっていると静的型付けが基本ですし自分は他の経験言語でも同様だったので、型が明記されない言語からKotlinに変更した時の体験談などを聞けたのも楽しかったです。

最後に、聴講したセッション以外で特に気になったセッションが 八木俊広さんの「Kotlin コルーチンを 理解しよう 2019」 です。 資料を後から読みましたが非常に内容が濃く、自分があまり細かく理解しないで使っていたんだなと思い知らされました…。 Rxでいう通信処理のzipなど、単純な通信処理から少し外れたものをどう実装するのが本来正しいのか、しっかり把握できた気がします。

隅山

今回Kotlin Fest初参加でしたが、オープニングセッションでのSvetlana Isakovaさんの講演から始まり、全体的にKotlinの盛り上がりを感じることができました。既存のコードを壊さず、モダンに保つようアップデートとフィードバックを繰り返すことで言語をブラッシュアップしていることを知り、今後のKotlinにも非常に期待が持てました。 セッションは全体的に勉強になりましたが、特に勉強になったのが「コルーチン」と「Contract」でした。

コルーチンは業務で使っているのですが、何となくでしか理解できていないことをKotlin Festを通じて感じました。 例えば、スレッドよりコルーチンの方が軽量であることは知ってましたが、スレッドは1MB~2MBほどメモリを使うのに対してコルーチンは1KB程度のため、コルーチンを積極的に使うべきだと思いました。設計面では「コルーチンスコープとアプリケーションライフサイクルを合わせる」「suspend関数とコルーチンはメインセーフティを考慮する」と発表されていたため、今後はそこを気をつけながら開発していきます。

Contractは初見でしたが、isNullOrEmpty()でチェックした後からNonnullで扱えるようになるのはContractのおかげということがわかり、Contractの有り難さを感じました。 Kotlinでの開発中にいきなりNullableの値がNonnullで扱えるようになったりと、何気なく使えていて非常に便利である機能がContractでした。Contractは関数の振る舞いをコンパイラに適用することができるため、関数書く際はContractも考慮していきたいと思いました。

まとめとしては今回のKotlinFestを通して、自分の知識の抜け漏れを補たり、Kotlinへ熱意を感じることができたため非常にいい経験となりました。

終わりに

今年は1名がLT登壇申し込みに終わってしまいました。 来年こそは、弊社からも登壇していきたいなと感じています。

日本Kotlinユーザグループのみなさま、素敵なFestをありがとうございました!

スタディプラス第一回自作キーボードもくもく会

自作キーボードを社内で始めたきっかけ

先日、本ブログにてキーボードに関する記事「突撃!隣のキーボード Studyplus 2019」を書きましたが、その執筆の最中に社内のキーボード好きの熱が高まり、今回、有志で集まってのもくもく会の開催となりましたので、その様子をご報告します。

f:id:ksugahara08:20190827112601p:plain:w600

当日の様子

f:id:ksugahara08:20190827112843j:plain:w600

参加者は4名でしたが、それぞれキットやキーボード関連のパーツ、道具を持ち寄り自作キーボード作成や関連作業を行いました。
各メンバーが撮った写真とあわせてご紹介します。

みんなでワイワイ半田付け

f:id:ksugahara08:20190827112939j:plain:w600

アクリルプレートをレーザーカッターで切ってきた冨山氏

f:id:ksugahara08:20190827113050j:plain:w600

Let's Splitが映える!!

f:id:ksugahara08:20190827113148j:plain:w600

Lube(キースイッチの潤滑)体験コーナー

f:id:ksugahara08:20190827113231j:plain:w600

菅原さんが訳あってキースイッチのはんだを吸い取り中。全てのキースイッチを取り外す頃にははんだ吸い取りを完全に理解していたように見えました。※大石談

f:id:ksugahara08:20190827113305j:plain:w600

当日は社外から自作キーボード好きな方が参加!!!とても滑らかな打ち心地!!!
(Corne + NovelKeys Cream Switch + SPRiT Designs MX 35sのスプリングに交換)

f:id:ksugahara08:20190827113531j:plain:w600

参加者のコメント

  • 大石(id:k_oishi) 前回作成したTreadstone32のスイッチの交換とPlanckを冨山さんに譲るためにキースイッチを外してパーツの状態に戻す作業をしました。
    前回、Treadstone32で難しいはんだ部分をがんばった図は以下のとおりです。USBコネクタ部分はさらに難しいですが、こちらもなんとか成功しました。

f:id:ksugahara08:20190827113338j:plain:w600

とりあえず完成したTreadstone32 スイッチはRosélios(67g)とSakurios(62g)の組み合わせ

f:id:ksugahara08:20190827113632j:plain:w600

キーキャップはEnjoypbt GrayScale keycaps set

f:id:ksugahara08:20190827114131j:plain:w600

各自、異なる作業をしていましたが、共通の話題でわいわいしたり、アドバイスをしたりされたりと、すごく盛り上がってよかったです。
積みキットやキーボードの引き取り先を探すにもちょうど良いイベントと思いました。
SPRiT Designsの交換用スプリングが大量にありますので、次回はスプリング交換もやってみたい所存です。

f:id:ksugahara08:20190827114208j:plain:w600

  • 菅原(id:ksugahara08) Slackのチャンネルでは大石さんに色々な情報を教えて頂いて順調に沼に沈んでます!!
      ズブブブ…うわっ、あっ、うわぁぁあっぁ…
        ....::::;;;;( ;・ω・);;;;::::......
    今回は遊舎工房さんでWoody Zincを一目惚れで購入し、もくもく会に参加、いや企画!!
    無心になってはんだ付けしていると時間を忘れるくらい没頭してしまいました。
    まだ未完成なので次回には完成させたいと思います!!

  • 冨山(id:atomiyama) 初自作キーボードのLet's Splitがついに完成.PCBの注文からケースのカットまで全部やってみてなんとか完成しました!!!!!(嬉しい)
    あと譲り受けたPlanckも組んで完成!!!!
    ただ完成した直ぐそばからケースを作り直したい欲に駆られています。

  • 山﨑
    非エンジニアのド素人。
    どれくらいド素人かというと
    職種→しがない総務労務担当っす(・∀・)
    ダイオード→なんか懐かしいやつだ!…何だっけ?(・∀・)ダイオー…チョ、モウイッカイ
    ハンダゴテ→中学生ぶりにみたー(・∀・)スゲースゲー
    というレベル。
    約3カ月前にキーボードが手作りできることを知り、ZINCが可愛かったので取り敢えず沼に片足突っ込んでみることを前日に決意。
    この日は都合により13:30〜15:30までの作業。昨日の今日で開始だし、道具も部品も全く揃っていないので、今日できる作業を取り敢えずやるという感じ。
    やったこと→ケースの組み立て、プレートにダイオードをはめて固定
    教えてもらったブログに「マスキングテープで固定するとよいよー。」と書いてあったので、途中マスキングテープを買いに行ったが、可愛いマスキングテープがたくさんあり迷ったため時間を食う。
    本日の収穫

    • 大師匠・大石さんは道具を見ただけでamaz●nの限定品とわかるくらいのマニアっぷりである。
    • キースイッチのバネの強度を変えることも可能らしい。
    • 部品揃えないといけない。
    • なんか楽しい(・∀・)

f:id:ksugahara08:20190827114355j:plain:w600 f:id:ksugahara08:20190827114429j:plain:w600 f:id:ksugahara08:20190827114458j:plain:w600

最後に

参加者全員でもくもくしつつ、時にはキーボードに関する話をしながら作業していると、時間が経つのがあっという間でした。
普段の業務では一緒に仕事をしたことがないメンバーもいましたので、そういう意味でも良い機会になりました。
途中で遊舎工房へキースイッチなどの部品を購入しにいくメンバーもいましたが、御茶ノ水という比較的近い立地なのが素晴らしいですね。

今回、参加メンバーではなかった人からも興味を持ってもらえたようなので、参加者が増えると嬉しいです。
次回の自作キーボードもくもく会を開催予定ですので、その時はまたレポートしたいと思います。

スタディプラスを支えるインフラ技術

はじめに

スタディプラスには学習管理SNS「Studyplus」と教育機関向け学習管理サービス「Studyplus for School」の2つのサービスがあります。

今回は、社内では「本体」と呼ばれている「Studyplus」のAPIシステムである、コードネーム「steak」を中心にしたインフラ環境を紹介します。

構成

Studyplus本体は「steak」を中心とした複数のサブシステムで構成されており、関連するサブシステムをVPCで区切って管理しています。 「steak」を始めとした各サブシステムはRuby on Rails + Pumaで運用しています。

f:id:yo-shimada:20190821144527p:plain
サーバー構成の概要図

利用中の主なAWSのサービスは以下になります。

  • EC2
  • RDS
    • Aurora
    • MySQL
  • ElastiCache
  • S3
  • CloudSearch
  • Athena

構成管理 

基本的にインフラ関連の設定はコード化するようにしています。 スタディプラスでは構成管理には主にPackerとAnsibleを利用しております。

Packer+AnsibleでAMIを作成してEC2のサーバーを構築する際に利用します。 AWSサービスの構成管理やEC2の環境構築にもAnsibleを利用しています。

また、サイロ化を防ぐため開発エンジニアが各自実行できるようにしています。

環境

環境を以下の3つに分けて運用しています。

  • cage:開発者全員が共有する開発環境
  • stag:本番DBに接続しており、主にリリース前にアプリケーションの最終チェックをする環境
  • prod:本番環境

CI/CD

CIはCircleCIを利用しており、以下のような運用となっています。

  1. エンジニアがPull Requestの作成
  2. CircleCIにテスト
  3. レビューを経て、エンジニアがmasterブランチにマージ

デプロイに関してはSlackからHubotを経由してJenkinsでデプロイをしています。

  1. SalckでHubotにコマンドと変数を受け渡す
  2. JenkinsのJobを実行して対象にデプロイを行う

開発エンジニアの要望もあり、テスト、build、deployのパイプラインはこのような形になっています。

blue/greenデプロイ形式に則っており、リリース規模や影響度を見てカナリアリリースを行うこともあります。

f:id:yo-shimada:20190819183422p:plain
デプロイイメージ図

監視・検知

  • サーバーのメトリクス監視にはMackerelを利用しています
  • アプリケーションのエラー検知にはSentryを利用しています
  • ログ収集には、S3に保存してAthenaで確認する方法と、Amazon Elasticsearch Serviceを利用してKibanaで確認する方法を採用しています
  • OnCallはMackerelにTwilioを設定して、担当者に連絡が飛ぶようになっています

改善・挑戦

以下は、今年取り組んだ改善と今後1年以内に取り組んでいきたいと考えているインフラ関連の活動内容です。

  • 今年取り組んだ改善活動
    • RDS監視強化(パフォーマンスインサイトによるボトルネックの可視化)
    • CircleCIのPerformance Planを導入
    • 複数システムのRubyとRailsのバージョンを最新バージョンへ更新
    • jemallocの導入(Railsアプリケーションのメモリ上昇を抑えるため)
  • これから取り組みたい事
    • Cloud Native化
      • EKS等のコンテナ導入
      • サーバーレス化
    • SREチームとしての活動推進
      • 監視・ログ基盤の整備
      • ポストモーテムの整備
      • SLI/SLOの設定

現状では、まだ手作業や自動化する余地があり、安定したサービスを提供するために出来ることはあります。 その中で運用負荷を軽減する事を考えたり、モダンな思想・ツールを取り入れたりしています。

Studyplus for Schoolをリニューアルしました

こんにちは、こんばんは、For School事業部のid:atomiyamaです。 先日フルリニューアルされたStudyplus for Schoolのサーバーサイド開発、フロントエンド開発の一部を担当しました。

f:id:atomiyama:20190805152336p:plain
新しいStudyplus for School

f:id:atomiyama:20190805152528p:plain
以前のStudyplus for School

リニューアルに至った理由

  • 画面読込速度の改善
    これまでのStudyplus for Schoolは基本的にRuby on Rails(Slim)で実装されており、複雑な処理が実行されるslimでは表現できないような部分のみreact_on_railsでReactコンポーネントをマウントするような実装になっていました。
    また主要な機能の多くのページに実装されている絞り込み条件設定や、表示期間変更などでは変更されるたびに画面全体が再描画されていました。
    これらの問題を解決するために今回のリニューアルでSlimから完全に脱却してReact+Reduxを導入することになりました。

  • デザインの刷新
    以前のStudyplus for Schoolが開発されたときは専任のデザイナーがいなかったため、外注で依頼したデザインをもとに実装されていました。
    しかし、 昨年から弊チームに専任のデザイナーが配属されたため、デザインシステムをはじめ今後の新規機能開発、改善をよりスピーディに行えるようデザインを刷新しました。

新たに導入したもの

上にあるように今回これまでのslim+react_on_railsから脱却し、React+ReduxSPAを実装しました。
既存のRailsはAPIサーバーとして活用し、ReactとJSON APIでデータをやり取りするようになり、変更があればコンポーネント単位で再描画が行われるようになり、ユーザーの待ち時間は大幅に削減されました。
デザインでも AtomicDesignに準拠して再利用可能なコンポーネントを実装、Storybookでの管理を行いそれらを組み合わせてページを組み上げる形で実装を進めていきました。
統一されたパーツの集合でページが構成されているのでユーザーもページごとに異なる印象をあまり持つことがなくサイト上のどのページに行っても同じ体験をできるようになったと感じています。
また、既存のRailsはAPIサーバーという役割に変更されたため認証まわりにも変更を行いOpenID Connectを新たに導入しました。
Studyplus for Schoolには今回リニューアルされたアプリケーションの他に入退室管理を提供するElmで実装されたアプリケーションがあるため、それらの認証を全てまとめる目的で今回OpenID Connectを選択しました。

バックエンドのRails APIサーバーからはJSON APIを提供するように変更されたため、OpenAPI3でAPIの仕様を記述し開発段階ではモックのみをはじめは提供しクライアント側の開発が終わってから本実装を行うような形を取りました。これは私があまりフロントエンドの実装に慣れていなくて見通しが立たなかったのに比べて、Railsであれば比較的見通しがたったことが大きな理由です。(最終的にはチーム全体でこのフローで開発が進んでいましたが)

以上のような技術が今回のリニューアルで導入され、クライアントの部分とサーバーの部分がAPIを境に切り離されたため、今後デザインの改善や機能の改善も全体的に行いやすくなりました。

これからのStudyplus for School

今回のリニューアルは実装に7ヶ月という長い時間を費やしました(計画も合わせると1年弱)。チームメンバーと技術選定を行ったり、計画通りに開発が進まずに開発プロセス改善を試行錯誤したり、Reactに馴染みのない自分がいち早く習得できるようにチームメンバーにペアプログラミングを実施してもらったりと色々と大変なことがありましたが無事リニューアルを終えることができました。

今回システム全体を洗い直したことで新たに技術的な負債が浮き彫りになったり、また今回のリニューアルの中で発生した技術的負債もあります。
今後もそれらと向き合ってユーザー目線で、より良い体験を届けていくためにどんどん新しい技術の導入に積極的に挑戦していきたいと思っています。

DatabaseView(Room 2.1)による本棚並べ替え機能リリースについて

こんにちは、Androidチームの若宮(id:D_R_1009)です。

スタディプラスのAndorid版にて、5月半ばより不具合の発生していた「本棚」機能を7月頭に修正したしました。 ご不便、ご迷惑をおかけしましたこと大変申し訳なく思っております。

「本棚」の不具合においては、2つの問題点がありました。

  1. Room DB の allowOnMainThread 指定による、DBファイルの破損問題
  2. 「本棚」内の並び順と、本棚に追加する「教材」の関係性が密すぎる問題

今回はRoom 2.1より追加された DatabaseView を活用し、2つ目の問題を解決しましたので、その経験をまとめたいと思います。

developer.android.com

DatabaseView とは

Room 2.1より追加された、複数のTableから1つのクラスを作る仕組みです。

docs.google.com

2.0までは、あるTableとTableを1対1で対応させるためには、下記のような方法が必要でした。

@Entity(tableName = "parent")
class ParentEntity {

    @Embedded
    lateinit var child1 : ChildClassA

    @Embedded
    lateinit var child2 : ChildClassB
}

@Dao
interface SampleDao {
    @Query("SELECT * FROM child_class_a, child_class_b where child_class_a.id = child_class_b.id_class_a")
    fun getParentEntityList(): LiveData<List<ParentEntity>>
}

stackoverflow.com

対し、DataBaseView ではSQLのInner JoinまたはOuter Joinを利用してEntityを作ることができます。

@DatabaseView(
    viewName = "parent",
    value = "SELECT * FROM child_class_a INNER JOIN child_class_b ON child_class_a.id = child_class_b.id_class_a"
)
data class ParentEntity(

    @Embedded
    val child1: ChildClassA,
    @Embedded
    val child2: ChildClassB

)

@Dao
interface SampleDao {
    @Query("SELECT * FROM parent")
    fun getParentEntityList(): LiveData<List<ParentEntity>>
}

DatabaseView により、旧来の方法に比べて下記の点が便利になっていると感じます。

  1. Inner JoinかOuter Joinかを選ぶことができるので、(View層のために)生成したいクラスのNullableがコントロールしやすくなった
  2. lateinit var では実行時のエラーによる検知しかなかったが、DatabaseView ではコンパイル時の検知が可能になっている
  3. val でフィールドを定義できる

今回解決するべき問題

本棚の並べ替えにおいては、教材並び順 の2つの要素を組み合わせていきます。

歴史的な経緯により 教材 はユーザー情報に紐づけられてサーバー上に保存されていますが、 並び順 は端末ローカルにしか存在しません。 このため 教材 の追加/修正/削除時には 教材並び順 の2つのテーブルを更新し、 教材 を並び替えた場合には 並び順 のテーブルのみを更新する必要があります。

また各 教材 には カテゴリー が紐づけられています。この カテゴリー は例えば「英語」や「数学」などの 教材 を本棚内で管理するための概念です。 もちろん、本棚内で カテゴリー の並べ替えを行うことができるため、 カテゴリー に対応する 並び順 が存在します。

教材の並べ替え カテゴリーの並べ替え
f:id:D_R_1009:20190722181453g:plain f:id:D_R_1009:20190722181519g:plain

全体の構成図

f:id:D_R_1009:20190723153124p:plain

アプリはAndroid Architecture Componentsを利用したMVVMアーキテクチャを採用しています。 昨年12月ごろは導入半ばといったところでしたが、最近はほとんどViewModelによるビジネスロジックの切り離しが進んでいます。

tech.studyplus.co.jp

tech.studyplus.co.jp

DatabaseView の便利なところ、注意した方が良いところ

DatabaseView の対象クラスに対して、@Insert@Update することはできません。 必ず、その構成している要素の各Tableに対して更新を行う必要があります。

一方で各Tableに対する更新が、対象クラスへの更新通知となります。 このため 並び順 の更新を行うと、 教材並び順 を組み合わせたクラスへの変更通知となります。結果として LiveData<List<SortedMaterial>> のようにDBからリストを購読していれば、 並び順 の更新後にUIへ変更を伝えることができます。

教材並び順 の親の関係となるため 並び順 の外部キーとして 教材 を指定しました。 このためInsert時のConflictStrategyに REPLACE を指定してしまうと、 教材 の更新をするたびに 並び順 が破棄されてしまうようになります。 DatabaseView では外部キー制約を考えることも多くなると思われるので、あらかじめ @Insert@Update を使い分けておくのが良さそうです。

設計上の工夫点

  1. View層にはDB層の結果だけを表示させる
    • RecyclerViewに渡すListはDaoから取得するListに限定
    • DBの 並べ替え テーブルの更新結果をView層が受け取り、Groupieのupdateメソッドによる並べ替えを行う
    • 各種のソート処理はView層で実行させない
  2. ViewModel層でView層とDB層(リポジトリ層)との非同期処理の調整を実施
    • DB層はKotlin CoroutinesによるDBアクセス、View層はLiveDataによるUI更新となるため、操作対象となるListはViewModelがハンドリング
    • LiveData.getValue() により CachedList を取得、View層から取得したPositionより並べ替えされた順序リストを作成
  3. DB層は @Update メソッドにより 並び順 テーブルのみを更新
    • Roomの操作をKotlin Coroutinesにより実施させる
    • @Update メソッドの処理はArchitecture Components の I/O Dispatcherで実施されるため、呼び出し側でいじらない
    • 適切に Index を貼ることで、 DBから 教材 を取得する速度を担保する

まとめ

Room 2.1の DatabaseViewLiveDataRecyclerView を組み合わせることによる並べ替え機能について紹介しました。 結果として冒頭であげた1つ目の問題、 UIスレッド上でDB内の並べ替えを(長時間)行う ことによるローカルDBの破損問題にも対応することができ、アプリの安定性に寄与できたかなと思います。

引き続きRoomによるアプリの改善に取り組んでいければ、と考えています!

Navigation で DialogFragment を表示した話

こんにちは、Studyplus Androidチームの中島です。

2019年6月より、Studyplus AndroidアプリはTarget SDK28への対応、合わせてAndroidXへの移行を行いました。

今まで「AndroidXに対応したらここ直しましょう」としていたところをガシガシ直していくのは楽しかったです。

閑話休題、今回のブログではAACのNavigationで DialogFragment を表示した話をしたいと思います。

developer.android.com

はじめに

はっきり言っておきますが、この DialogFragment の表示方は賞味期限が短いです。 なぜかと言いますと、DialogFragment は Navigation 2.1.0-alpha03 から公式にサポートされており、2.1.0安定板が公開された時に意味をなくすからです。

公式より抜粋

<dialog
    android:id="@+id/my_dialog_fragment"
    android:name="androidx.navigation.myapp.MyDialogFragment">
    <argument android:name="myarg" android:defaultValue="@null" />
        <action
            android:id="@+id/myaction"
            app:destination="@+id/another_destination"/>
</dialog>

developer.android.com

Studyplus Androidチームでは一応beta以上になってから採用しようという話をしていたことから、 コーディング当時(7/12)alpha06だった2.1.0の採用を見送っておりました。

(ちなみに、7/17に2.1.0-beta01、7/19に2.1.0-beta02がリリースされております…)

リファクタリング対応中のクラス群にDialogFragmentがあったのですが、他Fragmentとのデータのやり取りがかなり煩雑な状態でした。 そのため、ActivityレベルのViewModelでデータを保持しつつ、Navigationによる遷移に組み込んでしまいたいなと思ったのが今回の発端です。

NavigationはFragmentの画面遷移を視覚的にデザインできるようにした、AACの一機能です。

この節では基本となる実装を簡単に説明します。

Navigationによる画面遷移はxmlで指定します。 (この<navigation />タグでくくったxmlのことを以下navGraphとします) GUIでいじれるのでかなりやりやすいように感じました。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="sample.navigation.app.HomeFragment"
        android:label="HomeFragment" >
        <action
            android:id="@+id/action_homeFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        android:name="sample.navigation.app.SecondFragment"
        android:label="SecondFragment" />
</navigation>

次に遷移を実装したいActivityにnav_host_fragmentを配置して、app:navGraphのattributeにnavGraphのidを指定します。

<!-- activity_home.xml -->

<fragment
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/navigation_graph" />

あとはHomeFragmentからSecondFragmentに遷移させたいところで、actionを実行させるコードを書けばOKです。

// HomeFragment.kt

findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToSecondFragment())

ここでは説明しませんがAnimationを付与する、Argumentを渡す、DeepLinkによるFragment指定遷移なども可能です。

詳しいことは公式をご参照ください。

また、7/18に行われたGDC Tokyo主催の勉強会資料もとても参考になります。

speakerdeck.com

本題です。

前述の通り、Navigation2.0.0ではDialogFragmentに対応していないので対処法を考えなければいけません。 調査の結果、STAR-ZEROさんのブログ: Navigation + DialogFragmentを拝見し、参考にさせていただきました。

medium.com

結論として、やることは以下のような形になります。

  1. DialogFragment専用のCustomNavigatorを実装する
  2. CustomNavigatorで指定したhtmlタグを使い、navGraph内にDialogFragmentを定義する
  3. xml内で app:navGraph attributeを定義しないようにする
  4. NavHostFragmentを持つActivityのコードでNavigationProviderにCustomNavigatorを追加する
  5. CustomNavigatorの追加後、NavHostFragmentにnavGraphをセットする

基本的にはその通りにやればいけるはずだと思ったのですが、STAR-ZEROさんの記事ではNavigationのバージョンが1.0.0-alpha06だったため、一部変更が必要でした。

その変更が必要だった部分、CustomNavigatorについて詳しく説明したいと思います。

CustomNavigatorの実装

NavigatorとはNavigationにおけるactionの内容を実装するabstract classです。 @Navigator.Name("tag")で指定したタグを使ってnavGraphにFragmentを定義することにより、そのNavigatorを使って遷移させることができます。

DialogFragment専用のCustomNavigatorは、STAR-ZEROさんのものを基にして実装した結果、以下のようになりました。

@Navigator.Name("dialog-fragment")
class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
) : Navigator<DialogFragmentNavigator.DialogDestination>() {

    override fun navigate(
        destination: DialogDestination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? {
        val fragment = destination.createFragment(args)
        fragment.show(manager, TAG)
        
        // 注1. dispatchOnNavigatorNavigatedというAPIが削除されていたこと、返り値に NavDestination が追加されていたので変更
        return destination
    }

    override fun createDestination() = DialogDestination(this)

    // 注2. Dialogを閉じる処理を呼び出すように変更
    override fun popBackStack(): Boolean {
        val existingFragment = manager
            .findFragmentByTag(TAG)
        if (existingFragment != null) {
            (existingFragment as DialogFragment).dismiss()
        }
        return true
    }

    class DialogDestination(navigator: DialogFragmentNavigator) : NavDestination(navigator) {
        // 変更なしのため省略
    }

    companion object {
        private const val TAG = "navigation_dialog"
    }
}

navGragh 内で <dialog-fragment /> タグを使って定義することでこのCustomNavigatorが使われます。

    <dialog-fragment
        android:id="@+id/MyDialogFragment"
        android:name="sample.navigation.app.MyDialogFragment"
        android:label="MyDialogFragment">
    </dialog-fragment>

変更点について、実装順に説明していきます。

DialogFragmentなので、(DialogDestinationクラスで作成したインスタンスを)show()メソッドで表示します。 2.0.0では一部APIが削除されていたこと、返り値としてNavDestinationが追加されていたのでその部分を変更しています( 注1 )。

DialogFragmentの表示まではこれでできましたが、ここで問題が起きました。 バックキーでDialogFragmentを閉じてもう一度開こうとした場合にクラッシュしてしまったのです。

java.lang.IllegalArgumentException: navigation destination {actionのid} is unknown to this NavController

調査してみたところ、DialogFragmentをバックキーで閉じた場合は、Navigation内部でFragmentの切り替わりが認識されていませんでした。 その結果「DialogFragmentにそんなaction(dialogを開くaction)はないよ」ということでクラッシュしていたようです。

これについてはNavControllerOnDestinationChangedListenerを設定してみるとわかります。 バックキーでDialogFragmentを閉じても、以下のリスナーが呼ばれません。

navController.addOnDestinationChangedListener { controller, destination, arguments ->
    // destination(*Navigation*に設定されたFragment)が切り替わった時に呼ばれるリスナー
}
popBackStack()でバックキーを押した時の挙動を指定する

ではどうするのかということで色々調べたのですが解決策が見つからず、結局Navigation2.1.0-alpha06の実装コードを確認しました…

// androidx.navigation.fragment.DialogFragmentNavigator
@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination> {

// ~~~~~~~~

    @Override
    public boolean popBackStack() {
        if (mDialogCount == 0) {
            return false;
        }
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
                    + " saved its state");
            return false;
        }
        Fragment existingFragment = mFragmentManager
                .findFragmentByTag(DIALOG_TAG + --mDialogCount);
        if (existingFragment != null) {
            existingFragment.getLifecycle().removeObserver(mObserver);
            ((DialogFragment) existingFragment).dismiss();
        }
        return true;
    }

// ~~~~~~~~

}

「なるほど、dismiss()をここで呼ぶんだな!」 ということで 注2 の部分を実装するに至りました。

(画面回転や、開かれている個数のカウントなどの処理もありましたが仕様上必要ないと判断しオミットしています)

これで実装は完了したと思いましたが、再び同じクラッシュが起きてしまいました…

java.lang.IllegalArgumentException: navigation destination {actionのid} is unknown to this NavController

DialogFragmentでdismiss時にNavigationのnavigateUp()を実行する

NavControllerの方の popBackStack() などにもやはり変更があったことを確認したのですが、こちらまでカスタマイズとなると現実的ではありません。

要はバックキーによるDialogFragmentのcancel動作と、Navigation内部の階層を戻す動作が連携していないのが原因であると考え、以下のように実装しました。

// MyDialogFragment

override fun onCancel(dialog: DialogInterface) {
    super.onCancel(dialog)
    try {
        findNavController().navigateUp()
    } catch (e: IllegalStateException) {
        // NavHostFragment内以外で呼ぶとthrowされるので汎用的なDialogFragmentでは避けた方が無難
    }
}

navigateUp()Navigation内の階層を一つ戻る挙動をさせるメソッドなので、onCancel()で呼び出すことにより連携させられると考えた結果でした。

(結果的にこの処理があれば 注2 の部分がなくても挙動としては正しくなったのですが、公式のコードで行われていた処理であることを考慮して残しています)

しかしまだ終わりではありません。

DialogFragmentからの遷移actionにpopUpTopopUpToInclusiveを設定する

DialogFragmentから次のFragmentに遷移した場合、その後にバックキーを押したら通常はDialogFragmentを開いたFragmentに戻るかと思います。 つまり、DialogFragmentを含めて階層が二つ戻っていることになります。

しかし、今のままだとNavigation内部では階層が一つしか戻っていないため、齟齬が生じてしまっています。 これを回避するためには、DialogFragmentのactionに 戻る場合は自分を開いたFragmentまで戻す 指定をしなければいけません。

    <dialog-fragment
        android:id="@+id/MyDialogFragment"
        android:name="sample.navigation.app.MyDialogFragment"
        android:label="MyDialogFragment">
        <action
            android:id="@+id/action_to_second"
            app:destination="@id/SecondFragment"
            app:popUpTo="@id/MyDialogFragment"
            app:popUpToInclusive="true" />
    </dialog-fragment>

app:popUpTo attributeは、このactionの後に戻った場合どこまで戻すかを指定できます。 また、app:popUpToInclusive attributeは、trueにすることでapp:popUpToに指定したFragmentの一つ前まで戻すことができます。 app:popUpToにDialogFragment自身を、app:popUpToInclusivetrueをそれぞれ指定することで、自分の一つ前まで戻す指定としました。

ここまで実装してようやく期待通りの結果が得られました。

まとめ

Navigationは、Android開発で厄介なものの一つだったFragment同士の遷移を、直感的かつ安全に実装できるライブラリです。 まだリリースされて比較的日が浅いせいか、LiveDataやViewModelに比べてサンプルケースが少なく手を出しづらいかもしれません。 ですが、実際に触ってみるととてもわかりやすく便利なものだということが実感できました。 BottomNavigationViewなどにも簡単に接続できますので、Fragment遷移を行う際に試してみてはいかがでしょうか。

今回の話は冒頭でも述べたように長く使えるものではありませんでしたが、Navigationの理解が深められたと思えばなかなか有意義であったと思っています。 DialogFragmentがサポートされた以上、CustomNavigatorを作成する機会はあまりないかもしれませんが、今後にもこの知見を役立てていければと思っています。

突撃!隣のキーボード Studyplus 2019

こんにちは。今年の5月に入社したiOSエンジニアの大石(id:k_oishi)です。

私は入社時のオリエンテーション終了後、最初にしたことが社内のSlackでキーボードチャンネルの検索という程度にキーボードが好きな者です。
当然ではありますが、以前から他のエンジニアがどのようなキーボードを使っているのか気になっていました。
そこで今回は弊社のエンジニアに社内で使用しているキーボードやこだわりなどを聞いてみました。

現在使用しているキーボード

エンジニアは入社時に希望するPCやスペックを選択することができますが、現在は全員がMacBook Proを使用しています。
まずはオフィスでどのようなキーボードを使用しているか聞いてみました。

f:id:k_oishi:20190711145812p:plain

やはりMacBook Proにビルドインされているキーボードを使っている人が多く、それ以外のキーボードを使っている人は少数派であることがわかりました。
また、外部キーボードではHHKBを選ぶ人が多いので、やはり定番としての人気の高さがうかがえます。

MacBook Proのキーボードを使っている人のコメントはこちら

  • とにかくデフォルトやデファクトスタンダードを好む
  • あえてあまりこだわりを持たないようにしています
  • あまりこだわりを持たないというこだわり
  • ワイヤレスは信用できない病気なので使わないです
  • 机の上にキーボードは1個に留めたい。Macのキーボードが取り替えられるなら別のキーボードを使いたい
  • 最近はMBPのキーボードに慣れ過ぎてしまって英語配列でストローク浅めであれば特にこだわりないです

キー配列

キー配列にもこだわりがあるはず!と思い、聞いてみました。

f:id:k_oishi:20190711145815p:plain

日本語配列を使っている人が多かったです。
Dvorak配列を使っている人もいました。

キー配列に関するコメントはこちら

  • 小さい頃から日本語配列を使って慣れていたので日本語配列です
  • Enterキーが大きい形に慣れてるので日本語配列。小さめのキーボードなどでたまにあるただの長方形エンターキーだと辛いです
  • 日本語配列の英数キーをメタキーにしてるので日本語配列を使っている(Karabiner-Elementsを使ってキーバインドをかなり変えている
  • 自作キーボードで配列をカスタマイズしているがベースは英語配列
  • 英語配列信者です

それ以外のキーボードを使っているエンジニア

MacBook Pro以外のキーボードを使っているエンジニアに突撃しました。

サーバーサイドエンジニア 田口(id:tagucch)

f:id:k_oishi:20190711151132j:plain

  • HHKB Professional 2 無刻印
    • Karabiner-ElementsでDvorak配列に変更
  • 予備 Mistel BAROCCO MD650L(Cherry ML Switch ML1A)

  • 本人コメント
    (なるべく)静電容量無接点方式が良いです。

サーバーサイドエンジニア 花井(id:hiroyuki-hanai)

f:id:k_oishi:20190711151128j:plain

  • HHKB Lite2(英語配列)
  • トラックパッドはMBPを使用
  • キーボード近くにiPad miniも設置

  • 本人コメント
    以前所属していたエンジニアのお下がりです。

サーバーサイドエンジニア 冨山(id:atomiyama)

f:id:k_oishi:20190711151116j:plain

  • Mistel BAROCCO MD650L(Cherry ML Switch ML1A)
  • Kensington SlimBlade Trackball
  • 作成中のLet's Split(自作キーボード)

    • キーキャップ MDA Big Bang
  • 本人コメント
    Let's Splitはキースイッチも購入済みなのであとは組み立てるだけです。

Androidエンジニア 若宮(id:D_R_1009)

f:id:k_oishi:20190711151124j:plain

  • Apple Magic Keyboard(日本語配列)
  • Apple Magic Trackpad 2

  • 本人コメント
    本当は深いキーボードが好きなので、少し古めのThinkPad keyboardが好みです。

サーバーサイドエンジニア 石上(id:shgam)

f:id:k_oishi:20190711151111j:plain

  • HHKB Lite2(日本語配列)

  • 本人コメント
    ライトな感じです。

iOSエンジニア 大石(id:k_oishi)

f:id:k_oishi:20190712112616j:plain

  • Rhymestone
    • 分割型 自作キーボード
    • キーキャップ XDA 8bit
    • キースイッチ Orange Healios, Sakurios(ともにリニア静音タイプ) 潤滑(Lube)済み
  • Apple Magic Trackpad 2
    • 右手で操作するので右側に傾けると操作しやすいことを発見したので実践中
  • パームレスト FILCO Majestouch Wrist Rest "Macaron" 薄型12mm

  • 本人コメント
    キーボードはスイッチの感触が大事! 現在40%キーボードを使っているので、30%キーボードにチャレンジしたいです。

次に使ってみたいキーボードは?

f:id:k_oishi:20190711145809p:plain

現状のキーボードに満足している人もそれなりにいますが、次に使ってみたいキーボードがあると答えた人のコメントです。

  • HHKB
  • 自宅にある使っていないHHKB Professional BT
  • Mint60(自作キーボード)
  • メカニカルキーボード
  • 以前SteelSeriesのキーボードを使っていてすごい丈夫で打鍵感もよかったので買い換えるとしたらSteelSeriesの新しいキーボード
  • ErgoDox
  • Let's Split(自作キーボード)
  • 左右分割型の自作キーボードを作ってみたい
  • キットを積んでいる自作キーボード
  • 音声入力

私以外にも自作キーボード勢が何人かいることがわかりました 。😀
HHKBも一定の人気がありますね。
また、音声入力という回答には未来を感じました。10年後?には実現するくらいの科学の進歩に期待したいです。

自作キーボードに興味ありますか?

最後の質問です!
最近巷で流行っている自作キーボードに興味があるか聞いてみました。

f:id:k_oishi:20190711145754p:plain

思ったより興味を持っている人が多かったです!
興味があると答えたメンバーのコメントはこちらです。

  • ある。キーボードというより、工作することに興味があるので、キーボードでもなんでもいい
  • 作業用としては興味なし。スマートフォンに繋ぐ用とかであれば興味あり
  • ゲームによっては相性がよさそうなのでやってみたさはあります
  • 興味はあるが、無限沼の気配がするのと作成途中に部品を紛失する自信がある
  • 使うかどうかは置いといて自作することには興味がある
  • あります! Let's Split作成中
  • 自作キーボードを作る社内クラブの活動を行いたい

いかがでしたか?

やはりキーボードにこだわりを持っている人が多く、エンジニアにとってキーボードは切っても切れない存在であることがわかりました。
今回、この記事を通じてあまり関わりのなかったメンバーともコミュニケーションすることが出来たのは個人的によかったかなと思います。
機会があれば1年後くらいにまた調査してみたいですね。

そして、なんと弊社でも自作キーボードもくもく会の機運が高まっています。

f:id:k_oishi:20190711144939p:plain

次回はその模様をレポートしたいと思います。