Studyplus Engineering Blog

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

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

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

プロジェクト・タスク管理ツールにmonday.comを使い始めたので、いかにmonday.comが最高かを紹介したい

こんにちは、スタディプラスの栗山(id:shepherdMaster)です。

みなさんは社内でどんなプロジェクト・タスク管理ツールを使ってますでしょうか? 今回はスタディプラスで最近使い始めたプロジェクト・タスク管理ツールのmonday.comを紹介したいと思います。

Taskworld時代

社内では以前はTrelloに似たカンバン形式のプロジェクト・タスク管理ツールのTaskworldを使っていました。 しかしTaskworldは使い続けていると動作が重くなってきたり、たびたびTaskworldに障害が発生したり、Slackへ簡単に通知連携できなかったり、そもそもツールとして使いづらかったりと不満が上がっていました。 そのため新しいプロジェクト・タスク管理ツールを選定することになりました。

新しいプロジェクト・タスク管理ツールの選定

社内で新しいプロジェクト・タスク管理ツールを選定するためのメンバーを募り、メンバー間でいくつかのツールを試しました。 4月からツールの比較検討を始め、5月には全社でmonday.comを使い始めました。導入までかなりスピードが速かったと思いますが、これもひとえにmonday.comがとても便利で早く業務で使いたかったというタスク管理ツール選定メンバーの熱い思いからです。

ツール選定基準としては以下がありました。

  • 使いやすい
  • サクサク動く
  • 弊社では今のところスクラム開発をやってはおらず素朴なカンバン方式でタスク管理をしているので、JIRAのようなスクラム開発に向いた機能は必要ない
  • Slack通知が簡単にできる

なぜmonday.comにしたか。またはいかにmonday.comを愛しているか

私をはじめ、ツール選定メンバーはmonday.comを触ってすぐにmonday.comが好きになりました。 私は過去にTrac, Redmine, JIRAなどを使ってきましたが、それらのツールを使ってプロジェクト・タスク管理をするのは若干の心理的障壁がありました(要は使いにくく管理が面倒)。 しかしmonday.comはとても手に馴染み、個人的なタスク管理としても使っているくらい私を虜にしました。😍

ということで、monday.comのどこが素敵か紹介していきましょう。

UI/UX

monday.com のUIは非常によくできていて、やりたいことが素早くできるように工夫を凝らして作り込まれています。 また初めて使う人でもほとんど迷うことなく直感的に使えるため、monday.comを導入した後でもメンバーから使い方に関する問い合わせは来ませんでした。 個人的には色使いがカラフルで好きです。 😋

カスタマイズ性

高いカスタマイズ性はmonday.comの特徴の一つです。 タスク管理ツールで入力項目を増やそうと思うと設定画面にいく必要がほとんどかと思いますが、monday.comの場合その場で項目を足すことができます。(項目の移動もドラッグ&ドロップでできます) ステータス、担当者、日付、テキスト、数値、チェックボックス、ドロップダウンといったよく使う項目の他にタグ、投票、レーティング、 ロケーション、URLカラム等々、全部で35種類の項目が用意されています 😁

それぞれの項目はとても気が利いていて例えば
ステータス項目では、種類と色が自分で設定できます。

f:id:shepherdMaster:20190621192920p:plain:w300

タイムライン項目では、どのくらい日付が進んでいるかが可視化されます。

f:id:shepherdMaster:20190621193148p:plain:w200

タイムライン項目をGoogleカレンダーに同期させることもできます。

f:id:shepherdMaster:20190621193215p:plain:w300

Board

タスクを積む場所としてboardというのがあります。たとえばチームごとにboardを作ったりプロジェクトごとにboardを作ることになります。 boardにはShareable Boardという機能があり、このShareable Boardを作ると自分の会社のメンバーではない人をこのboardにゲストとして招待することができます。これにより社外の人には特定のboardしか見せないといったことができます。

