- 投稿日:2020-11-28T23:27:08+09:00
【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.ktclass 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.ktclass 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へ値を渡すことが出来ます。
- 投稿日:2020-11-28T22:46:01+09:00
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.gradledependencies { ... 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をレイアウトに設定
MotionLayout
のlayoutDescription
で作成したレイアウトファイルを指定します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_toTopOf
とconstraintTop_toBottomOf
の制約を入れ替えたのでその間が補完されてアニメーションするようになってます発展!
赤Viewもスワイプできるようにしたい
- モーションのトリガーが増えるので、赤Viewスワイプ用の
<Transition>
を増やします- 今回はアニメーション開始時に赤Viewが上にあるので下スワイプ時にアニメーションするように
dragDirection
とtouchAnchorSide
を設定します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.ktclass 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" /> ...余談
- どのアニメーションにも言えることですが、細かいパーツに分けて考えるとアニメーションさせやすいですね
これも2つのレイアウトを作るだけでいいので、MotionLayoutで簡単にアニメーションさせられますね
ロゴのレイアウトが出来ていれば、アニメーション開始時のレイアウトを組み替えるだけでロゴのレイアウトに組み変わるアニメーションを自動でやってくれます
正方形 複数ブロック 外枠 おわりに
- レイアウトを2つ作ればアニメーションさせられると考えると、いろいろなことが簡単に出来そうですね
- ConstraintLayout 2.0が安定版になり、バグも修正されてきているのでこれを期に是非やってみてください!
- 投稿日:2020-11-28T19:41:54+09:00
アプリ名『焼き鳥』事件 ???
本記事は 個人開発 Advent Calendar 2020 の 7 日目の記事です。
前日は @UedaTakeyuki さんの 個人開発でも気軽に使える Copy Protection サービスをつくりました でした!はじめに
こんにちは あずきしろもち(@azukisiromochi) です。
ここ数年は色に関わる Web サービスを公開してきましたが、今回はそれではなく、僕が個人開発を始めたばかりの頃……それこそ駆け出しプログラマーだった頃に作ったアプリを公開して感じたことを紹介します。
そのときに『とある事』をやらかしてしまったのですが、その際のアプリレビューが個人的に面白かったのでまとめてみました。これから個人開発をしてみたいと思っている人のきっかけや、励みになればと思い書きましたので、楽しんでもらえると嬉しいです!
公開したアプリ
現在も公開中の
Android
アプリですが、『 画面消女 ~こんなときに画面消灯しないでよ~ 』というものです。
Android 1.X
の終わりから2.X
の始めのころに開発したアプリで、自分が不便だなと感じた
- 簡単操作で画面が消灯しないようにしたい
- 同じく簡単操作で画面消灯する設定に戻したい
を実現したものです。
ちなみに、僕は今でも愛用中ですw
やらかしたこと(その1)
このアプリですが比較的好評で、「ポケ○ンGo」がリリースされたときはスリープモードにしたくない需要が増え、たくさんの人に使っていただきました。
ところが 2017 年 4 月……、それは起こりました。
開発用メールアドレスに多数の次のようなメールが届いていたのです。端末アップデートしたら起動しなくなった
……
原因
これは
Android
あるあるですが、バージョンアップのたびに使えない機能が出たり、追加でなにか実装しないといけなかったりがあり、それを対応していなかったためにアプリがクラッシュしていたようです。対応
徹夜です
所感
当時はあたふたしてしまいましたが、今となっては慣れたものです。
(対応サボって 2 回ほどストアから削除されたことがあるのでw)Android だけではなくプログラミング言語、フレームワークは日々進歩していますので、デベロッパーである僕達もサボらずについていかないといけませんね!
(またストア削除される自信あり!!)やらかしたこと(その2)
こっちがメインです。
この記事のタイトルにもした「アプリ名『焼き鳥』事件」です。ことのいきさつ
Android
端末のメニューに表示されるアプリの一覧を見たことがあるでしょうか?
大抵の人が見たことがあると思いますが、このようにアプリの省略名が表示されます。当時の僕のアプリは『 こんなときに画面消灯しないでよ 』という名称で、長ったるいものでした。
当然、アプリ一覧画面ではアプリ名は途切れ『 こんな… 』と表示されていたのです。
※画像の『カレンダー』が『カレン…』と表示されているような感じですね徹夜の対応に疲れていた僕は、
「このなっがいアプリ名なんとかならんかな…………。おっ! いいこと考えた!!」
と、思いつきでアプリ名を変えたのでした。
起こったこと
(徹夜の方の)バージョンアップ対応を終え、ストアへの公開も完了。
使ってくれている人の反応を見ようと、ストアのレビューに目を向けました。ぎゃぁああーーーー!!
えらいこっちゃです!徹夜明けの働かない脳みそで見たこれらレビューは、本当にこたえたのを覚えています ?
中には「ウイルスかと思った」といったものもあり、さすがに遊びすぎたなと反省しました。※レビューは再レビューすると古いものが消えてしまうため、残っているもののみ利用しました。日付が前後していることがありますが、ご了承くださいませ
そんななかでも
というような意見もいただきました。
さらに、さらに!
わかってくれる人いたぁああーーーー!! 1
もう感情の振れ幅がすごかったですwことのてんまつ
けっきょくアプリ名を変えました
レビュー欄がプチ炎上状態になりコメントを返す対応に追われている中、僕は次のようなコメントを書きました。
コメントありがとうございます。 アプリ名は文字数が多いためランチャーで途切れてしまうので、アイコンのキャラにちなんだ略称(?)に変更しています。 あまり評判が悪ければ直しますね(^_^;) #略称募集でもしてみようかな
アプリ名を募集しちゃいましたw
(ぜんぜん懲りてない)すると、別の方がレビューで
こうして『画面消女』が誕生したわけです!w
おわりに
「けっきょく何が言いたかったの?」という声が聞こえてきそうですが、伝えたかったのは 個人開発は使ってくれるユーザーとの距離が近い ということです。
自分が使いたいものを作っているような僕みたいな開発者は、アプリ名も気軽に変えてしまいます。(←いや、お前だけだろ)
それに対してユーザーは、ダメと言ってくれるし、良いとも言ってくれます。開発者はそんなレビューに一喜一憂しますが、忘れてはいけないのは 自分のアプリを使ってくれている人がいる こと。
それも、レビュー欄でアプリ名が決まってしまうくらい身近な距離に。
とてもじゃないですけど、企業で開発をしていてもこんな体験はできません。これから個人開発をしてみたいなと思っている人、してみたいと思っているけど踏み出せないでいる人、一度アプリを公開してみてください!
自分の『好きや使いたい』を形にしたら、きっと同じ『好きや使いたい』を持った人に刺さるはずです!
酷いレビューがあると悲しい気持ちになります。
でも、嬉しいレビューがあると蘇ります!
10 個の酷評よりも、 1 個の良いお言葉で蘇るんです!!
だって開発者ってそういうものでしょ!?
じゃじゃぁああーーーーん!!(おわりの効果音)
さ~て明日のアドベントカレンダーは?
技術要素もなく稚拙な文章に最後までお付き合いいただき、ありがとうございました!
アドベントカレンダーはまだまだ続きます。
さて、明日のアドベントカレンダーは……@soshi1822 さんの 今年初めに防災情報サービスを開始した話 です!
お楽しみに~!!
アプリのアイコンに使っているゲームのキャラクターは、作中で焼き鳥のネタがあるのです ↩
- 投稿日:2020-11-28T13:53:48+09:00
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_dependenciesMenuActivity.ktclass 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.ktclass 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>参考
- 投稿日:2020-11-28T12:16:47+09:00
KeyStoreのjksファイルを復元する方法
拡張子jksは、KeyStoreファイルです。(ここではAndroidアプリ開発で利用)
CIの環境変数に入れようと思ったところ、中身がバイナリでした。
方法
以下でテキストにデコードできます。
openssl base64 -A -in .signing/release.jks自分の場合は2764文字のテキストが出力されました。
CI上では、↑で出力されたbase64テキストを環境変数に設定して、
以下のようにまたjksにエンコードして使うのが一般的のようです。echo $KEYSTORE_BASE64 | base64 -d > .signing/release.jksnote
タイトルはよく知らない人(自分)がこんなキーワードで検索する。というのを考えて設定しました。
中身自体はいろんな記事にありますがQiitaはピンポイントで索引できるといいなと思いました。
- 投稿日:2020-11-28T02:37:00+09:00
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なぜサムスクラスについての記事なのか?
本質的に、人生のいくつかのことは独学です。
私自身は、トライアルとイーヨーがたくさんあります。愛好家以上のもの、おそらくマニアックまたは単に頑固で愚かです。
いずれにせよ、私は仕事をあきらめるのは好きではありません。最近では、なじみのない、または不可能なタスクのトラブルシューティングを行うために頻繁に使用されます。
私たちはインターネットに目を向けています。解決策につながるヒントを探すのは疲れないかもしれませんが、
たまに、非常に貴重であることが証明されているユニークなリソースを見つけます。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.infoSams Class Old Research:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/old-research.htmSams Class Old Classes:
https://translate.google.com/translate?hl=&sl=en&tl=ja&u=https%3A%2F%2Fsamsclass.info%2Fold-classes.htmlSams 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.htmlSams Class CTF:
https://translate.google.com/translate?sl=en&tl=ja&u=https://samsclass.info/CTFs.htmInstructor Videos
https://samsclass.info/videos.htmlStudent Videos
https://samsclass.info/125/proj11/student-videos.htmNOTE: Some workshops may require a specific IDE -
https://ja.wikipedia.org/wiki/Template:Integrated_development_environmentsFor 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-j7dWhy 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.
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!
- 投稿日:2020-11-28T02:05:13+09:00
テストを簡単にする 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()
)を用意しているのは、これらの機能を利用する側が実装クラス(CalcCircleAreaFunctionImpl
とCalcVolumeFunctionImpl
)を意識しなくて済むようにするためです。こうすれば、機能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()
メソッドをテストできます。たとえば以下のように書けるでしょう。45class 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.ktprivate val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(requireContext()) } // 画面Aの Fragment で BGM 再生開始 private fun playBgm() { bgmPlayer.play() }FragmentB.ktprivate 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
クラスの新しいインスタンスを生成してしまっているためです。正常に動作させるにはFragmentA
とFragmentB
で同じインスタンスを共有する必要があります。異なる Fragment 間でオブジェクトを共有するにはどうすれば良いでしょうか?その一つの方法として、共有したいオブジェクトを
Application
クラスの派生クラスに保持させることが考えられます7。たとえば以下のようにします。DIContainer.kt// アプリ内で共有するオブジェクトのコンテナ class DIContainer(context: Context) { val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(context) } }MainApplication.ktclass MainApplication : Application() { lateinit var diContainer: DIContainer private set override fun onCreate() { super.onCreate() diContainer = DIContainer(applicationContext) } }こうしておけば、各 Fragment で同一インスタンスの
BgmPlayer
を以下のように取得できます。FragmentA.ktclass 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.gradlebuildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' } }app/build.gradleapply 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()
Activity
やFragment
にもアノテーションを付けるさらに
Activity
やFragment
(の派生クラス)に@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.gradleandroid { 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.ktclass 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 の使い方まで引っ張りました。執筆時間をあまり取れなかったのでだいぶ端折ってしまいましたが、参考にしてくれる人が少しでもいてくれたら嬉しいです。
マサカリはいったん床に置きましょう! ↩
言うまでもないと思いますが、こんな簡単な処理をわざわざ分解するなんて、普通はやらないと思います。 ↩
Kotlin ならインタフェースじゃなく高階関数を使えば良いんじゃない?というツッコミが飛んできそうですが、はい、この例ではそうですよね。ですがモック化する対象のオブジェクトは通常、複数のメソッドを持っていますので、インタフェースを使うということでご理解いただきたく…(例が悪いという説) ↩
この記事ではモックをすべて自前で実装していますが Mockito や MockK を使えばもっと簡単に実装できます。 ↩
このテストコード、「円柱の体積の計算なんてめっちゃ単純な処理のためにこんな面倒なテスト書かなきゃいけないんかい!」というツッコミをもらえそうですね。もちろんこれは「DI とは何か」を解説するために単純な処理を題材にしたからなのですが、ある意味、「必要以上に機能を分解してもデメリットが多い」ことを示す例にもなっていますね。 ↩
「ContextからMediaPlayerを生成する」処理を
BgmPlayerImpl
のセカンダリコンストラクタとして実装する方法もありますが、「MediaPlayerの生成処理はテスト対象外である」ことが明示的になるようファクトリメソッド側に書いてしまうのが個人的には好みです。 ↩アプリ全体で共有するオブジェクトを Application クラスに保持させる理由は以前書いた記事をご参照いただければ! ↩
- 投稿日:2020-11-28T01:57:35+09:00
[Android]HomebrewでAndroidSDKをインストールする。
- Mac利用者で、
Homebrew
を利用してソフトウェアを管理している方、AndroidSDK
もHomebrew
で管理したい方向けです。
android-sdk
のインストール
- インストール方法は以下です。
$ brew install android-sdk
platform-tools
のインストール
Homebrew Cask
にplatform-tools
というFormulae
がありそちらからもインストールできますが、先ほどインストールしたandroid-sdk
に付属しているsdkmanager
を利用してインストールします。android-sdk
は不要で、adb
やfastboot
を利用したい場合、platform-tools
単体でインストールすることも可能です。 そのため独立したFormulae
が用意されているのでしょう。- SDK Platform-Tools リリースノート | Android デベロッパー | Android Developersに以下の記載があるように、
sdkmanager
でインストールしましょう。
Android デベロッパーは、最新の SDK Platform-Tools を Android Studio の SDK Manager から、または sdkmanager コマンドライン ツールから取得する必要があります。
Android Studio
やintellij
はsdkmanager
でインストールしたplatform-tools
が利用されます。- インストール方法は以下です。
$ sdkmanager platform-tools
- また、インストールした
platform-tools
のパスをzprofile
やzshrc
などお好みの方法で通します。$ export PATH=/usr/local/share/android-sdk/platform-tools:$PATH
- これで
adb
やfastboot
が利用できるようになっているはずです。以上
- これで以上になります。
Android Studio
やintellij
などのIDEに以下android-sdk
のパスを設定することでHomebrew
でインストールしたandroid-sdk
を利用して開発を行うことができます。/usr/local/share/android-sdk