20201128のAndroidに関する記事は8件です。

【Kotlin】DialogFragmentからActivityへ値を渡す

はじめに

AndroidアプリでDialogFragmentを実装する際、普通の?(はい、いいえの二択のヤツ)ダイアログではなく、EditTextなどを含むダイアログを作成しその値をActivityへ渡す方法です。
やり方はいくつかあるらしいのですが、今回は新しくインターフェースを実装するやり方です。
Fragment自体の実装については割愛する部分が多めです。
おかしな所があれば教えていただけると幸いです!

構成

MainActivity.kt:Fragmentの呼び出しとFragmentから受け取った値を表示するだけ。
CustomDialogFragment.kt:ダイアログ用のFragmentです。
dialog_layout.xml:EditTextを含むダイアログ用のレイアウトファイルです。

レイアウト

先にレイアウトを実装しておきます。
MainActivityはなんでも良いので割愛します。
dialog_layout.xmlはres/layout下に作成します。

dialog_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <EditText
        android:id="@+id/dialog_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hint_dialog_text"
        android:textColor="@color/colorText"
        android:textColorHint="@color/colorHintText"
        />

</LinearLayout>

実装

フラグメントの作成
CustomDialogFragment.kt
class CustomDialogFragment:DialogFragment() {

    public interface DialogListener{
        public fun onDialogPositive(dialog: DialogFragment)//今回は使わない。色んなダイアログで使いまわす際には使います。
        public fun onDialogNegative(dialog: DialogFragment)
        public fun onDialogTextRecieve(dialog: DialogFragment,text: String)//Activity側へStringを渡します。
    }
    var listener:DialogListener? = null

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        val inflater = activity?.layoutInflater
        val dialogView = inflater?.inflate(R.layout.dialog_layout,null)//作ったダイアログ用のレイアウトファイルを設定します。
        builder.setView(dialogView)
               .setTitle("タイトル")
               .setPositiveButton("決定"){ dialog, id ->
                    val text = dialogView?.findViewById<EditText>(R.id.dialog_text)?.text//EditTextのテキストを取得
                    if (!text.isNullOrEmpty()){//textが空でなければ
                           listener?.onDialogTextRecieve(this,text.toString())
                     }
                }
                .setNegativeButton("キャンセル"){ dialog, id ->
                     listener?.onDialogNegative(this)
                }
        return builder.create()
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = context as DialogListener
        }catch (e: Exception){
            Log.e("ERROR","CANNOT FIND LISTENER")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }
}

複数のダイアログを実装する場合は、setPositiveButtonでOnDialogPositiveとすれば値は渡されず、OnDialogTextRecieveとすればStringが渡せるようになります。
リスナーはonAttachのタイミングでcontext(Activity)を設定することで、ActivityやFragmentの再生成時でも問題なく動いてくれるようです。

Activity側で受け取る
MainActivity.kt
class MainActivity : AppCompatActivity(),CustomDialogFragment.DialogListener {

    //~省略~

    override fun onDialogTextRecieve(dialog: DialogFragment, text: String) {
        //値を受け取る
        Log.d("dialog",text)
    }
    override fun onDialogPositive(dialog: DialogFragment) {
        //実装なし
    }
    override fun onDialogNegative(dialog: DialogFragment) {
        //キャンセル時
    }
}

おしまい

最低限の実装はこれで終わりです。一度実装してしまえばFragment自体はWhen文などで切り分けて使いまわせるので、インターフェースとレイアウトを増やすことで色々なダイアログからActivityへ値を渡すことが出来ます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MotionLayoutでアニメーションさせるイメージを掴む

Life is Tech ! #2 Advent Calendar 2020 1日目の記事です?

はじめに

  • ゴディバです!
  • 2020 LiT!#2アドベントカレンダー1日目はAndroidの記事になります!
  • これを読めば誰でもMotionLayoutを使えるようになることを目指しました!
  • いまさらMotionLayout・・・?
    • はい!!!

なにやんの

  • ようやくConstraintLayout 2.0が安定版になったこともあり、改めてMotionLayoutを使ったアニメーションの簡単なAndroidサンプルアプリを作ります
  • え、これでアニメーションできるの。ちょっとMotionLayout使ってやろうかな」って思ってくれたら僕の勝ちです

MotionLayoutとは

  • ConstraintLayout 2.0.0から使えるようになったレイアウト
  • 得意技はアニメーションとそのモーションの管理
  • ConstraintLayoutのサブクラスなので基本的にConstraintLayoutと同じように使えます
  • 詳しくはこちら

超ざっくりしたイメージ

  • アニメーション前のレイアウトとアニメーション後のレイアウトを作ると、その間のアニメーションを勝手にやってくれる凄いやつです
  • なのでやることとしてはレイアウトを2つ作るイメージです

実装!

依存追加

  • 2.0.1でも使えますが、諸々のバグが修正された2.0.4でいきます
build.gradle
dependencies {
...
    androidx.constraintlayout:constraintlayout:2.0.4
}

該当レイアウトのGroupを変更

  • アニメーションさせたいレイアウトのroot-viewをMotionLayoutに変えます
  • とりあえずView2つくらい置いときます
  • エラー出ると思いますが、後ほど作るモーションファイルを指定してないだけなので慌てなくて大丈夫です
activity_main.xml
<androidx.constraintlayout.motion.widget.MotionLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/black"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#444444"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/red" />

    <View
        android:id="@+id/red"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#cc4444"
        app:layout_constraintBottom_toTopOf="@id/black"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

MotionSceneを作成

  • /res/xml/scene_main.xmlみたいなレイアウトファイルを作成します (名前は何でもいいです)
  • このレイアウトファイルでアニメーション前とアニメーション後のレイアウトをそれぞれ作るイメージです
scene_main.xml
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragUp"
            app:touchAnchorId="@+id/black"
            app:touchAnchorSide="top" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/red" />

        <Constraint
            android:id="@+id/red"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toTopOf="@id/black"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toTopOf="@id/red"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/red"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/black" />
    </ConstraintSet>

</MotionScene>
  • こんなレイアウトを作ってます