またboard間をCtrl+Bで切り替えられるのが便利です(SlackのCtrl+Kのような感じ)

View

boardにはView機能があり、タスクをカンバン形式でみるためのKanban Viewや、ガントチャート形式でみるためのTimeline View、カレンダーViewなどがあります。

カンバンView

f:id:shepherdMaster:20190621193252p:plain

Timeline(ガントチャート)View

f:id:shepherdMaster:20190621193305p:plain

カレンダーView

f:id:shepherdMaster:20190621193311p:plain

Integration機能

昨今のWebサービス同様に、他のWebサービスと連携ができます。 たとえばSlackと連携して通知を送ったり、Google カレンダーと連携できたり。GithubでPullRequestが作成されたらレビュー用のタスクを自動的に作成することもできます。 f:id:shepherdMaster:20190621193428p:plain

Automation機能

このAutomation機能はmonday.comの目玉機能の一つです。
Automation機能を設定することによって、様々な作業が自動化されます。😌

以下はAutomation機能でできる一例です。

  • タスクのステータスをDoneにしたら自動でアーカイブしたり他のグループへ移動させたりする
  • タスクのステータスが〇〇になったら△△さんへ通知する
  • タスクが追加された時、自動で特定の担当者にアサインする
  • タスクの項目に変更があったら△△さんへ通知する
  • 日付項目に入力された日付の〇日前に△△さんへ通知する

以下はAutomation設定画面です。様々なAutomationが用意されています。
f:id:shepherdMaster:20190621193548p:plain

その他にもZapierやIntegromatと連携もできるので、やりたいことはほぼほぼできると思います。便利ですね。

高い検索性

monday.comは検索性も素晴らしいです。 例えばboard画面では以下のように勝手にタスクを分類してくれて、ボタンを押すだけでタスクを絞り込むことが可能です。エクセレント 😊 f:id:shepherdMaster:20190621193620p:plain

また検索画面ではboardを横断して検索ができます。そしてなんと検索結果からタスクをその場で編集することができます。 f:id:shepherdMaster:20190621193632p:plain

テンプレート機能

monday.comでは豊富なテンプレートをもとにboardを作成することができ、boardの項目を設定する手間が省けます。(もちろん空のboardを作ることもできます) また独自のテンプレートを登録できるため、自社でよく使う形式のboardを登録しておくと使い回しがきいて捗ります。

My Week

My Week画面ではその週に終わらせる必要があるタスクを一覧で見ることができます。 期限が今日までのタスクや、期限が明日以降のタスク、今週終わったタスクが見れるため、自分がやるべきタスクが明確になります。 また他のメンバーをフォローすると、MyWeek画面にそのメンバーのタスクもでてくるため、チームメンバーがどのようなタスクをどんな進捗でやっているのかが分かります。 たくさんメンバーをフォローしたらMy Week画面にタスクがたくさん表示されてしまうと懸念されるかもしれませんが、もちろんユーザでタスクを絞ることができます 😉

その他の地味に便利な機能

  • タスクにつけたコメントに対していいねができる
  • だれがそのコメントを見たか表示してくれる
    f:id:shepherdMaster:20190624125215p:plain:w400
  • スマホアプリがある
    • 帰宅途中とかにサクッとタスクを追加したり、出社途中に今日やるタスクを確認することができて便利ですね。

不満点

正直不満点はないのですが、会社によっては以下が不満に繋がりそうでした。

  • 日本語サポートがされていない
    • 表示は英語ですがUIが分かりやすく作られているため、それほど問題ないかなと思います。
    • ちなみに日本ではGapriseさんが代理店をやっていて日本語の解説記事を載せてくれているのでそちらをみると機能の理解が進むかなと思います
  • JIRAのようなスプリント管理に特化はしていない(例えばバーンダウンチャートとかはない)
    • どちらかというとmonday.comはタスク管理に特化しています。これはチームの開発スタイルによって良し悪しが分かれるでしょう。

