Studyplus Engineering Blog

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

リモートチームでスクラム開発

こんにちは、ForSchool事業部の島田です。好きな漫画は「王様達のヴァイキング」です。

スタディプラス社では、現在リモートでの開発が主体となっています。その状況の中でStudyplus for School(以下FS)開発チームはスクラムによる開発を進めています。

今回はFS開発チームのリモート環境下における開発プロセスを紹介したいと思います。

(ちなみに、Studyplusのサーバーサイドチームについてはこちらの記事を参照していただければと思います)

tech.studyplus.co.jp

開発プロセスについて

以下が、FS開発チームのスクラムの概要です。

  • スプリント期間:2週間
  • デイリースクラム:毎日30分
  • スプリントプランニング:隔週2時間
  • スプリントレビュー:隔週2時間
  • スプリントレトロスペクティブ:隔週2時間

デイリースクラム

スプリントゴールの達成に対して課題・障害がないかを確認するミーティングです。 スプリントバッグログの進行具合を中心にして、チームがその日に取り組んだ事を確認していきます。その中でこのまま開発を進めて計画どおりに行かないと考えられる場合は課題を明確にして(別のミーティングを設けるなどして)解決策を考え、早い段階で軌道修正できるようにしています。

スプリントプランニング

スプリント期間の最初に行われる、スプリントの作業を計画するイベントです。

スプリントで実現するバックログの項目を選択してスプリントで実現するタスクにしていきます。

選択する項目は、機能開発や技術的な改善も含まれています。それらを具体的にどう実現していくかをタスク化して見積もります。

以下のステップで進めています。

  1. スプリントバックログの優先度を決める
  2. バックログの見積もりをする
  3. これまでのベロシティを参考にしてスプリントゴールを決める

スプリントレビュー

スプリントの最後に行われるスプリントの成果物をレビューするイベントです。プロダクトオーナー、開発者を中心にして、レビューをしてフィードバックをしていきます。その結果により新たなバックログが追加されたりします。

  1. スプリントの成果物のレビュー・デモ
  2. レビューに応じて、必要ならば新たなバックログを追加

スプリントレトロスペクティブ

スプリントレビューの後に、チームをより良くするために改善策を話し合うイベントです。 スプリントを振り返り、ゴールの達成度(ベロシティはどうだったか)を確認します。発生した課題の解決策やチームをより良くするための改善案ついて話したりします。 FS開発チームでは話し合いの際に、Lean Coffeeのやり方を参考にしてすすめています。以前はKPTで進めていました。しかし各自の課題感が発散した場合に議論の優先度を付けるのが難しくなり、本当に話し合うべき事を話す時間が足りなくなる事も少なくありませんでした。このやり方と後述するScatterSpokeというツールの相性もあり、チーム全体で重要だと思われる課題から優先的に話していけるようになり、ミーティングの質が向上したと感じました。

  1. Brainstorm(5分)
    1. 各自がスプリントで気になったトピックを記載する
    2. 各自二票まで全員があげたトピックの中で気になったトピックへ投票する
  2. To Discuss
    1. 投票数があったトピックをピックアップする
    2. 投票数が多い順に議論を始める
  3. Discussing
    1. 7分議論する
    2. 時間が経過したら、このまま継続して議論するかを親指のサインで投票する
      1. 継続の意思が過半数以上の場合-> 4分議論、過半数に満たない場合 -> 2分議論
  4. Discussed
    1. 全員がこれ以上の議論が必要ないとなったら、当該トピックの議論を終える
    2. 必要に応じて改善アクションを決める

以上が、FS開発チームのスプリントイベントの内容になります。

ツールについて

FS開発チームで利用しているツールについて紹介したいと思います。

Slack

ほぼ全てのやりとりはチャット上でやります

Zoom

定例のミーティングは主にZoomを利用しています。サクッと話したい場合にはSlack Callなども使います。

monday.com

バックログ・タスクの管理ではmonday.comを利用しています。詳しいツールの説明についてはこちらの記事を参照していただければと思います。

tech.studyplus.co.jp

esa

仕様や議事録などesaで管理しています。必要に応じてGoogelスプレッドシートを利用したりもします。

Scrum Poker Online

スプリントプランニングの見積もりで利用しています。オンラインでプラニングポーカーが出来るツールは数多くありますが必要最小限の事が実現できるので重宝しています。以前はSlack上で見積もりの数を出すなどもしていたのですが、やはりタイミングや一覧性などもあり課題がありました。このツールは無料で、簡単にroomをつくれて入ることができ全員の見積もりが揃ったらポイントを確認することが出来るので大変便利です。

f:id:yo-shimada:20210222093957j:plain
Scurm Porcker Online

ScatterSpoke

スプリントレトロスペクティブで利用しているツールです。