解説

  • <Transition>
    • レイアウトの動きを指定します
    • 後述するレイアウトの状態のidを開始と終了でそれぞれ指定してます
    • durationは言わずもがな、アニメーション時間を指定してます(ms)
  • <OnSwipe>
    • ユーザの動きによってアニメーション開始させたい時に使います
    • どのViewをどの方向にスワイプした時にアニメーションさせるかを指定しています
    • もちろんコードからも動かせるので、無くても大丈夫です
scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragUp"
            app:touchAnchorId="@+id/black"
            app:touchAnchorSide="top" />
    </Transition>
...
  • <ConstraintSet>
    • アニメーションでのレイアウトの状態をまとめます
    • ここで指定したidを<Transition>constraintSetStart/constraintSetEndで指定します
    • ConstraintSetタグで囲まれてる部分でアニメーション開始時/終了時それぞれのレイアウトを組むイメージです
scene_main.xml
...
<ConstraintSet android:id="@+id/start">
...
  • <Constraint>
    • レイアウトの状態を指定します
    • ここで指定するidでレイアウト上の要素が紐付きます
    • id以外の要素を指定することでstart/endでアニメーションを補完してくれます
      • width, height
      • margins
      • ConstraintLayoutの各種制約 (layout_constraintBottom_toBottomOfとか)
      • alpha
      • visibility
      • elevation
      • rotation, rotationX, rotationY
      • translationX, translationY, translationZ
      • scaleX, scaleY
scene_main.xml
...
<Constraint
            android:id="@+id/black"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/red" />
...

作成したMotionSceneをレイアウトに設定

  • MotionLayoutlayoutDescriptionで作成したレイアウトファイルを指定します
activity_main.xml
<androidx.constraintlayout.motion.widget.MotionLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_main">
...

実行

  • 黒のViewをスワイプで赤のViewと入れ替わるようになったかと思います
  • scene_mainでそれぞれのconstraintBottom_toTopOfconstraintTop_toBottomOfの制約を入れ替えたのでその間が補完されてアニメーションするようになってます

発展!

赤Viewもスワイプできるようにしたい

  • モーションのトリガーが増えるので、赤Viewスワイプ用の<Transition>を増やします
  • 今回はアニメーション開始時に赤Viewが上にあるので下スワイプ時にアニメーションするようにdragDirectiontouchAnchorSideを設定します
scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragDown"
            app:touchAnchorId="@+id/red"
            app:touchAnchorSide="bottom" />
</Transition>
...

コードから動かしたい

  • ここまでレイアウトファイル完結でアニメーションさせましたが、実際のところコードで別の処理を走らせつつアニメーションさせることがほとんどかと思います
  • MotionLayout.transitionToEnd()で start -> end のアニメーション、MotionLayout.transitionToStart()でその逆を行うことができます
  • ほかにもいろいろ知りたいよって人はここ見るか自分でやってみて、良さげなのあったら記事にでもしてください
MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var flg = true

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

        root.setOnClickListener {
            if (flg) {
                root.transitionToEnd()
            } else {
                root.transitionToStart()
            }
            flg = !flg
        }
    }
}
  • OnClickListenerも設定する場合は<Transition><OnSwipe>を設定してるとタッチイベントを取られてしまうので取り除いてください
  • ちなみにConstraintLayoutのバグレポートの回答ではプログラムでいい感じにやってねと言われてるので当たり前ですがこれらのタッチイベントが手を取り合うことはないと思います
scene_main.xml
...
<Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000" />
...

余談

  • どのアニメーションにも言えることですが、細かいパーツに分けて考えるとアニメーションさせやすいですね
  • 例えば某ロゴはこんな感じにパーツ分けすると作りやすいかと思います (これFigmaでつくってる時が一番楽しかった)
    part_logo.png

  • これも2つのレイアウトを作るだけでいいので、MotionLayoutで簡単にアニメーションさせられますね

  • ロゴのレイアウトが出来ていれば、アニメーション開始時のレイアウトを組み替えるだけでロゴのレイアウトに組み変わるアニメーションを自動でやってくれます

正方形 複数ブロック 外枠

おわりに

  • レイアウトを2つ作ればアニメーションさせられると考えると、いろいろなことが簡単に出来そうですね
  • ConstraintLayout 2.0が安定版になり、バグも修正されてきているのでこれを期に是非やってみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ名『焼き鳥』事件 ???

本記事は 個人開発 Advent Calendar 2020 の 7 日目の記事です。
前日は @UedaTakeyuki さんの 個人開発でも気軽に使える Copy Protection サービスをつくりました でした!

はじめに

こんにちは あずきしろもち(@azukisiromochi) です。

ここ数年は色に関わる Web サービスを公開してきましたが、今回はそれではなく、僕が個人開発を始めたばかりの頃……それこそ駆け出しプログラマーだった頃に作ったアプリを公開して感じたことを紹介します。
そのときに『とある事』をやらかしてしまったのですが、その際のアプリレビューが個人的に面白かったのでまとめてみました。

これから個人開発をしてみたいと思っている人のきっかけや、励みになればと思い書きましたので、楽しんでもらえると嬉しいです!

公開したアプリ

現在も公開中の Android アプリですが、『 画面消女 ~こんなときに画面消灯しないでよ~ 』というものです。

Android 1.X の終わりから 2.X の始めのころに開発したアプリで、自分が不便だなと感じた

  • 簡単操作で画面が消灯しないようにしたい
  • 同じく簡単操作で画面消灯する設定に戻したい

を実現したものです。

ちなみに、僕は今でも愛用中ですw

やらかしたこと(その1)

このアプリですが比較的好評で、「ポケ○ンGo」がリリースされたときはスリープモードにしたくない需要が増え、たくさんの人に使っていただきました。

ところが 2017 年 4 月……、それは起こりました。
開発用メールアドレスに多数の次のようなメールが届いていたのです。

端末アップデートしたら起動しなくなった

…… :scream: :scream: :scream:

原因

これは Android あるあるですが、バージョンアップのたびに使えない機能が出たり、追加でなにか実装しないといけなかったりがあり、それを対応していなかったためにアプリがクラッシュしていたようです。

対応

徹夜です :innocent:

所感

当時はあたふたしてしまいましたが、今となっては慣れたものです。
(対応サボって 2 回ほどストアから削除されたことがあるのでw)

Android だけではなくプログラミング言語、フレームワークは日々進歩していますので、デベロッパーである僕達もサボらずについていかないといけませんね!
(またストア削除される自信あり!!)

やらかしたこと(その2)

