それがいいことの序章です

楽しいことならいっぱい夢見ることならめいっぱい

MockKを使ってて便利だった関数たち

MockKにお世話になっていて、便利だった関数をいくつかご紹介します。

confirmVerified

verifyですべての呼び出しが検証されたかをチェックすることができます。

たとえば「ボタンがクリックされたときにアナリティクスイベントを記録する」という挙動をテストしたいというケースで、このようにテストを書いたとします。(ViewModelでクリックイベントをハンドリングしたとする)

val analytics: AnalyticsManager = mockk(relaxed = true)
// テスト対象を生成
val viewModel: HogeViewModel = createViewModel(analytics = analytics)
viewModel.onClickButton()

verify(exactly = 1) {
    analytics.logCustomEvent(Event.TapButton)
}
// analyticsの呼び出しすべてを検証したかをチェック
confirmVerified(analytics)

このとき、HogeViewModel#onClickButtonを呼び出した際にanalyticsに対してverifyで検証したもの以外の呼び出しがあったときに、confirmVerifiedは例外をスローしてくれます。

予期せぬ呼び出しがないかや、検証の漏れがないかという場合に便利です。

ofType

マッチャーのひとつで、値の型をチェックすることができます。

通常の検証ではeq(value)が使われますが、ものによってはequalsの比較の検証が難しい場合もあります。

val analytics: AnalyticsManager = mockk(relaxed = true)
// テスト対象を生成
val viewModel: HogeViewModel = createViewModel(analytics = analytics)
viewModel.onClickSave()

verify(exactly = 1) {
    // Event.Saveという型のイベントが記録されたことを検証
    analytics.logCustomEvent(ofType(Event.Save::class))
}

このように書くと、AnalyticsManager#logCustomEventに渡したパラメータがEvent.Saveであることを検証できます。

withArg

こちらもマッチャーの一つで、検証するパラメータについてコードの実行が可能なため、ofTypeより’柔軟に検証することができます。
こちらはMockKVerificationScope内でのみ使うことができます。

val analytics: AnalyticsManager = mockk(relaxed = true)
// テスト対象を生成
val viewModel: HogeViewModel = createViewModel(analytics = analytics)
viewModel.onClickSave()

verify(exactly = 1) {
    // 引数についてAssertで詳細に検証
    analytics.logCustomEvent(withArg { event: Event ->
        Assert.assertEquals(1234, event.params["user_id"])
        Assert.assertEquals("hoge", event.params["type"])
    })
}

メソッド呼び出しの引数についてテストしたい場合に便利です。

アフターデジタル2を読み始めた

アフターデジタル2の1章を読んだ。
Kindleは便利で、気になったところにさっとハイライトとメモを残せる。
ザクッとメモを振り返りながら思ったことをザクッとブログに残そうと思う。

ハイライトとメモと振り返り

リアルとデジタルの接点の主従関係を逆転させて考える必要があるというのが、「アフターデジタル」という

ほんのタイトルにもある"アフターデジタル"という言葉の意味が面白かった。
書いてもある通り、どうやってリアルの中にデジタルを、言い換えれば、どうやって人の生活の中でサービスを使ってもらうか、という考えになりがちだなあと感じた。
言葉遊びにならないように気をつけなければいけないと思うものの、インターネットの普及によって色んなものが繋げられた今は、リアルとデジタルを分けて考える必要はなくなっている、といのは自分の体感としてもあるし、意識していきたい。

個別接点のデータがいくら集まっても大した意味はなく、データがシーケンス型に整理されていることが大事

プロダクトの成長や分析のためにも、データを取ることは必要不可欠になっているが、そのデータを線として捉えるのは意外とできていないし、難しい。でもただただ取りがち。
ユーザーの行動の整理ができていないのかもしれない。どう行動させたいか、という定義すら決まってないとシーケンス的に取ることはかなわない。
こういったとこからも、エクスペリエンスと行動データ、つまりUXとエンジニアリングはつながるのかもなと感じた。
どういう体験をさせたいか、それが実現できているかを、体験ベースのシーケンスになるように行動データを取るべきで、それができていれば、UXの良し悪しは数字によって測定可能になりそう。

デジタルによる利便性という意味では、中国は日本より進んでいます。「利便性というマスの余白」は既に取りつくされてしまっており、多くの日本人が求めるような「バズりやすいキャッチーな事例」が出てこなくなってしまった

「利便性というマスの余白」という言い回し良い。そしてこれの取り合いがまだまだ日本だと多い印象がある。これについてはもちろん、余白が残っているということかもしれない。
だが、この取り合いに終止してしまうと伸びしろは余白分しかないとも言えるかもしれない。人の生活は点でもマスでもパズルでもなくて、時間軸のある線なので、余白を埋めるだけではそこに入るのは難しいかも。