振り返りを実施する際に各自が課題をあげて投票を行うために利用しています。リモート以前は付箋を利用するなどしてました。リモート後はesaなどに記載していましたが、ドキュメント共有ツールだと他者が記載している内容や投票がわかるので、自分以外の意見に影響を受けてしまう可能性もありました。

ScatterSpokeを利用することにより、

  • 他者の振り返りトピックの内容・投票が公開されるまで分からない
  • タイマー機能で制限時間がわかりやすい
  • トピック内容が被った場合にグルーピングしやすい
  • Slack連携がありアクションアイテムを確認する事ができる

などのメリットがあります

f:id:yo-shimada:20210222095756j:plain
ScatterSpoke

その他

その他にリモート環境下だと中々きっかけがないとコミュニケーションが発生しなかったりする事があるので、開発チームが話し合うチャンネルにSlackのリマインダーで「時報」をするようにしています。今やっている事を簡単に書いたり、困ったことなどを気軽に投稿したりしています。

f:id:yo-shimada:20210222100126p:plain

最後に

世間にはリモートで生産性をあげるためのツールは数多くありますが、チームやメンバーの状況に応じて合ったツールを選ぶ事が重要だと思いました。

現時点でのFS開発チームは様々な紆余曲折の末に以上のような開発プロセスを行っています。

これも日々のスクラムでの振り返りによる成果であり、半年後にはまた何かやり方を変えている可能性があります。リモートやリアルにかかわらず、大切なのは現状に満足することなく常により良くできないかを考えることだと思いました。

幸いな事として会社やチームは新しいツールや仕組みの導入を試す事に対して寛容なので(もちろん、それ相応の必要性がないとダメですが)、それも改善の後押しになっていると感じています。

Dagger の Assisted Inject 統合とマイグレーション

こんにちは、モバイルクライアントグループの中島です。 年末少し膝を痛めてしまいランニングを中断していたのですが、そろそろ再開していきたい今日この頃。

さて、今回は Dagger が Assisted Inject を統合したことによるマイグレーションについてお話ししたいと思います。 特に、WorkManager と、2/9にリリースされた v2.32 の変更部分とでつまづいたことについてお話したいと思います。

Assisted Inject とは

簡単に言えば、Daggerによる Inject constructor に対して、 Dagger によってinjectできるもの以外のパラメータを入れ込むための機能です。 Androidで一般的な使い所としては、固有のデータIDを用いて詳細を表示する画面などで、そのIDを不変な値としてViewModelのconstructorに入れたいときなどでしょう。

square/AssistedInject

この機能は以前より外部サポートライブラリである square/AssistedInject を用いて実現されていました。

compileOnly `com.squareup.inject:assisted-inject-annotations-dagger2:0.6.0`
kapt `com.squareup.inject:assisted-inject-processor-dagger2:0.6.0`
@Module(includes = [PresenterModule::class])
abstract class HogeModule {
    // ViewのInjector定義など
}

@AssistedModule
@Module(includes = [AssistedInject_PresenterModule::class])
internal abstract class PresenterModule
// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted private val hogeId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedInject.Factory
    interface Factory {
        fun create(hogeId: String): HogeDetailViewModel
    }
}
// View
    private val args: HogeDetailFragmentArgs by navArgs()

    @Inject
    lateinit var viewModelFactory: HogeDetailViewModel.Factory
    private val viewModel: HogeDetailViewModel by assistedViewModels {
        viewModelFactory.create(hogeId = args.hogeId)
    }

なお、assistedVieModels はtakahiromさんの以下の記事を参考にさせていただき作成して運用している、Dagger用の拡張関数です。 詳しくはそちらをご参照ください。

qiita.com

Dagger 2.31 における公式への統合

今年の1月15日、 Dagger v2.31 のアップデートにて Assisted Injection が公式に統合されました。

github.com

これにより square/AssistedInject の依存が消え、PresenterModule など、 Module への追加記述も必要なくなりました。

// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted private val hogeId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedFactory <- // ここだけアノテーション名が違います
    interface Factory {
        fun create(hogeId: String): HogeDetailViewModel
    }
}
// View
    private val args: HogeDetailFragmentArgs by navArgs()

    @Inject
    lateinit var viewModelFactory: HogeDetailViewModel.Factory
    private val viewModel: HogeDetailViewModel by assistedViewModels {
        viewModelFactory.create(hogeId = args.hogeId)
    }

このマイグレーションについてもtakahiromさんの記事が大変参考になりました。

qiita.com

今回つまづいたところ

本題に入っていきます。公式の Assisted Injection へのマイグレーションを行なう上で、つまづいた点が二箇所ほどありました。

  • WorkManager のビルドが通らない
  • 同じ型の Assisted パラメータが判別できない

順を追って説明していきます。

WorkManager のビルドが通らない

Studyplus Android では一部のバックグラウンド処理に WorkManager を利用しています。 WorkManager の Assisted Inject について詳しくは以前の記事をご覧ください。