こっちがメインです。
この記事のタイトルにもした「アプリ名『焼き鳥』事件」です。

ことのいきさつ

Android 端末のメニューに表示されるアプリの一覧を見たことがあるでしょうか?
大抵の人が見たことがあると思いますが、このようにアプリの省略名が表示されます。

app_name.png

当時の僕のアプリは『 こんなときに画面消灯しないでよ 』という名称で、長ったるいものでした。
当然、アプリ一覧画面ではアプリ名は途切れ『 こんな… 』と表示されていたのです。
※画像の『カレンダー』が『カレン…』と表示されているような感じですね

徹夜の対応に疲れていた僕は、

「このなっがいアプリ名なんとかならんかな…………。おっ! いいこと考えた!!」

と、思いつきでアプリ名を変えたのでした。

起こったこと

(徹夜の方の)バージョンアップ対応を終え、ストアへの公開も完了。
使ってくれている人の反応を見ようと、ストアのレビューに目を向けました。

rev1.png
rev2.png

ぎゃぁああーーーー!!
えらいこっちゃです!

徹夜明けの働かない脳みそで見たこれらレビューは、本当にこたえたのを覚えています ?
中には「ウイルスかと思った」といったものもあり、さすがに遊びすぎたなと反省しました。

※レビューは再レビューすると古いものが消えてしまうため、残っているもののみ利用しました。日付が前後していることがありますが、ご了承くださいませ

そんななかでも

rev3.png
rev4.png

というような意見もいただきました。

さらに、さらに!

rev5.png

わかってくれる人いたぁああーーーー!! 1
もう感情の振れ幅がすごかったですw

ことのてんまつ

けっきょくアプリ名を変えました :yum:

レビュー欄がプチ炎上状態になりコメントを返す対応に追われている中、僕は次のようなコメントを書きました。

コメントありがとうございます。 アプリ名は文字数が多いためランチャーで途切れてしまうので、アイコンのキャラにちなんだ略称(?)に変更しています。 あまり評判が悪ければ直しますね(^_^;) #略称募集でもしてみようかな

アプリ名を募集しちゃいましたw
(ぜんぜん懲りてない)

すると、別の方がレビューで

rev6.png

こうして『画面消女』が誕生したわけです!w

おわりに

「けっきょく何が言いたかったの?」という声が聞こえてきそうですが、伝えたかったのは 個人開発は使ってくれるユーザーとの距離が近い ということです。

自分が使いたいものを作っているような僕みたいな開発者は、アプリ名も気軽に変えてしまいます。(←いや、お前だけだろ)
それに対してユーザーは、ダメと言ってくれるし、良いとも言ってくれます。

開発者はそんなレビューに一喜一憂しますが、忘れてはいけないのは 自分のアプリを使ってくれている人がいる こと。
それも、レビュー欄でアプリ名が決まってしまうくらい身近な距離に。
とてもじゃないですけど、企業で開発をしていてもこんな体験はできません。

これから個人開発をしてみたいなと思っている人、してみたいと思っているけど踏み出せないでいる人、一度アプリを公開してみてください!

自分の『好きや使いたい』を形にしたら、きっと同じ『好きや使いたい』を持った人に刺さるはずです!


酷いレビューがあると悲しい気持ちになります。


でも、嬉しいレビューがあると蘇ります!


10 個の酷評よりも、 1 個の良いお言葉で蘇るんです!!


だって開発者ってそういうものでしょ!?


じゃじゃぁああーーーーん!!(おわりの効果音)

さ~て明日のアドベントカレンダーは?

技術要素もなく稚拙な文章に最後までお付き合いいただき、ありがとうございました!

アドベントカレンダーはまだまだ続きます。
さて、明日のアドベントカレンダーは……

@soshi1822 さんの 今年初めに防災情報サービスを開始した話 です!

お楽しみに~!!


  1. アプリのアイコンに使っているゲームのキャラクターは、作中で焼き鳥のネタがあるのです 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのRecyclerViewでフォーカスを中央にキープする

AndroidのRecyclerViewでフォーカスを中央にキープする必要があり、ググったのですが日本語の記事がなかったので、記事にします。

やりたいこと

メニュータブを表示する際に、フォーカスを中央にキープする。
※menu1フォーカス中もmenu2フォーカス中もセンターにキープする。
 

ポイントは3つ

  • HorizontalGridViewを使う。(縦の場合はVerticalGridView)
  • windowAlignmentをWINDOW_ALIGN_NO_EDGEに設定する。
  • LayoutManagerを設定しない

実装内容

上記の画像のサンプルでは、ActivityにHorizontalGridViewを表示させています。
※HorizontalGridViewを使うには、Leanbackを依存関係に追加する必要があります。
 https://developer.android.com/jetpack/androidx/releases/leanback?hl=ja#declaring_dependencies

MenuActivity.kt
class MenuActivity : AppCompatActivity() {
    private val viewAdapter = MenuAdapter(arrayOf("menu 1", "menu 2", "menu 3", "menu 4", "menu 5"))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_menu)
        // HorizontalGridViewを使用する
        // LayoutManagerを設定しない
        findViewById<HorizontalGridView>(R.id.recycler_view).apply {
            adapter = viewAdapter
            // windowAlignmentにWINDOW_ALIGN_NO_EDGEを設定
            windowAlignment = WINDOW_ALIGN_NO_EDGE
        }
    }
}
activity_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context="com.ykato.sample.kotlin.MenuActivity">

    <androidx.leanback.widget.HorizontalGridView
        android:id="@+id/recycler_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints">
        <requestFocus/>
    </androidx.leanback.widget.HorizontalGridView>
</androidx.constraintlayout.widget.ConstraintLayout>

RecyclerViewに設定する、AdapterとViewHolderは下記の通り。

MenuAdapter.kt
class MenuAdapter(private val data: Array<String>) :
        RecyclerView.Adapter<MenuAdapter.MenuViewHolder>() {

    class MenuViewHolder(val view: View) : RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MenuViewHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.sample_text_view, parent, false)
        return MenuViewHolder(view)
    }

    override fun onBindViewHolder(holder: MenuViewHolder, position: Int) {
        holder.view.menu_text.text = data[position]
    }

    override fun getItemCount() = data.size
}
sample_text_view
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/menu_text"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:focusable="true"
        android:gravity="center"
        android:textSize="24dp"
        android:background="#AAAAAA"
        tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>