目標設定とかなんとかでよく「whatじゃなくてwhy」と言われているが、サービスにおいても自然と「何を使うか」ではなく「なぜ使うか」という視点で選ばれていく時代になりそうな気がする。本の言葉を使うと「新たな生活スタイルの提案」をしていくことが重要と言えるかもしれない。便利な○○を使う、のではなく、こういう自分でありたい、あるいはこういう生活をしたいから○○を使う、というフェーズになったときに戦えるようだと強そう。

雑記

自分はUXに興味があって、だけど(だけどってのもおかしいけど)軸としてはエンジニアなので、どう向き合うかという部分についてふわふわしてたけど、少しだけ、解像度が上がった気がする。

エンジニアとしても、点を線にすることが大事。データにも言えるしユーザーの分析にも言える。点を提供するのではなく、点は線にし、線と線のコネクタも作らなければならない。 どうアプローチするのかしたいのか、まだまだ具体化できない。抽象的である。

あしたには別のことを言っているかもしれない。

言語化すると自分との対話みたいで思考が整理されるのでよいです。

Navigation Architecture Componentを使ったログイン機能実装方法

素振りの一環でTwitterクライアントのサンプルを作っています。(WIP)
starをもらえると喜びます。 github.com

そんなときに@AndroidDevのツイートで、Navigation Architecture Component(以下Navigation)を使った際のログインのCase Studyが紹介されていたので、Twitterクライアントにも必要であろうと実装してみました。

ログイフロー概説

Navigationを用いる場合、まずはナビゲーショングラフに開始のディスティネーションを設定する必要があります。
ログインを必須とするアプリの場合、開始のディスティネーションにログイン画面を設定したくなりますが、これはよくないとされています。
(記事末にこのときの弊害について記載しておきます。)

ナビゲーションの原則に従いながら、ログイン画面への遷移を実現するためには、各画面から必要なときのみログイン画面に遷移する方法を取るのが好ましいようです。

詳しくはAndroid DevelopersのYouTube動画を御覧ください。

www.youtube.com

実装

前提として私が試した環境はこちらです。

サンプルアプリ概略

一般的なTwitterクライアントを題材にしています。

アプリはタイムラインを表示する画面(以下HomeFragment)があり、こちらはログインをしているユーザのみ見れる想定です。また、今回は開始ディスティネーションをHomeFragmentとしています。
未ログインのユーザにはログイン画面(以下LoginFragment)へ遷移させるようにします。

順を追って説明します。

LoginFragmentへの遷移

未ログインの状態を判定し、ログイン画面へ遷移をさせます。

class HomeFragment : Fragment(R.layout.fragment_home) {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            viewModel.authUser.collect { user -> if (user == null) navigateLogin() }
        }
    }
    ...
}

今回は簡易的に、保存されたログインユーザがnullの場合に遷移する処理を実装しています。 ログインが必須の場合は、ディープリンク経由で遷移できる画面すべてに同様の処理を実装する必要がありそうです。

ログイン結果を遷移元(HomeFragment)に伝える

LoginFragmentは、必要なときに別の画面から呼び出されることを前提として作ります。

Navigation(ver2.3.0)ではpreviousBackStackEntryを使い、遷移元のFragmentに値を伝えることができるので、これを活用します。

class LoginFragment : Fragment(R.layout.fragment_login) {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val navController = findNavController()
        val savedStateHandle = requireNotNull(navController.previousBackStackEntry).savedStateHandle

        val binding = FragmentLoginBinding.bind(view)
        binding.loginButton.setOnClickListener { viewModel.saveAuthUser() }

        viewModel.succeeded.observe(viewLifecycleOwner) {
            // ログインの成功を前の画面に伝える
            savedStateHandle.set(KEY_LOGIN_SUCCESSFUL, true)
            navController.popBackStack()
        }
    }
    ...
}

サンプルではログインボタンを押したら適当なAuthUserを保存するようにしていますが、実際には認証処理を行い、その結果の成否によって処理を分ける必要があるでしょう。

ログインの成否を受け取る

HomeFragmentではcurrentBackStackEntryを使い、先程LoginFragmentがセットした成否の値をもとに処理を行います。

class HomeFragment : Fragment(R.layout.fragment_home) {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val navController = findNavController()
        val savedStateHandle = requireNotNull(navController.currentBackStackEntry).savedStateHandle
        savedStateHandle.getLiveData<Boolean>(KEY_LOGIN_SUCCESSFUL)
            .observe(viewLifecycleOwner) { success ->
                // 成否の結果から処理を行う
                if (success) viewModel.getTweets()
                savedStateHandle.remove<Boolean>(KEY_LOGIN_SUCCESSFUL)
            }
    }
    ...
}

アプリの性質によって成否の処理はうまく処理しましょう。
今回はログインが成功したときのみ前の画面に戻るように実装しているので、失敗したときの処理は実装していません。