tech.studyplus.co.jp

問題

ViewModelと同様にマイグレーションを行なっていたところ、ビルドエラーが発生しました。

interface ChildWorkerFactory {
    fun create(appContext: Context, params: WorkerParameters): ListenableWorker
}

class HogeWorker @AssistedInject constructor(
    @Assisted private val appContext: Context,
    @Assisted private val params: WorkerParameters,
    private val repository: HogeRepository,
) : CoroutineWorker(appContext, params) {

    override fun doWork(): Result {
        // ~~
    }

    @AssistedFactory // <- アノテーションの変更
    interface Factory : ChildWorkerFactory
}
エラー: [~~.ChildWorkerFactory.create(android.content.Context, androidx.work.WorkerParameters)]
Invalid return type: androidx.work.ListenableWorker. An assisted factory's abstract method must return a type with an @AssistedInject-annotated constructor.

該当する Dagger の生成コードを見てみます。

    @dagger.assisted.AssistedFactory()
    public static abstract interface Factory extends ~~.ChildWorkerFactory {
    }

要は何もoverrideされていないので、継承元である ChildWorkerFactory のままListenableWorkerをcreateしようとしているようです。 その結果、「@AssistedInjectのアノテーションが付与されているconstructorがないぞ」と言われているわけですね。

それならばとcreateメソッドまでoverrideして、返り値の型を「@AssistedInjectのアノテーションが付与されているconstructorを持つWorker」にしてみました。

    @AssistedFactory
    interface Factory : ChildWorkerFactory {
        override fun create(appContext: Context, params: WorkerParameters): HogeWorker
    }

その結果、またビルドエラーが出ましたがメッセージが変わりました。

エラー: 不適合な型:
 dagger.internal.Factory<HogeWorker_Factory_Impl>をProvider<~~.HogeWorker.Factory>に変換できません:

…?

FactoryをProviderに変換できない? それはそうだろうと思うのですが、そこが変わるような変更を加えた覚えがなく、色々いじっても解決することなく結局合計2日程度ここで止まってしまいました。

解決

2月9日、 Dagger v2.32 のアップデートにて解決しました。 リリースノートを見ると、 Java 7で起きる型推論issueだったようです。

余談ですが、試しにDagger v2.31.2 へ戻した上でWorkManagerのあるモジュールをJava 8でビルドしてみたところ、2日間悩んでいたのが嘘のように通りました。 Dagger 関連のコードではなく、gradleでJavaバージョンを変更することで通るようになるとは発想が至りませんでした…修行不足です。

// build.gradle(:workmanager)
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

v2.32 で修正されたので「DaggerのためにJava 8でビルドする」ということはもう必要ないとは思いますが、デフォルトでJava 8になるのはいつかなぁなどとふと思いを馳せました。

同じ型の Assisted パラメータが判別できない

問題

v2.32 にすることでWorkManagerの問題は解決したのですが、また新たな壁が立ちはだかりました。 同じ型のパラメータがAssisted Injectされているとエラーメッセージが出る問題です。

エラー:
 @AssistedInject constructor has duplicate @Assisted type: @Assisted java.lang.String
解決

これについてはv2.32のリリースノートを見てすぐ解決しました。

Parameters in @AssistedFactory classes that have the same type now require a name to be set via @Assisted("foo") to disambiguate between arguments. Previously, order of parameters was used.

今までは記述された順番でパラメータの対応をしていたけど、v2.32 からは @Assisted("foo") でそれぞれに名前を設定してcreateメソッドと対応させるよ、ということですね。 やることとしては、同じ型をAssisted Injectしている箇所全てに名前を付けていくだけでした。

// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted("hogeId") private val hogeId: String,
    @Assisted("fugaId") private val fugaId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedFactory
    interface Factory {
        fun create(
            @Assisted("hogeId") hogeId: String,
            @Assisted("fugaId") fugaId: String,
        ): HogeDetailViewModel
    }
}

アノテーションへの値の設定もですが、createメソッドのパラメータにも@Assistedアノテーションが必要になったのは新しい要素ですね。 以前のsquare/AssistedInject では引数の名前そのものの一致で対応させていたので、自由度は増えたけど少し冗長かなという印象は否めない感じでしょうか。

終わりに

簡単ではありますが、Studyplus Androidにおける、Dagger の公式Assisted Inject対応の際に引っかかった事例を紹介しました。

Assisted Injectは、Studyplus Androidにとって必須の機能として重宝しているので、 Dagger に統合されたのは非常に嬉しいですね。 square/AssistedInject の完成度も高かったためかどうしても比較してしまう部分もあります。 ですが、issueの対応なども積極的に行なわれているので、これからもより便利になっていくだろうと思っています。

最後までご覧いただき、ありがとうございました!

DropboxのStoreで通信量を削減しました