お値段

https://monday.com/pricing/
Standard Planで年間契約すると

  • 10人 $79/month
  • 25人 $195/month
  • 50人 $399/month

なので1人1ヶ月10ドル以下ですね。他のサービスと比べても安いほうかなと思います。

最後に

日本では知名度はまだまだですが、試しに使ってみると細かいところまで作り込まれたmonday.comの最高さが心で理解できると思います。 monday.comを使うことで月曜日を素敵な週の始まりの日にしていきましょう!

Flutterもくもく自習室 in スタディプラス #3 を開催しました

こんにちは。 スタディプラスではAndroidの開発を、趣味でflutterの開発をしている若宮(id:D_R_1009)です。

6月22日(土)に開催した「Flutterもくもく自習室 in スタディプラス #3」の結果についてまとめます。

connpass.com

Flutterもくもく自習室とは?

第1回のブログをご参照ください。

tech.studyplus.co.jp

運営スタッフから見た第3回の様子

今回は運営スタッフが1名増え、3名で開催しました。

参加者の様子は当日のTwitterのハッシュタグ #flutter_studyplus で見れます。

須藤(id:kurotyann)

今回は天候が悪かったにもかかわらず、前回の倍以上の参加者が参加してくれました。

参加していただいた皆様、ありがとうございました。

当日は参加者が増えたことにより、自習の内容が多岐にわたってきたように思います。

私が観測した範囲だと、下記のような自習が行われていました。

  • 趣味でFlutterのアプリ開発を試す人
  • 趣味でFlutter for Webを試す人
  • Flutterに関係するOSSリポジトリへPRを出そうとする人
  • 次のプロジェクトでFlutterを検討しており、開発に関わる疑問や相談をしている人
  • その他の言語やフレームワークを試す人

Flutterもくもく自習室と銘打っていますが、Flutterにかかわらず、自分のスキルアップのために参加者の皆さんはもくもくと学習しています。

その結果、弊社オフィスの雰囲気はとても良く、当日は私も心地よく集中して開発することができました!

回を重ねるたびに良い環境へ変わっているので、今後も月1ペースで開催していければと思います。

若宮(id:D_R_1009)

想定外の激しい雨が降ってしまい、少し外を歩きにくい雰囲気の開催となってしまいました。 そんな中、14名の方に参加いただきとても嬉しく思っています。

個人的な成果としては、前回に引き続き簡単なAndroidライブラリの開発に成功しました。

github.com

Androidアプリ開発に3時間、その後のflutter開発に1時間ほどの配分でしたが、どちらも考えていた以上の開発を行うことができました。 休日に家で進めていてもここまで捗ることはそうそう無いため、運営スタッフとしても「もくもく自習室」を活用できているなと感じます。

参加者のみなさま、ありがとうございました! また何かお気づきの点などあれば、ぜひスタッフまでお寄せください!

大石(id:k_oishi)

今回より運営スタッフとして参加しましたiOSエンジニアの大石です。 当日はあまり天気が良くなかったのですが、たくさんの参加者の方々に集まっていただき感謝いたします。

自身の進捗としましては、現在個人開発しているイベント向けアプリのFlutter移行を進めており、今回はFlutterの開発環境の構築を行いました。 最終的にはAndroid StudioとFlutter Pluginを導入して、Android StudioからiOS端末でアプリの起動まで進めることができました。 ここ数日の間に個人のMacBook ProとMac mini、職場のMacBook Proに開発環境を構築しましたので、そこそこのやる気を感じています。

次回の開催も予定していますので、お気軽にご参加ください。 今後ともよろしくお願いいたします。

次回の開催は?

次回のもくもく学習会の開催日は7月中を予定しています。 connpass上にて募集をいたしますので、よければ下記アカウントのフォローなどをお願いします。

connpass.com

それでは、次回の「Flutterもくもく自習室」でお会いしましょう!