参考

https://stackoverflow.com/questions/56934396/scroll-the-verticalgridview-or-recyclerview-by-keeping-focus-on-center-element

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KeyStoreのjksファイルを復元する方法

拡張子jksは、KeyStoreファイルです。(ここではAndroidアプリ開発で利用)

CIの環境変数に入れようと思ったところ、中身がバイナリでした。

方法

以下でテキストにデコードできます。

openssl base64 -A -in .signing/release.jks

自分の場合は2764文字のテキストが出力されました。

CI上では、↑で出力されたbase64テキストを環境変数に設定して、
以下のようにまたjksにエンコードして使うのが一般的のようです。

echo $KEYSTORE_BASE64 | base64 -d > .signing/release.jks

note

タイトルはよく知らない人(自分)がこんなキーワードで検索する。というのを考えて設定しました。

中身自体はいろんな記事にありますがQiitaはピンポイントで索引できるといいなと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Resources - Sams Class

日本人

SAMS CLASSさんと共有したいと思います。
お楽しみください。
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info

サムスクラスオールドリサーチ:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/old-research.htm

サムスクラスオールドクラス:
https://translate.google.com/translate?hl=&sl=en&tl=ja&u=https%3A%2F%2Fsamsclass.info%2Fold-classes.html

サムスクラスワークショップ-モジュール環境は、連邦政府の標準品質のワークショップに大学レベルのトレーニングを提供します。知識と知恵。

https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/workshops.htm

サムスクラスAndroidとOWASP
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/android/

サムスクラスデフコン:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/defcon.html

サムスクラスCTF:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/CTFs.htm

インストラクタービデオ
https://samsclass.info/videos.html

学生のビデオ
https://samsclass.info/125/proj11/student-videos.htm

注:一部のワークショップでは、特定のIDEが必要になる場合があります-
https://ja.wikipedia.org/wiki/Template:Integrated_development_environments

たとえば、IntelliJIDEA-Androidオープンソース
https://ja.wikipedia.org/wiki/IntelliJ_IDEA
https://www.jetbrains.com/idea/

VSC(vscodium)| VisualStudioコード|オープンソース
https://translate.google.com/translate?sl=en&tl=ja&u=https://github.com/VSCodium/vscodium%23download-install
VisualStudioCodeからVSCodiumに飛行する理由と方法
https://translate.google.com/translate?sl=en&tl=ja&u=https://dev.to/0xdonut/why-and-how-you-should-to-migrate-from-visual-studio-code- to-vscodium-j7d

なぜサムスクラスについての記事なのか?

本質的に、人生のいくつかのことは独学です。
私自身は、トライアルとイーヨーがたくさんあります。

愛好家以上のもの、おそらくマニアックまたは単に頑固で愚かです。
いずれにせよ、私は仕事をあきらめるのは好きではありません。

https://translate.google.com/translate?sl=en&tl=ja&u=https://www.linux.com/training-tutorials/how-kill-process-command-line/

最近では、なじみのない、または不可能なタスクのトラブルシューティングを行うために頻繁に使用されます。
私たちはインターネットに目を向けています。

解決策につながるヒントを探すのは疲れないかもしれませんが、
たまに、非常に貴重であることが証明されているユニークなリソースを見つけます。

SAMSCLASSは全武であると私は信じています。
これまでで最高の1つであり、私が偶然見つけた技術リソースです。 お楽しみください!

English:

I would like to share with you, SAMS CLASS.
Please, enjoy.
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info

Sams Class Old Research:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/old-research.htm

Sams Class Old Classes:
https://translate.google.com/translate?hl=&sl=en&tl=ja&u=https%3A%2F%2Fsamsclass.info%2Fold-classes.html

Sams Class ワークショップ - 開発環境 provides federal government standard quality workshops with university level training. Knowledge and Wisdom.

https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/workshops.htm

Sams Class Android and OWASP
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/android/

Sams Class Defcon:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/defcon.html

Sams Class CTF:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/CTFs.htm

Instructor Videos
https://samsclass.info/videos.html

Student Videos
https://samsclass.info/125/proj11/student-videos.htm

NOTE: Some workshops may require a specific IDE -
https://ja.wikipedia.org/wiki/Template:Integrated_development_environments

For example, IntelliJ IDEA - Android Open Source
https://ja.wikipedia.org/wiki/IntelliJ_IDEA
https://www.jetbrains.com/idea/

VSC (vscodium) | Visual Studio Code | Open Source
https://translate.google.com/translate?sl=en&tl=ja&u=https://github.com/VSCodium/vscodium%23download-install
Visual StudioCodeからVSCodiumに移行する理由と方法
https://translate.google.com/translate?sl=en&tl=ja&u=https://dev.to/0xdonut/why-and-how-you-should-to-migrate-from-visual-studio-code-to-vscodium-j7d

Why an article about Sams Class?

By nature some things in life are self taught.
As for myself, there is much Trial and Eeyore.


More than a hobbyist, possibly a maniac or just stubborn and foolish.
Either way I do not like to give up on a task.

https://translate.google.com/translate?sl=en&tl=ja&u=https://www.linux.com/training-tutorials/how-kill-process-command-line/

So often these days in order to troubleshoot an unfamiliar or impossible task;
We look to the internet.

While it may feel tireless searching for any hints to lead to a solution;
Every once in a while we find a unique resource that proves to be invaluable.

It is my belief that SAMS CLASS is zenbu;
by far, one of the best - technical resource that I have ever accidentally found. Please Enjoy!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テストを簡単にする DI を簡単に説明してみるテスト::Android風味

はじめに

こんにちは。これは Hamee Advent Calendar 2020 6日目の記事です。

これって何の話?

私の現在のお仕事は主に Android アプリの開発なのですが、その中では Dependency Injection (DI) を多用しています。
そのこと自体は今となってはそれほど珍しくないことだと思いますが、DI は少しとっつきにくい概念でもあるので、「DI って何がおいしいの?」とか「よくわからんけど DI が良いらしいから使うべ」という人もいるんじゃないかなと勝手に思ったりしています。
だって「Dependency Injection」でググっても概念的な解説が多くて、なかなか頭に入らないというか、分かったような分からないようなだったりしません?私だけかな。。