こんにちは、モバイルクライアントグループの隅山です。 前回は両OS開発についてのブログを書きましたが、今回はDropboxのStoreを用いてAndroidアプリの通信量を削減した話をしていきます。

Storeについて

まず、DropboxのStoreとはアプリ内のデータ操作(取得、共有、保存、検索)を簡素化するライブラリです。 ネットワーク経由でデータをいつ取得するか、メモリとディスクのどっちにキャッシュするか、データをKotlinのFlowで返却するかなどを簡単に実装することができます。

最近ではネットワークを最適化することが推奨されており、データをキャッシュしてオフラインでも使用できるようにしたり、不要なネットワークリクエストを防ぐ必要があります。 今回はネットワークを最適化するための第一歩として、Storeを用いて不要なネットワークリクエストを防ぐ対応を行ったのでその話をしていきます。

導入について

ライブラリをアプリへ導入する方法はStoreのREADME.mdに記載されているので、ここでは割愛させていただきます。

github.com

導入方法

不要な通信を防ぐために導入する目的であれば、実装は非常に簡単です。 弊社のコードを具体例に説明していきます。

弊社のアプリではユーザーが自分の所属している高校を設定することができます。 その高校一覧をサーバから取得してUI上に表示している箇所が下記のコードとなります。

Repository層

class HighSchoolsRepository(private val service: HighSchoolsService) {
    suspend fun index(): List<HighSchool> = service.index()
}

ViewModel層

class StudyGoalHighSchoolViewModel(private val repository: HighSchoolsRepository) : ViewModel() {
    val highSchoolList = MutableLiveData<List<HighSchool>>()

    init {
        viewModelScope.launch {
            runCatching {
                repository.index()
            }.onSuccess {
                highSchoolList.value = it
            }
        }
    }
}

Activity層

class StudyGoalHighSchoolActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.highSchoolList.observe(this) {
            // リストとして表示
        }
    }
}

上記のコードでStoreを導入する場合、変更するのはRepository層のみです。 導入後のRepository層が下記となります。

class HighSchoolsRepository(private val service: HighSchoolsService) {

    private val store =
        StoreBuilder
            .from(Fetcher.of { service.index() })
            .build()

    suspend fun index(): List<HighSchool> = store.get()
}

APIを叩く際に引数を設定しませんでしたが、高校一覧取得APIに都道府県コードを設定する場合も簡単に実装することができます。

class HighSchoolsRepository(private val service: HighSchoolsService) {

    private val store =
        StoreBuilder
            .from(Fetcher.of { locationCode: Int -> service.index(locationCode) })
            .build()

    suspend fun index(locationCode: Int): List<HighSchool> = store.get(locationCode)
}

導入解説

Repository層からAPIを叩く際にStoreを導入しましたが、何故これで通信が削減されるか説明していきます。

まず、Storeとはアプリ内のデータ操作を簡素化するライブラリです。 APIを叩く箇所にStoreを導入するだけでは実装前後で差分は生まれません。 差分が出るのはStoreからどのようにデータを取り出すかによって決まります。

Storeからデータを取り出す方法は、現状では下記の3通りあります。

  • Store.get(key: Key):メモリ内のキャッシュかsourceOfTruthからデータ取得、取得できない場合はネットワークからのデータ取得
  • Store.fresh(key: Key):ネットワークからデータ取得
  • Store.stream():Storeのデータが更新されるとFlowでデータ返却

Storeを導入してもfresh(key: Key)でデータ取得を行えば、導入前のコードと同様の動作となります。 しかし、get(key: Key)でデータ取得を行うことでメモリ内のキャッシュかsourceOfTruthからデータを取得するため、ネットワークからデータ取得する回数を削減することができます。 メモリ内のキャッシュの生存期間はデフォルトで100個のアイテムを24時間に設定されています。*1

まとめると、弊社の導入後のコードではget(key: Key)を用いているため、24時間以内であれば再び通信することはなく通信量が削減されています。

メリット・デメリット

導入解説で細かく説明しましたが、メリットデメリットを簡単にまとめたいと思います。

メリット:

  • メモリ内のキャッシュに保存できるため通信量が削減できる
  • 端末がオフラインでもデータを表示することができる
  • Flowでデータをリアクティブに流したい場合やローカルDBに保存したい場合など拡張しやすい
  • 導入が非常に簡単

デメリット:

  • サーバのデータが頻繁に変わるAPIは導入に向かない
  • APIごとにStoreBuilderからStoreを作成するためコード量がだいぶ増える

まとめ

今回は通信量を削減することに絞ってStore導入を説明しましたが、拡張性が高く他にも多くのことができます。 弊社のアプリでは14箇所のAPIでStoreを導入し通信量の削減をしました。

今後はFlowを用いてデータをリアルタイムに更新したり、ローカルDBとのデータ操作部分に導入していきたいと思います。 他の導入方法で発見があったらまたブログでまとめていきます。