GitHubのPull Panda連携を(さっそく)導入しました!

Androidチームの若宮(id:D_R_1009)です。 今朝方、Twitterを眺めていたら下記のツイートが目にとまりました。

「これは便利そうだ!」と感じたため社内Slackに投稿し、

f:id:D_R_1009:20190618132709p:plain

利用を開始したところ

f:id:D_R_1009:20190618132725p:plain

期待以上の便利さだったので、本ブログでも紹介したいと思います。

f:id:D_R_1009:20190618132756p:plain

Pull Pandaとは

https://pullpanda.com/

GitHubのリリースでは下記のように紹介されています。

We’re excited to share some big news: we’ve acquired Pull Panda to help teams create more efficient and effective code review workflows on GitHub.

https://github.blog/2019-06-17-github-acquires-pull-panda/

ブログでも紹介されている通り、Pull Pandaには"Pull Reminders"・"Pull Analytics"・"Pull Assigner"の3機能があります。

  • Pull Reminders : Slack通知
    • Pull Requestが開かれた、コメントされたなどの条件でSlack通知をカスタマイズできます
    • 指定の時間にPull Requestをチェックし、コードレビューやコードレビューへの対応を促すリマインダー機能も備えています
  • Pull Analytics : Pull Requestに関連する様々なデータ分析機能
    • Pull Requestの平均マージ時間や修正量、各コントリビューターのコメント量などを一覧にします
    • チームやリポジトリごとの取り組みが可視化されるため、データをベースにした組織文化の改善を行うことができます
  • Pull Assigner : 自動レビュアーアサイン機能
    • Pull Requestへのレビュアーが偏らないよう、自動的にコントリビューターをアサインします

Slack用GitHubアプリとの違い

Slackの公式サイトで紹介されている通り、SlackとGithubを連携させているチームも多いと思います。

https://get.slack.help/hc/ja/articles/232289568-GitHub-%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B

大きな違いとしては、下記3点があげられると感じています。

  • 通知をSlackチャンネルだけでなく個人のDMとして受け取ることができるので、Slack通知を個人やチームに合わせてカスタマイズしやすい
  • レビューのリマインダー機能があるため、「レビューをお願いします!」と個人間でやり取りする必要がなくなる
  • 「レビュー終わりました! 対応お願いします!」とレビュアーがレビュイーに連絡する必要がなくなる

どれもチーム内の個人の活動でなんとかしていたことを自動化 + ルールとして運用することができるようになります。 「ツーカーの仲」で開発しているチームより、多人数で協働しているチームをサポートするツールではないでしょうか。

運用ルール

弊Androidチームの場合

現在は下記ルールで運用を開始しています。

  • リマインダー機能(チャンネルへのリマインダー)

    • 11:00と17:00の1日2回リマインダー at #android_pr
    • レビュー待ち、レビューコメント対応待ちの2条件で通知
    • PRは作成されて2時間以上たったものを通知
    • 通知したくないPRには WIP ラベルをつける
  • リマインダー機能(個別DMへのリマインダー)

    • カスタマイズ推奨
    • 開発スタイルに合わせておまかせ
  • 自動アサイン機能(Automated review assignment)

    • Androidチームは CODEOWNER を利用しているため対応しない
    • Androidチームが増えたら対応を検討(2019年6月現在3名)

設定について

スタディプラスでは個々人の働き方に合わせた出勤時間となるため、可能な限り緩めのルールで運用したいと考えています。 そのため、リマインド設定は下記の条件としました。

  • 昨日レビューが終わっていないPull Request ⇨ 11:00にリマインド
  • 当日15時までに作成されたPull Request ⇨ 17:00にリマインド

今後

Androidチームで試験的に取り入れとなりましたが、社内の他チームでも取り入れる機運が高まっています。 社内の全チームで展開し、知見を取り入れてより便利に使っていきたい……と思っていたら、他チームの導入がどんどん進んでいました。