というわけでこの記事では、概念的な話からではなく、具体的な例から DI とは何なのかを紐解いてみようかなと思います。ついでに Android のいわゆるモダンな開発での活用方法みたいな話まで持っていこうと思います。

あ、具体的な例から解説していくと、どうしても網羅的な説明は難しくなってしまいます(DI は解説するなら○○も書くべきだ的な)。そこはどうかご理解いただきたいです。。。1

例題1: 円柱の体積を計算する機能

「半径と高さから円柱の体積を計算する」アプリを作ることを考えてみたいと思います。

機能の分解

昨今のソフトウェア開発では複雑な機能をなるべく単純な機能に分解して開発していくのが一般的です。
今回の例題では

  • 機能A : 半径と高さから円柱の体積を計算する

という機能が求められていますが、この機能は少し複雑な気がしないでもないような気がしますので(異論は認めない!)2、以下の2つに機能に分解しましょう。

  • 機能B : 半径から円の面積を計算する
  • 機能C : 面積と高さから体積を計算する

コードにするとこんな感じでしょうか。

object CylinderVolumeCalculator {
    // 機能A: 半径と高さから円柱の体積を計算する
    fun calcCylinderVolume(radius: Double, height: Double): Double {
        return calcVolume(calcCircleArea(radius), height)
    }

    // 機能B: 半径から円の面積を計算する
    private fun calcCircleArea(radius: Double): Double {
        return radius * radius * Math.PI
    }

    // 機能C: 面積と高さから体積を計算する
    private fun calcVolume(area: Double, height: Double): Double {
        return area * height
    }
}

このコードは問題なく動くはずです。本体プログラムから CylinderVolumeCalculator.calcCylinderVolume() 静的メソッドを呼べば正しく動作するでしょう。ですがこういう書き方は、以下のような観点からあまりよろしくないと言われています。

  • 問題1: 分解された機能B、機能Cが非公開のためテストできない。
  • 問題2: 公開されている機能Aもテストしにくい。

問題1: 分解された機能ごとのテストができない

複雑さを嫌って機能を分解したのですから、単体テストも分解された単純な機能ごとに行うべきでしょう。それなのに機能Bと機能Cのメソッドが非公開(privateフィールド)になっていてはテストのしようがありません。まずはこれらを公開メソッドにしてあげましょう。

object CylinderVolumeCalculator {
    // 機能A: 半径と高さから円柱の体積を計算する
    fun calcCylinderVolume(radius: Double, height: Double): Double {
        return calcVolume(calcCircleArea(radius), height)
    }

    // 機能B: 半径から円の面積を計算する
    fun calcCircleArea(radius: Double): Double {
        return radius * radius * Math.PI
    }

    // 機能C: 面積と高さから体積を計算する
    fun calcVolume(area: Double, height: Double): Double {
        return area * height
    }
}

これで機能B, Cの単体テストは問題なく書けるでしょう。

問題2: 公開されている機能Aもテストしにくい

calcCylinderVolume() メソッドは元から公開されていますからテストを書くことはできます。が、ちょっと厄介な問題を抱えています。
このメソッドの機能は、本来的には「入力された半径と高さから円柱の体積を計算して返却する」ですが、機能を分解した結果、単体としての機能(スペック)は以下のように変わっています。

  • 機能Bに半径を渡して円の面積を受け取る。
  • 機能Cに面積と高和を渡して体積を受け取る。
  • 受け取った体積を返却する。

どういうことかというと、仮に機能Bの実装にミスがあって円の面積が間違っていたとしてもそれは機能B側の問題であって機能Aの問題ではありません。同様に機能Cの実装にミスがあって体積が間違っていたとしてもそれは機能Cの問題であって機能Aの問題ではありません。機能Aの役割はあくまで、受け取ったデータを機能Bや機能Cに渡したり受け取ったり返却したりすることだけです。言い換えれば、機能Aの単体テストは、受け取ったデータを正しく機能Bや機能Cに渡したり返却したりできているかだけを確認できれば良いことになります(もちろん機能Bと機能Cは十分にテストされていることが前提となります)。

このような場合、機能B、機能Cの想定される正しい挙動を模したモックを用意して機能Aのテストを行うのが一般的です。しかし calcCylinderVolume() の実装は、calcCircleArea() メソッドや calcVolume() メソッドを直接呼んでしまっているため、モックの使いようがありません。どうしたものでしょうか?

モックを使えるようにする

機能B、機能Cのモックを作成し、また使えるようにするには、一般に各機能のインタフェースを用意します。3

// 機能Bのインタフェース
interface CalcCircleAreaFunction {
    companion object {
        fun newInstance(): CalcCircleAreaFunction = CalcCircleAreaFunctionImpl()
    }

    fun calcCircleArea(radius: Double): Double
}

// 機能Bの実装
class CalcCircleAreaFunctionImpl : CalcCircleAreaFunction {
    override fun calcCircleArea(radius: Double): Double {
        return radius * radius * Math.PI
    }
}

// 機能Cのインタフェース
interface CalcVolumeFunction {
    companion object {
        fun newInstance(): CalcVolumeFunction = CalcVolumeFunctionImpl()
    }

    fun calcVolume(area: Double, height: Double): Double
}

// 機能Cの実装
class CalcVolumeFunctionImpl : CalcVolumeFunction {
    override fun calcVolume(area: Double, height: Double): Double {
        return area * height
    }
}

こんな感じですね。ちなみに各インタフェースにファクトリメソッド(newInstance())を用意しているのは、これらの機能を利用する側が実装クラス(CalcCircleAreaFunctionImplCalcVolumeFunctionImpl)を意識しなくて済むようにするためです。こうすれば、機能Aはとりあえず以下のように書き換えることができます。

    // 機能Aの実装(円柱の体積を計算する)
    fun calcCylinderVolume(
        radius: Double,
        height: Double,
        calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
        calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
    ): Double {
        return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
    }

機能Aのメソッド calcCylinderVolume() に引数を追加して機能B、機能Cの関数オブジェクトを受け取れるように拡張しています。デフォルトで各機能の通常の実装が渡されるようになっているため calcCylinderVolume() メソッドの使い方に変化はありませんが、単体テストではこれらの引数に各機能のモックオブジェクトを渡すことで calcCylinderVolume() メソッドをテストできます。たとえば以下のように書けるでしょう。45