*1:生存期間はMemoryPolicyを用いて設定変更することもできます

週1回30分のフロントエンドミーティングを始めました

こんにちは。ForSchool事業部の石上です。ハライチのターンというラジオが好きです。ぜひRadikoで聴いてみてください。

今日は、ForSchool事業部で行っているフロントエンドミーティングという取り組みについて紹介します。

3行で

  • フロントエンドミーティングを始めました
  • いろいろ課題が出てきました
  • やっていくぞ

フロントエンドミーティングとはなにか

ForSchool事業部におけるフロントエンドミーティングとは、週1回チーム内でフロントエンドに話題を絞って振り返りや相談を行うミーティングのことです1

なぜやっているのか

Studyplus for Schoolのフロントエンドには、日常の業務で各人が課題を感じているのにそれを整理できていないという問題がありました。そこで、フロントエンド技術改善に絞ったロードマップを作り直すことにして、昨年の11〜12月の間にMTGを重ねて、ロードマップを作成しました。

ところが、このロードマップには少し不安が残りました。「作り直すことにしました」と書いたとおり、実はこういったものを作るのは初めてではなく、昨年の4月にも同じようなものを作っていました。ここでまた作り直しが発生してるということは、問題だと感じる点や改善案などがその当時から変わっているということです。ということは、今回つくったものも、半年後にはどうなっているかわかりません。このロードマップは、なるべく日常的に見直す必要があると考えました。

そこで、週1回、30分時間をとってこのロードマップを見直す会を設けることにしました。そして、ただ真面目に見直しだけをしても面白くないので、気になっているフロントエンド技術についての雑談などをアジェンダに盛り込みました。これが今ForSchool事業部で行っているフロントエンドミーティングです。

フロントエンドミーティングで整理できた問題

f:id:shgam:20210205092705p:plain
フロントエンドミーティングはこれまで4回開催

このフロントエンドミーティングは12月から始まり、これまでに4回開催しています。たとえば以下のような相談や問題解決がこの場でできています。

  • コーディングガイドラインを定めて、今後どのようにコードを書くかを明記していく。
  • typed-css-modulesというライブラリを利用してclassNameの型定義を生成していたが、これはwebpack設定のメンテが大変になる影響があるのでやめる。
  • 新たに必要になるウェブアプリケーションのインフラについて相談(Next.jsとVercelを採用)
  • 使いまわしにくいコンポーネントをどう整理していくか、タスクとしてどこをゴールにして進めていくか

こうやって見ると、毎週30分にしてはだいぶ実りがあるように思えます(これは昨年8月に入社してくれた@okuparaさんの専門性があってこそできていることではあるのですが2)。

もしこのミーティングがなければこういったことを整理する場もなく、問題が横たわったままだったかもしれません。やってよかった。

ちょっとしたキャッチアップの場に

フロントエンドは流れが速いとよく言われますが、フロントエンドミーティングのような場でそれぞれが気になっている技術を紹介すれば、ちょっとしたキャッチアップの場にはなると思います。これまでのフロントエンドミーティングで出てきた話題はReact Server Components, blitz, snowpackなどです。

発表資料をつくるわけではなく、「こんなのあるらしいんですけど、どう思います?」みたいな雑な話ができるので、とても楽しいです。

今後

まだまだ大小いろんな問題があるのですが、コツコツとやっていけたらなと思います。採用も積極的にしているので、手伝ってくれるエンジニアはぜひ応募してください。

www.wantedly.com


  1. フロントエンドミーティングという呼び名は、フィードフォースさんが過去にその名でフロントエンド技術の共有会を行っていた記憶があり、それを真似しました。

  2. 入社早々、脱enzyme->RTLの導入をしてくれたりしてます。すごい。https://tech.studyplus.co.jp/entry/2020/10/05/090000

Studyplus iOSアプリでWidgetに対応しました

初めまして、モバイルクライアントグループの上原です。昨年11月からiOSアプリ開発を担当しています。 最近は、Apex Legendsで目標だったランクのダイヤ4に到達し、ランクのモチベーションが下がりカジュアルをずっと回す日常になりました。

さて、本題に入ります。Studyplusでは、iOS 14から実装されたWidgetに対応し、昨年11月30日にカウントダウンWidgetをリリースしました。
今回はどのようなWidgetを作成したのか、導入経緯やTipsなどを紹介していこうと思います。

カウントダウンWidget

Studyplusでは、アプリ内でユーザが設定したイベント(模試や期末テストなど)までの日数を表示するイベントカウントダウン機能を提供しています。
上記の機能を、ホーム画面でも確認できるようにしたのがカウントダウンWidgetです。

f:id:nappannda:20210120155037p:plain:w200
カウントダウンWidget画像

Widgetの設定画面からアプリ内で設定しているカウントダウンを選択したりシンプルモードといった形で端末の外観モードに合わせた表示ができるようになっています。