f:id:D_R_1009:20190618143248p:plain

気軽にPull Requestのフローを改善できるので、ぜひぜひ取り入れてみてください!

スキーマ駆動開発のススメ

こんにちは,For School事業部のid:atomiyamaです.

現在Studyplus for Schoolはサービスのフルリニューアルを行っています.
弊サービスはこれまでRailsでslimを使いViewを提供してきましたが,今後より良い体験をユーザーへ届けるためにリニューアルを行いサーバーサイドとクライアントサイドを分離しました.
リニューアルに向けて現在サーバーサイドはRailsでJSON APIサーバーの開発を行っており,その中で導入したスキーマ駆動開発の話をします.

TL;DR

  • 技術スタックはOpenAPI3.0, swaggerUI,committee
  • クライアントサイド開発者と連携してJSONスキーマを仮決定する.
  • サーバーサイドの開発者はRailsコントローラにそのJSONをべた書きした仮実装を行う.
  • クライアントサイド,サーバーサイドの開発者が互いにフィードバックしながら並行して実装を進める.

スキーマ駆動開発とは

スキーマ駆動開発とは,
「スキーマをはじめに定義し,スキーマを元にAPI開発者,利用者が同時に開発を進めていくこと」です.

スキーマ駆動開発を導入するモチベーション

今回スキーマ駆動開発を導入した背景は以下の2つの問題により思ったようなスケジュールで開発が進行してしなかったことです.

  • サーバーサイドの実装が完了するまでクライアントサイド開発者が実装に着手できない.
  • 一度実装したAPIにクライアントサイドから変更の依頼が入り手戻りが頻発する.

今回のフルリニューアルのような長期にわたるプロジェクトでは特に既に開発を終えたタスクに対してな手戻りが頻発することは当初のスケジュール通りにプロジェクトを進めていく上で大きな問題になります.
またクライアントサイドにおいてはAPIで提供されるデータの仕様が不明確なままでは開発を進めることができませんからサーバーサイドの進捗から強く影響を受けてしまいます.

そこでサーバーサイド,クライアントサイドが並行して独立して開発できるようにスキーマ駆動開発を導入しました.

スキーマ駆動開発

私達のチームではスキーマ駆動開発を進める上で以下のようなステップにそって開発を進行しています.

  1. APIのスキーマを利用者の合意のもと決定する
  2. APIスタブサーバを提供する
  3. 本実装を行う

APIドキュメンテーション

APIのスキーマを決定しドキュメント化するために弊社ではOpenAPI3.0を使っています. OpenAPI3.0に従ってyamlで定義を書くんですがサービスがある程度の規模になってくると一つのyamlファイルにすべてのエンドポイントの仕様を書くのは辛くなってきます😭
そこで以下の用にエンドポイントURIのパスと対応するレベルでファイルを分割し,Rakeタスクでファイルを結合してswaggerUIが読み込み可能な形に吐き出しています.

├── app
├── bin
├── config
├── db
├── dockerfiles
├── lib
├── log
├── public
├── spec
├── swagger
│   └── v1
│       ├── main.yaml
│       ├── paths.yaml
│       ├── components
│       │   └─ user.yml
│       └── paths
│           └─ users.yml
├── tmp
└── vendor
# swagger/main.yml
openapi: 3.0.0
info:
  title: SampleAPIApp
  version: 0.0.1
servers:
  - url: https://example.com/v1
paths:
  $ref: ./paths.yaml
components:
  schemas:
    $ref: ./components/schemas.yaml

# swagger/paths.yaml
/v1/users:
  $ref: ./paths/users.yaml

# swagger/path/users.yaml
get:
  summary: ユーザー一覧
  tags:
    - ユーザー
  responses:
    200:
      description: OK
      content:
        application/json:
          schema:
            type: object
            required:
              - users
            properties:
              users:
                type: array
                items:
                  type: object
                  required:
                    - name
                    - age
                    - email
                  properties:
                    name:
                      type: string
                      description: 名前
                      example: スタプラタロウ
                    age:
                      type: integer
                      description: 年齢
                      example: 20
                    email:
                      type: string
                      description: メールアドレス
                      example: sutapura.tarou@example.com