class CylinderVolumeCalculatorTest {
    @Test
    fun calcCylinderVolume_success() {
        // テストで使うパラメータ
        val requestRadius = 2.0
        val requestHeight = 3.0

        // 機能Bのモックが返す値
        val expectArea = requestRadius * requestRadius * 3.14

        // 機能Cのモックが返す値
        val expectVolume = expectArea * requestHeight

        // 機能Bのモック
        val calcCircleAreaFunctionMock = object : CalcCircleAreaFunction {
            override fun calcCircleArea(radius: Double): Double {
                // 機能Aは機能Bに対して requestRadius の値を渡していなければならない
                assertThat(radius, `is`(requestRadius))
                return expectArea
            }
        }

        // 機能Cのモック
        val calcVolumeFunctionMock = object : CalcVolumeFunction {
            override fun calcVolume(area: Double, height: Double): Double {
                // 機能Aは機能Cに対して requestHeight の値を渡していなければならない
                assertThat(height, `is`(requestHeight))
                // 同様に機能Aは機能Cに、機能Bが返した expectArea の値を渡していなければならない
                assertThat(area, `is`(expectArea))
                return expectVolume
            }
        }

        // テスト対象のメソッドを実行する
        // その際、機能B、機能Cのモックを渡す
        val volume = CylinderVolumeCalculator.calcCylinderVolume(
            requestRadius,
            requestHeight,
            calcCircleAreaFunctionMock,
            calcVolumeFunctionMock
        )
        // 機能Aは機能Cが返した expectVolume の値を返却しなければならない
        assertThat(volume, `is`(expectVolume))
    }
}

依存する機能の「注入」とは

ここでもう一度、機能A(calcCylinderVolume メソッド)の実装を見てみましょう。

    // 機能Aの実装(円柱の体積を計算する)
    fun calcCylinderVolume(
        radius: Double,
        height: Double,
        calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
        calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
    ): Double {
        return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
    }

機能Aは機能B(calcCircleAreaFunction)と機能C(calcVolumeFunction)を、引数として受け取り、そしてそれらを利用して、自分自身の機能を実現しています。機能Aは機能Bと機能Cに依存していて、それら「依存する機能」が外部から送り込まれているようにも見えます。そのためこのような設計パターンは Dependency Injection (DI) と呼ばれています(個人的にはこの名称はあまり好きではないのですが…)。

ただし、一般に DI と言った場合、上記の実装のように依存する機能を直接メソッドに渡すことはあまりありません。というのも、機能Aを使う側(calcCylinderVolume メソッドを呼び出す側)から見ると、本来なら半径と高さの2つのパラメータさえ渡せば良いはずのメソッドなのに、なんだかよく分からない引数を余計に渡さないといけないように見えてしまうからです。それに第3引数と第4引数を渡さなかった場合、つまりデフォルト値が利用された場合、CalcCircleAreaFunctionImplクラスとCalcVolumeFunctionImplクラスがインスタンス化されてしまうので、機能Aが連続して呼ばれるようなシーンではパフォーマンス面でも問題になるかも知れません。

そのため依存する機能はメソッドに直接渡すのではなく、そのメソッドが属するクラスに渡すのが一般的だと思います。

class CylinderVolumeCalculator(
    private val calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
    private val calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
) {
    // 機能A: 半径と高さから円柱の体積を計算する
    fun calcCylinderVolume(radius: Double, height: Double): Double {
        return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
    }
}

この例ではクラスのコンストラクタで依存する機能を渡すようにしていますが、クラスがインスタンス化されたあとに渡すようにすることもあります。
ちなみにこうした場合、テストコードは以下のように書き換える必要があります(変更箇所のみ抜粋)。

        // テスト対象のメソッドを実行する
        // その際、機能B、機能Cのモックを渡す
        val volume = CylinderVolumeCalculator(calcCircleAreaFunctionMock, calcVolumeFunctionMock)
            .calcCylinderVolume(requestRadius, requestHeight)

例題2: BGM の再生と停止をする機能

次に、もう少し Android アプリ開発っぽい例を考えてみましょう。
アプリ内で BGM を再生したり停止したりする機能を作成してみようと思います。

ざっと実装

Android で音楽を再生するにはMediaPlayerクラスを使うのが一般的だと思います。そこでMediaPlayerクラスを簡単に使うためのラッパークラスを書いてみましょう。

interface BgmPlayer {
    companion object {
        fun newInstance(context: Context): BgmPlayer =
            BgmPlayerImpl(context)
    }

    // 再生
    fun play()
    // 停止
    fun stop()
}

class BgmPlayerImpl(context: Context) : BgmPlayer {
    private val mediaPlayer = MediaPlayer.create(context, R.raw.battle_bgm).apply {
        isLooping = true
    }

    // 再生
    override fun play() {
        mediaPlayer.start()
    }

    // 停止
    override fun stop() {
        mediaPlayer.stop()
    }
}

BgmPlayerImplクラスは問題なく動作するはずです。ですがこの実装にはいくつか問題があります。一つずつ見ていきましょう。

問題1: BgmPlayerImplクラス自体をテストしにくい

これは例題1でも問題になった「依存する機能を内包してしまっている」ことに起因します。今回の例題ではBgmPlayerImplクラスが内部的にMediaPlayerクラスに依存しており、かつ private メンバとして保持しているためモックに置き換えることができません。そこで例題1でやったのと同じように、MediaPlayerをコンストラクタで渡せるようにしてみましょう。

interface BgmPlayer {
    companion object {
        fun newInstance(mediaPlayer: MediaPlayer): BgmPlayer =
            BgmPlayerImpl(mediaPlayer)

        fun newInstance(context: Context): BgmPlayer =
            BgmPlayerImpl(
                MediaPlayer.create(context, R.raw.battle_bgm).apply {
                    isLooping = true
                }
            )
    }

    // 再生
    fun play()
    // 停止
    fun stop()
}

class BgmPlayerImpl(
    private val mediaPlayer: MediaPlayer
) : BgmPlayer {
    // 再生
    override fun play() {
        mediaPlayer.start()
    }

    // 停止
    override fun stop() {
        mediaPlayer.stop()
    }
}