f:id:nappannda:20210118072955p:plain:w200
Widget設定画面画像

f:id:nappannda:20210118073957p:plain:w200
カウントダウンWidget ライトモード
f:id:nappannda:20210118073912p:plain:w200
カウントダウンWidget ダークモード

Widget実装経緯

iOS 14から実装されたWidgetですが、Studyplusで実装に至った経緯は下記になります。

エンジニアからiOS 14の新機能のどれかを作りたい意見が出る
WidgetがSwiftUIのみで書くものだったので今後のSwiftUI環境に向けての勉強になりそう、Widgetを作りたい旨を伝える
ディレクターやデザイナーとユーザへの新しいアプローチでユーザ価値が出せるものがないかを検討
ユーザの学習に対して緊張感・危機感を高めるものとしてカウントダウンWidgetを実装

アプリを触っていただいた方には伝わるかなと思うのですが、イベントまでの日数がカウントダウンWidgetに表示されていると、スマホを開くたびに緊張感が高まり学習の習慣化を促すことができ新しい価値を提供することができたと思います。 しかし、SwiftUIの学習にWidgetが利用できたかというとViewの少しの実装に関しては利用できましたが、やはり@Stateや@ObservedObjectなどで値が更新されたらViewを更新するなどSwiftUIの肝となる部分などはWidgetではサポートされておらずSwiftUIの学習面では少し微妙だなと感じました。

実装Tips

Widgetで実装した機能のなかでどうやって実装したかどうかなどを紹介していきます。

シンプルモード ON/OFF時のviewへのShadowオンオフ

カウントダウンWidgetでは、シンプルモードではない時に特定のViewにShadowを付け、シンプルモードではShadowを付けないといった仕様がありました。 何も考えずにSwiftUIで愚直にやろうとすると下記のようなコードになります。 条件によってほぼ同じViewが存在してしまったり、見た目をカスタマイズしようとすると分岐が複雑化してしまったりと、見にくいコードになってしまいます。

let isSimpleMode: Bool
var body: some View {
    if !simpleMode {
        Text("タイトル").shadow(color: .init(red: 0, green: 0, blue: 0, opacity: 0.75), radius: 0.5, x: 0.5, y: 0.5)
    } else {
        Text("タイトル")
    }
}

上記をもっとスマートにある特定の条件式の場合であればShadowを付けたいですよね?
下記のブログで紹介されているViewにExtensionで条件に適していればクロージャーを実行、適していなければそのままViewを返すコードを実装すればこのコードがすっきりします。

extension View {
    @ViewBuilder
    func `if`<Content: View>(_ condition: Bool, content: (Self) -> Content) -> some View {
        if condition {
            content(self)
        }
        else {
            self
        }
    }
}

blog.kaltoun.cz

上記を適用したコードが下記になります。同じようなViewが複数定義されることなく条件によって何が適用されるかが分かりやすくなりました。

let isSimpleMode: Bool
var body: some View {
    Text("タイトル")
        .if(!isSimpleMode) {
            $0.shadow(color: .init(red: 0, green: 0, blue: 0, opacity: 0.75), radius: 0.5, x: 0.5, y: 0.5)
        }
}

Widgetを押した際にアプリの特定画面に飛ばしたり、Widgetからの起動を計測する

Widgetは要素を押した際にURLを渡すことができます。 この機能を利用するとURLを解析しアプリの特定画面を開いたり、Widgetからの起動を計測したりすることができます。
具体的には、widgetURLにURLを渡すことで要素を押した時にそのURLが開くことになります。
注意事項としてWidgetのサイズがSmallでは、一つしか遷移に利用できません。Medium以上だと複数のwidgetURLを定義して利用することができます。

var body: some View {
    VStack {
        Text("タイトル")
        Text("サブタイトル")
    }.widgetURL(URL("app://countdown"))
}

実装ではまった&困惑したところ

TextのfontSizeを48以上に指定するとSimulator上で一瞬表示された後、消える

Xcode 12.1 ~ 12.3時点でビルドしたSimulatorで発生することを確認した挙動です。 Simulatorのみで起きており実機では再現しないのでfontSize 48以上の指定で実装した場合は、実機で確認する必要があります。

Widgetの処理がブレークポイントで止まらない

ビルド後に上部メニューからDebug->Attach to Processを選択しその中からWidgetを選択すると止まるようになります。
時々止まらないこともあるので、その時は端末からWidgetを削除したりXcode再起動を試すと上手くいくと思います。

最後に

Widgetはいろいろ制約がありますが、その制約が強いことでユーザにシンプルな情報を提供できるように感じました。 そして、制約の強さがWidgetの実装が複雑化しないようになっているのかなと実装していて感じました。
また、新しい機能ということもあり実装情報が少なかったり、予期せぬ動作が起きたり実装していくなかで様々なことがありましたが新しいものに触るのは大変面白くいい経験でした。