APIスタブサーバの提供

前項でスキーマが決定しswaggerUIリーダブルな形に変換できたのでswaggerUIの公式イメージを立ち上げ出力したファイルを読み込むことで下のようにAPIのスキーマが閲覧可能になりました.
f:id:atomiyama:20190612170745p:plain ここから実際にRailsの実装を行っていくのですが,いきなり本実装をはじめてしまうと完了するまでクライアント開発者は開発に着手できなくなってしまうので最低限の実装でAPIスタブを実装します.

実際にはswaggerUIのResponsesのExample valueにあるオブジェクトをコピーして,Railsのコントローラにそのまま貼り付けます.

# app/controllers/v1/users_controller.rb
class V1::UsersController < ApplicationController
  def index
    return json: {
      "users": [
        {
          "name": "スタプラタロウ",
          "age": 20,
          "email": "sutapura.tarou@example.com"
        }
      ]
    }
  end
end

テストも最低限書いておきます.テストではcommitteeを使っています.
committeeを使ってテストをすることでOpenAPI3.0に則って定義したスキーマと差分の無いレスポンスが返却されることをテストすることができます.
committeeの導入についてはこちらを確認してください.

require 'rails_helper'

RSpec.describe V1::UsersController, type: :request do
  describe "GET /v1/users" do
    subject { get "/v1/users" }

    # 最低限のテスト
    it "200" do
      subject
      expect(response).to have_http_status(200)
      assert_schema_conform
    end
  end
end

ここまでをやったら一度PullRequestを作成してデプロイしてしまいます.
これらの作業は2,3時間もあれば終わってしまう作業ですし,ここまで完了すればサーバーから仮のデータが提供されるようになっているためクライアントサイドの開発者も開発を進めることができます.

本実装

残すはクライアント,サーバーともに前項までにやった仮の実装を本実装していくだけです.
ひたすらにスキーマ通りにデータを返せるようにコーディングします.

まとめ

スキーマ駆動開発を導入することにより早い段階で決定されたAPIスキーマ(APIスタブも含む)を元に,クライアントサイド,サーバーサイド互いが自身の作業に専念できるようになりました.
個人的にいいなと思った点は,最初のスキーマを決定する際にクライアントサイド開発者のフィードバックを受けるので互いの仕様への理解をすり合わせる空気感が自然とできることです.
またほぼ同時に本実装が進むため,もしどちらかの実装の都合によりスキーマに仕様変更が入っても完成前なので大きな手戻りにはなりにくいためスケジュール遅延リスクも最小限に抑えることができます.

是非これからAPI開発に取り組む方はスキーマ駆動開発取り入れてみてはどうでしょう.

Android Architecture Componentsに感動した話

初めまして、Studyplus開発部のAndroidエンジニアの隅山です。今年5月からスタディプラスでAndroidアプリ開発を行っています。

本格的にAndroid開発を始めてまだ日が浅いので、初中級エンジニア目線で学んだことをまとめていきたいと思います。

背景

初めてAndroid開発を行った時感じたことが「Androidの画面表示関連の仕様の複雑さ」でした。

以前開発してた時もAndroidのライフサイクルや画面回転あたりで非常に苦しめられていました。しかし今回、Android Architecture Components(AAC)を学ぶ+導入することで、ライフサイクルや画面回転問題が簡単に解消されたのでAAC実装例を紹介します。

やったこと

今回、ユーザが入力する文字列に対応した検索結果を、リアルタイムでリスト表示する機能を実装しました。