開始ディスティネーションにログイン画面を設定したときの弊害

アプリの多くは、ディープリンクによってWebサイトやSNSアプリなどからアプリに遷移させることが多いかと思います。
Navigationでもディープリンクをハンドリングする機構が整っています。そして、Navigationではディープリンクの場合、手動ナビゲーションをシミュレーションするという性質があります。

つまり、リスト画面とリスト要素の詳細画面を持つアプリの場合、ディープリンク経由で詳細画面に遷移した場合も、バックスタックにはリスト画面も含まれているということになります。(詳しくは公式ドキュメントのナビゲーションの原則をご参照ください)

これにより、ログイン画面を開始ディスティネーションに設定した場合、ディープリンクでアプリ内のコンテンツにアクセスした際に、ログイン画面がバックスタックに積まれることになってしまいます。

もちろん、ログイン済みかどうかの判定をしてここの調整をするとは思いますが、少し煩雑になりますし、ナビゲーションの原則に沿っていません。

References

potatotips #68 参加していました!

参加した回はこちら
ブログ枠での参加でしたが、大変遅れてしまって申し訳ないです、書いていきます!

potatotipsはAndroid/iOSの入り乱れた勉強会で、全部で10個以上のLTが!
1つは5分と短いですが、Tipsの所感を掴むのにもってこいです。
Android開発者としては、普段は聞かないiOSのつらみや解決策を聞けるのも良いです!

Androidの発表はこちら!

今日からはじめるGithub Actions

ワークフローを網羅的に説明していてもはやバイブル!
Github ActionsはGitHubのさまざまなイベントをフックにできるので触っていて楽しい!

実際にリリースノートを自動化していて素敵でした。

Navigation Componentのデータ受け渡しのつまづき

Nested Graphのときのデータ受け渡しのTipsでした!
Nested Graphを使う場合はactionタグにargumentタグを追加で解決とのこと。
これはハマりそう…知見…

複雑になった画面をリファクタした話

複雑になりがちなメッセージング画面をリファクタリングした話でした。
こういう実プロダクトでの課題解決は本当に聞いていて面白いですね。

1Activity&1Fragment -> 1Activity&2Fragment にして、責務によって階層を分けるという解決策でした。
メッセージング画面、すごい知見の宝庫だと思うので各社の実装方法でわいわい議論するの楽しそう。。

ViewBindingで手軽にView操作

ViewBindingはだいぶ浸透してきたという肌感。お手軽で便利なので個人でも使っていきたい!
DataBindingライブラリを使うまでもないけどfindViewByIdKotlin Android Extensionもなあ〜ってときはすぐ使える。

スライドにもあるように、メモリリークにも注意しましょう。
最近個人的には、極力onViewCreatedの中でbindingを使って完結させられるように意識してます。(なかなかそうはいかない)

Flutter (Dart) と Platform (Android/iOS)- 相互呼び出しのMethodChannelについて -

Flutterのお話。
ネイティブの機能を使うandよいライブラリがなければやはり自前実装になりますが、そのつなぎをどうやるかという発表。

Flutterは最近少し触っていますが、非常に開発者体験がよいです!
いざというときにネイティブの知識を持っているのは強みだな〜と感じてます。

ML Complete Features(Flutter)

続きましてもFlutterの話、ですが、ML Completeというコード補完のお話。

実験的機能ですが、変数が補完されるのかなり嬉しいかも…変数めちゃめちゃ悩みますよね…
未来は簡単な変数名ならエディタが考えてくれる時代を感じました。

Badging for Tabs and Bottom Navigation

Material Components 1.1.0でのBottomNavigationViewでバッヂがつけれるように!

ドットにしたりマックス値以上は+で表現したり、基本的なユースケースは満たしてくれそうで素敵。

AndroidのSkeletonプロジェクト作ってみてる

個人プロジェクトだったり検証用サンプルだったり、作り始めるときに毎回書いているコードないですか。
Timberの初期化だったりモノによってはDaggerだったり。

そんなときにクローンしてパッケージ変えて使えるの作ってみてます🙋🏻‍♂️
github.com

気が向いたときに少しずつ整えていきたいし、自分用にメンテもしていきたい。
実プロダクト向けではないのでどんどんalpha版/beta版つっこんでいくぞ!

蛇足

ケルトン作るぞって思ってちょこちょこ作業してたけど、なぜかTODOぽいものを作ろうとしてしまう自分がいて、それはTodoサンプルや…と自分と戦いながら最終的には不要なコードが散見される感じになってしまったw
とはいえアーキテクチャ的に参考になる部分は含めたいのでまたちょこちょこアップデートしたい。なにがいるかな?

コードの間違いやアドバイスは大歓迎なのでスターください!(?)