Kubernetes輪講会を開催しました

こんにちは、Studyplus事業部 サーバーサイドエンジニアの葉坂です。

以前弊社の「Kubernetesを本番導入しました」という記事でも紹介していますが、スタディプラスでは2020年9月にKubernetes本番導入を果たしました。

tech.studyplus.co.jp

それに伴いサーバーチームもKubernetesでの運用をしていく上で必要な知見を高められるようSREチームと合同でKubernetes輪講会を開催しました。今回はこの輪講会について紹介したいと思います。

はじめに

輪講とは一般的に、

「一つの書物の範囲を決め、その範囲の担当者が事前に内容を調べて、参加者に対して説明をする」

というものです。

そもそもなぜ輪講会形式にしたのか

当初はSREチームの有識者による講義形式の開催も検討されていましたが、講義形式だと理解が浅くなってしまうことも多いので、輪講会形式で開催されました。

そうすることで担当者は担った範囲に関して、最低限人に説明できるレベルにまで理解を深める必要が出てきます。実際、「Kubernetesはコンテナ・オーケストレーション・ツールのあれだな」くらいの知識しかなかった私でも、人に説明できるレベルにまで理解を深められました。

使用した参考書

輪講会では、「Kubernetes完全ガイド」を使用しました。Kubernetesは本体の技術要素が膨大かつ、周辺のエコシステムも充実しているため、学習コストはかなり高いように感じるかと思います。

ですが、「Kubernetes完全ガイド」は網羅性が高く、マニフェストのサンプルも充実しています。記載内容のレベルも全般的に一段深いため、Kubernetesだけでなくインフラ周りの知見に乏しい私でも非常に分かりやすい参考書に感じました。 book.impress.co.jp

また GitHub にも、「Kubernetes完全ガイド」で紹介されているマニフェストが公開されているため、試したり写経したりと無限に学ぶことができます。 github.com

運営ポリシー

進め方

  • 事前に範囲と担当者を決める。
  • 担当者は範囲を事前に調べておき、当日参加者が内容を理解できるように説明をする。
    • 説明の仕方は担当者に委ねる。スライド を作ったり、ホワイトボードに書いて説明したり、口頭で済ます等々。
    • 本に書いてあることを全て説明する必要はなく、自分で重要だと思うポイントをピックアップし説明するのでもよい。
    • 担当者が内容を調べる際、不明点やうまくいかない部分があれば slack で質問を投げる。
  • 参加者は事前に範囲を読み、当日に疑問点を担当者に聞いたり、参加者同士でディスカッションを行う。

上記のような流れで輪講会を開催しました。

ちなみに輪講会の準備に関しては業務に関係のあることなので、業務時間内に行っています。

Kubernetes環境

Kubernetesのほとんどの機能は試せるため、簡単にローカルKubernetes環境を構築できる「minikube」を使用しました。ただ、マルチノードクラスタの作成時は「kind」を使用しました。

参加人数

5人(サーバーサイドエンジニアとSREの全員)で開催しました。

開催場所

Zoomで開催しました。

開催回数

週1回、業務時間内で1時間半の枠を確保し、全9回に分けて開催しました。

ちなみに

現在はKubernetes輪講会の開催枠で、もくもく会形式のGolang勉強会を行っています。それはサーバーチームが担当している10あるマイクロサービスのうち1つがGolangで書かれていることや、今後新たなマイクロサービス作成の際の言語の選択肢を増やすためです。

輪講会に参加して分かったメリット

実際に開催してみると、冒頭にて説明した以外にも、輪講会形式には下記のような様々なメリットがあることがわかりました。

  • 分担し合うことで、独学では時間のかかる内容&ボリュームの書籍でも、メンバーの助けを借りて理解することができた。
  • 自分の担当範囲に関しては、内容の説明だけでなく質問を受けた際も想定し疑問点を潰していくため知識も増えた。
  • 参加者への伝え方を考える上で、資料構成や表現方法、アウトプット手法の学びにも繋がった。
  • 同じ内容からでも、自分とは違う解釈を知れたり、そこから派生した知見共有があったりと、独学や講義形式で知識を受けるよりも学びの幅がかなり広がった。

Kubernetesを学んでみて

私がインフラ周りの知見に乏しいこともあり、初めはKubernetesに対してかなり苦手意識がありました。ただ、実際学んでみると、ServiceとIngressの理解には苦しめられましたが、Kubernetesに登場する概念が多いだけで、1つ1つ丁寧に学んでいけば、そこまで難しいと感じることはありませんでした。今では、弊社で運用しているKubernetesマニフェストを難なく読むことができるくらいに成長しました。輪講会様様です。

さいごに

輪講会は開催してみると良いことずくめでした。 題材書籍への学習意欲向上、吸い上げた知識のアウトプット方法の学び、メンバーとの盛んなコミュニケーション等々…。