実装としては画面にEditTextとListViewを用意し、EditTextの文字の変更通知に合わせてListViewのアイテムを更新するという仕組みとなります。AACを使わない場合と使う場合で大きく変わった点が「EditTextの文字変更通知」部分なのでそこに着目していきます。

AAC使わない場合

まずはAACを使わないやり方、TextWatcherで文字変更通知を受ける場合です。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val itemList = listOf("くろ", "あか", "あお", "しろ")
        val editText = findViewById<EditText>(R.id.edit_text)
        val listView = findViewById<ListView>(R.id.list_view)
        val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, itemList)
        listView.adapter = arrayAdapter

        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                // 文字変更通知を受けて、ListViewの中身更新
                val list = if (s.isNullOrEmpty()) itemList else itemList.filter { it.contains(s) }
                arrayAdapter.clear()
                arrayAdapter.addAll(list)
            }
            override fun beforeTextChanged(s: CharSequence?, s: Int, c: Int, a: Int) {}
            override fun onTextChanged(s: CharSequence?, s: Int, b: Int, c: Int) {}
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • Androidの標準APIのみで書ける
  • デメリット
    • 画面回転考慮する必要がある
    • アクティビティの役割が大きすぎる
    • モダンな実装ではない

特に、画面回転問題を考慮する場合、onSaveInstanceStateなどでデータを保持する必要があります。 また、サーバーから検索結果を取得し表示するなどの機能拡張を行うと、アクティビティクラスのコード量も役割もどんどん増えてしまいます。

AAC使う場合

つぎにAACのViewModel+LiveDataで文字変更通知を受ける場合です。

class SampleViewModel : ViewModel() {
    private val itemList = listOf("くろ", "あか", "あお", "しろ")

    // serchTextは双方向BindingによりEditTextと接続
    val searchText = MutableLiveData<String>()
    val items: LiveData<List<String>> = Transformations.map(searchText) { text ->
        // ここで文字変更通知を受け、リストに表示するアイテム作成
        if (text.isNullOrEmpty()) itemList else itemList.filter { it.contains(text) }
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProviders.of(this).get(SampleViewModel::class.java)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1)

        binding.lifecycleOwner = this
        binding.viewModel = viewModel
        binding.listView.adapter = adapter

        viewModel.items.observe(this, Observer { items ->
            // リスト表示アイテムを受け、ListViewに表示
            adapter.clear()
            items?.let { adapter.addAll(it) }
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • ViewModelがViewModelProviderで管理されているため画面回転されても情報が保持される
    • SampleViewModelで文字変更通知を受け、リストに表示するアイテムを作成するため、アクティビティは流れてきたアイテムを表示する役割のみ
    • モダンな実装のため、Google公式の最新アップデートに追随しやすい
  • デメリット
    • AACに関して勉強する必要がある

文字変更通知もLiveDataを使うと簡単に受けることができ、リストに表示するアイテムを自由にViewModel側でカスタマイズすることができます。今回は簡単な文字列リストを表示するだけですが、実際は検索文字を用いてサーバアクセスしたため、SampleViewModelでKotlin Coroutinesなどを使って実装しました。

まとめ

Androidでつまづきやすい画面回転によって情報が落ちてしまう問題や、クラスの役割が膨大になる問題をAACを導入することで解決できたのは感動しました。

今回は簡単な例を示すためリストに文字列表示するだけでしたが、実際の利用シーンでは検索文字列をサーバに投げてレスポンス結果をリストでリアルタイム表示しています。この際、Kotlin Coroutinesによる非同期処理や入力待ちの遅延処理を行なっています。AACを使わない場合だとアクティビティが肥大化して運用できない形になっていたため、今後の運用面も考慮して開発することができたのはよかったと思います。

AndroidのことやAACのことは同じチームメンバーの若宮さんや中島さんに教わりつつ開発することができたため、大きくつまづくことはほとんどなく非常に助かりました。Androidの画面回転やライフサイクルなどで詰まっている方は是非Android Architecture Componentsを導入してみてください。