BgmPlayerImplクラスのコンストラクタはMediaPlayerを受け取るようにして、「ContextからMediaPlayerを生成する」という処理はファクトリメソッド側で行うよう修正しました。6
これでBgmPlayerImplクラスにMediaPlayerを注入できるようになりましたね。
こうすればBgmPlayerImplクラスの単体テストは、たとえば以下のように書けそうです。

class BgmPlayerTest {
    @Test
    fun start_and_stop_success() {
        var isStartCalled = false
        var isStopCalled = false

        // MediaPlayer のモックを作る
        val mediaPlayerMock = object : MediaPlayer() {
            override fun start() {
                isStartCalled = true
            }

            override fun stop() {
                isStopCalled = true
            }
        }

        // テスト対象のクラスをインスタンス化する
        // ただし MediaPlayer にはモックを使う
        val bgmPlayer = BgmPlayerImpl(mediaPlayerMock)

        // play メソッドのテスト: MediaPlayer の start メソッドが呼ばれること
        bgmPlayer.play()
        assertTrue(isStartCalled)

        // stop メソッドのテスト: MediaPlayer の stop メソッドが呼ばれること
        bgmPlayer.stop()
        assertTrue(isStopCalled)
    }
}

問題2: 複数個所から呼ばれる場合どうするか

BGM の再生/停止は、アプリの色々な箇所から呼ばれる可能性があります。画面が複数あるアプリでは、たとえば画面Aで BGM の再生を開始し、画面Bに遷移してから BGM を停止するということも十分考えられます。そのような場合、たとえば以下のように実装しても良いものでしょうか?

FragmentA.kt
    private val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(requireContext()) }

    // 画面Aの Fragment で BGM 再生開始
    private fun playBgm() {
        bgmPlayer.play()
    }
FragmentB.kt
    private val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(requireContext()) }

    // 画面Bの Fragment で BGM 停止
    private fun stopBgm() {
        bgmPlayer.stop()
    }

現在の Android アプリの開発では、多くの場合、画面ごとに異なる Fragment を用意してその Fragment ごとに動作を制御するのが一般的です。上記の例でもそれに倣って、画面Aの Fragment (FragmentA) と画面Bの Fragment (FragmentB) にそれぞれ再生と停止のコードを入れてみました。ですがこれでは正しく動作しません。なぜならそれぞれの箇所でBgmPlayerImplクラスの新しいインスタンスを生成してしまっているためです。正常に動作させるにはFragmentAFragmentBで同じインスタンスを共有する必要があります。異なる Fragment 間でオブジェクトを共有するにはどうすれば良いでしょうか?

その一つの方法として、共有したいオブジェクトをApplicationクラスの派生クラスに保持させることが考えられます7。たとえば以下のようにします。

DIContainer.kt
// アプリ内で共有するオブジェクトのコンテナ
class DIContainer(context: Context) {
    val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(context) }
}
MainApplication.kt
class MainApplication : Application() {
    lateinit var diContainer: DIContainer
        private set

    override fun onCreate() {
        super.onCreate()
        diContainer = DIContainer(applicationContext)
    }
}

こうしておけば、各 Fragment で同一インスタンスの BgmPlayer を以下のように取得できます。

FragmentA.kt
class FragmentA : Fragment() {
    private lateinit var diContainer: DIContainer
    private lateinit var bgmPlayer: BgmPlayer

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        diContainer = (requireActivity().application as MainApplication).diContainer
        bgmPlayer = diContainer.bgmPlayer
    }
}

これが DI コンテナを使った DI となります。これまで見てきた、コンストラクタを介しての「機能の注入」とは少し勝手が違いますが、「特定の機能を外部から受け取っている」という意味で同じ意味合いを持ちます。

Hilt の導入

ここまで DI の基本的な考え方を書いてきました。単体テストを書く上で多くのメリットがある手法であることは確かなようですね。
その一方で DI を導入するには少し面倒くさいコードを書かなければならないというデメリットがあることも分かったと思います。例題2で見たDIContainerの実装と、それを使えるようにするためのApplicationクラスやFragmentへのコードの追加がそれです。

ですがこれらの面倒くさいコードは、便利な DI コンテナライブラリを導入することで多くの部分を省略できるようになります。ここでは Hilt というライブラリを導入して例題2のコードを書き換えてみます。

なお、この記事では Hilt のさわり程度しか解説しません。より詳しく知りたい方はこちら等を参考にしてみてください。

Hilt の初期設定

まずは開発環境に Hilt を組み込みます。

build.gradle
buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}
app/build.gradle
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Applicationクラスにアノテーションを付ける

次にApplicationクラス(の派生クラス)に@HiltAndroidAppのアノテーションを付けます。
また Hilt の導入によって自前の DI コンテナは不要になるためDIContainerのインスタンス化やそれを保持するためのインスタンス変数は不要になります。つまりMainApplicationクラスの定義は以下だけで十分となります。

MainApplication.kt
@HiltAndroidApp
class MainApplication : Application()

ActivityFragmentにもアノテーションを付ける

さらにActivityFragment(の派生クラス)に@AndroidEntryPointのアノテーションを付けます。

MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // 省略
}
FragmentA.kt
@AndroidEntryPoint
class FragmentA : Fragment() {
    // 省略
}
FragmentB.kt
@AndroidEntryPoint
class FragmentB : Fragment() {
    // 省略
}

Hilt モジュールを作る

Hilt を使うには DI して使いたいクラス、つまり例題2でいうBgmPlayerのインスタンス化の方法を Hilt に知らせる必要があります。そのためには Hilt モジュールと呼ばれる object を定義する必要があります。この object は名前は何でも構いませんが@Module, @InstallInのアノテーションを付ける必要があります。また@Providesアノテーションを付けたメソッドを定義する必要もあります。

BgmPlayerModule.kt
@Module
@InstallIn(ApplicationComponent::class)
object BgmPlayerModule {
    @Provides
    fun provideBgmPlayer(
        @ApplicationContext context: Context
    ): BgmPlayer = BgmPlayer.newInstance(context)
}

これでBgmPlayerの機能が要求されたときのBgmPlayerのインスタンス化の方法を Hilt に教えることができました。

BgmPlayerImplクラスをシングルトンにする