特に、昨今コロナでのリモートワークが増え、メンバー同士のコミュニケーションが減っていることに懸念や一抹の寂しさを感じている方に大変オススメです。

興味のある方は是非、輪講会を開催してみてはいかがでしょうか!!

We Are Hiring

現在スタディプラスでは、サーバーサイドエンジニアを募集しています! open.talentio.com

Studyplus AndroidアプリでMAD Scoreを計測してみました

新年あけましておめでとうございます。

モバイルクライアントチームの若宮(id:D_R_1009)です。 お正月にようやくポケモンシールドのチャンピオンを倒しました。 本当に強かった……。

さて、昨年末にMAD Scoreが登場しました。

今回はMAD Scoreの紹介をしつつ、弊社Studyplus Androidアプリの計測結果をお見せしたいと思います。

MAD Scoreとは?

developer.android.com

Modern Android Development から MAD を取っているようです。 Androidも世に出てから10年以上経っているので Modern を強調しているのかなと思います。

Android 11では、ついに AsyncTask を非推奨にすると明言されました。 AndroidX Fragment 1.3.0からは onActivityCreatedstartActivityForResult/onActivityResult が非推奨になっています。 いつの間にかKotlinは1.4系がリリースされ、1.4系の機能を利用したJetpack Composeはalpha版になりました。

Kotlin、Jetpack、Android Studio、Android App Bundle など、最新の Android 開発(MAD)は優れたアプリを構築するための基礎となります。

挙げられている4つの項目も、それぞれ10年前とは様変わりしています。 見比べてみると、確かに Modern なアプリ開発な気がしてきませんか?

  • JavaからKotlin
  • SupportライブラリからJetpack
  • EclipseからAndroid Studio
  • Android application package(apk)からAndroid App Bundle(aab)

次からStudyplus Androidアプリのスコアを見つつ、それぞれの項目がどう評価されるのかを見てみます。

Kotlin

f:id:D_R_1009:20201225084805p:plain

Kotlinは、アプリを構成しているKotlinのパーセントが表示されます。 そのほか、Kotlinのバージョンや利用しているライブラリが掲載されていますね。

tech.studyplus.co.jp

上記ブログのように2019年9月末に86%程度だったKotlin率は、2020年6月頭に99%となり、その後はKotlinのみ増減している状態になります。 1ファイルのみ、Paging 2のutilクラスをJavaで利用しているので、Paging 3のリリースと同時に100%となる見込みです。 リリースが待ち遠しい……!

そのほか、KTXやFlowの利用が表示されています。 FlowはRoomの他、Storeライブラリなどで利用しています。Cold Streamを手軽に扱えるのは、本当に重宝しますね。

tech.studyplus.co.jp

github.com

Jetpack

f:id:D_R_1009:20201225085802p:plain

Jetpackは、利用しているライブラリの数が表示されるようです。 思っていた以上に利用していてびっくりしました。

tech.studyplus.co.jp

Studyplusアプリは開発しているエンジニアの数が(利用ユーザーに対して)少なめなこともあり、Jetpackライブラリを活用した開発を重視しています。 機能性が高い、安定したライブラリが提供されていることは、とても幸運なことだと思っています。 今後も活用できそうなライブラリがあれば、積極的に利用していきたいところです!

Android Studio

f:id:D_R_1009:20201225090753p:plain

ちょっとコメントが難しいのですが、利用しているAndroid Studioのバージョンが表示されます。 現時点ではAndroid StudioとAGPのアップデートが一致してしまっているので、Android Studioを更新しにくい環境もあるかもしれません。

AGP 7.0からはAndroid Studioのバージョンと切り離されることになります。 そうするとこの問題も解決しますね。個人的にはR8のバージョンをいい感じにアップデートできるので嬉しいアップデートです。

android-developers.googleblog.com

なお、Android Studioのスコアを取得できるのは今だけ! (かも)

Android App Bundle

f:id:D_R_1009:20201225090833p:plain

1TB と圧のある数字が出ていますが、aabにすることで100万人がDLしたときに削減される容量のようです。 ただGoogle Play Storeをみてみると、Studyplus Androidは 1,000,000+ のDLとのことなので、数年規模で見ると的外れな数字ではないような気もしてきます。

もともとのアプリサイズ、そしてNDKを利用しているかどうかでこの値は変動する気がするので、大小を一概に良い悪いとは言えない値だと思います。 ですが、日々のアプリサイズを小さくする試みの成果をわかりやすく見ることができるのは、気分も軽くなるのではないでしょうか。

まとめ

f:id:D_R_1009:20201225090848p:plain

トータルの評価は G.O.A.T でした! 日々のAndroidチームの頑張りが評価されたようで、とても嬉しいです。

madscorecard.withgoogle.com

Android Studioにpluginを入れると数分で計測することができます。 ぜひ、計測してみてください!