上記の設定でBgmPlayerのインスタンス化の方法を規定できましたが、このままだとBgmPlayerを注入するたびに新しいBgmPlayerImplインスタンスが生成されてしまいます。例題2でも見たように、このクラスは複数個所から同じインスタンスを使われる想定になっています。つまりシングルトンである必要があります。
Hilt ではシングルトンにしたいクラスに@Singletonアノテーションを付けるだけでそれを実現できます。

@Singleton
class BgmPlayerImpl(
    private val mediaPlayer: MediaPlayer
) : BgmPlayer {
    // 省略
}

これでお膳立ては整いました。

BgmPlayerの注入

以上で Hilt を使ってBgmPlayerを DI できるようになりました。
たとえば例題2で見たのと同じように Fragment でBgmPlayerを使いたい場合は以下のように注入できます。

FragmentA.kt
@AndroidEntryPoint
class FragmentA : Fragment() {
    // BgmPlayerを注入する
    @Inject
    lateinit var bgmPlayer: BgmPlayer

    // BGMを再生する
    fun playBgm() {
        bgmPlayer.play()
    }
}

インスタンス変数bgmPlayerは初期化されていないように見えますが、@Injectアノテーションを付けることで Hilt が自動的に初期化してくれます。その際 Hilt モジュール(上で定義したBgmPlayerModule object)が参照され、適切にBgmPlayerImplクラスがインスタンス化されます。あるいは、すでにBgmPlayerImplのインスタンスが存在する場合はそれが利用されます(@Singletonアノテーションがあるため)。

例題2では DI の仕組みを自前で実装しましたが Hilt を使えば「お作法」に従って書いていくだけで済みます。自分でごちゃごちゃ考える必要がなくなりますし、コードもすっきりして見通しが良くなります。例題2でやった自前実装とほぼ同じことを Hilt が自動的にやってくれているわけですね。

おまけ:ViewModel への注入

MVVM で開発する場合は ViewModel に注入することも可能です。

app/build.gradle
android {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation "androidx.fragment:fragment-ktx:1.2.5"
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}
AViewModel.kt
class AViewModel @ViewModelInject constructor(
    // BgmPlayerを注入する
    private val bgmPlayer: BgmPlayer
) : ViewModel() {
    // BgmPlayerを使ってBGMを再生する
    fun playBgm() {
        bgmPlayer.play()
    }
}
FragmentA.kt
@AndroidEntryPoint
class FragmentA : Fragment() {
    private val viewModel: AViewModel by viewModels()

    // AViewModel#playBgm()メソッドを呼んでBGMを再生する
    fun playBgm() {
        viewModel.playBgm()
    }
}

おわりに

初めて Dependency Injection という言葉を耳にしたとき皆さんはどう感じたでしょうか?「はぁ?なんだそりゃ?」と思った人が多いのではないでしょうか?私もそのうちの一人です。私は中毒患者じゃないぞ。
DI のとっつきにくさはその名称にも原因があるんじゃないかなと思っています。依存性の注入…本当になんだそりゃですよ。。。

なのでこの記事では DI という言葉に振り回されないよう、どんなところからこういう考え方が生まれたのかという「ことの発端」みたいなところから書いてみたつもりです。またその話を「ふーん、概念はなんとなく分かった」で終わらせず、最新のモダンな開発で活かせるよう Hilt の使い方まで引っ張りました。執筆時間をあまり取れなかったのでだいぶ端折ってしまいましたが、参考にしてくれる人が少しでもいてくれたら嬉しいです。


  1. マサカリはいったん床に置きましょう! 

  2. 言うまでもないと思いますが、こんな簡単な処理をわざわざ分解するなんて、普通はやらないと思います。 

  3. Kotlin ならインタフェースじゃなく高階関数を使えば良いんじゃない?というツッコミが飛んできそうですが、はい、この例ではそうですよね。ですがモック化する対象のオブジェクトは通常、複数のメソッドを持っていますので、インタフェースを使うということでご理解いただきたく…(例が悪いという説) 

  4. この記事ではモックをすべて自前で実装していますが MockitoMockK を使えばもっと簡単に実装できます。 

  5. このテストコード、「円柱の体積の計算なんてめっちゃ単純な処理のためにこんな面倒なテスト書かなきゃいけないんかい!」というツッコミをもらえそうですね。もちろんこれは「DI とは何か」を解説するために単純な処理を題材にしたからなのですが、ある意味、「必要以上に機能を分解してもデメリットが多い」ことを示す例にもなっていますね。 

  6. 「ContextからMediaPlayerを生成する」処理をBgmPlayerImplのセカンダリコンストラクタとして実装する方法もありますが、「MediaPlayerの生成処理はテスト対象外である」ことが明示的になるようファクトリメソッド側に書いてしまうのが個人的には好みです。 

  7. アプリ全体で共有するオブジェクトを Application クラスに保持させる理由は以前書いた記事をご参照いただければ! 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]HomebrewでAndroidSDKをインストールする。

  • Mac利用者で、Homebrewを利用してソフトウェアを管理している方、AndroidSDKHomebrewで管理したい方向けです。

android-sdkのインストール

  • インストール方法は以下です。
$ brew install android-sdk

platform-toolsのインストール

  • Homebrew Caskplatform-toolsというFormulaeがありそちらからもインストールできますが、先ほどインストールしたandroid-sdkに付属しているsdkmanagerを利用してインストールします。
  • android-sdkは不要で、adbfastbootを利用したい場合、platform-tools単体でインストールすることも可能です。 そのため独立したFormulaeが用意されているのでしょう。
  • SDK Platform-Tools リリースノート  |  Android デベロッパー  |  Android Developersに以下の記載があるように、sdkmanagerでインストールしましょう。

Android デベロッパーは、最新の SDK Platform-Tools を Android Studio の SDK Manager から、または sdkmanager コマンドライン ツールから取得する必要があります。

  • Android Studiointellijsdkmanagerでインストールしたplatform-toolsが利用されます。
  • インストール方法は以下です。
$ sdkmanager platform-tools
  • また、インストールしたplatform-toolsのパスをzprofilezshrcなどお好みの方法で通します。
$ export PATH=/usr/local/share/android-sdk/platform-tools:$PATH
  • これでadbfastbootが利用できるようになっているはずです。

以上

  • これで以上になります。Android StudiointellijなどのIDEに以下android-sdkのパスを設定することでHomebrewでインストールしたandroid-sdkを利用して開発を行うことができます。
/usr/local/share/android-sdk
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む