- 投稿日:2020-09-20T23:06:08+09:00
String型キーをやめてマーカーインタフェースを導入する
はじめに
Javaのライブラリでは、キーやプロパティ名などオブジェクトを識別するための値(以下「キー」とします。)としてString型のオブジェクトを用いるものをよく目にします。例えば、プロパティ・リストの導入に利用する
java.util.Properties
クラスであったり、プロパティの変更の通知などイベントの送信に利用するjava.beans.PropertyChangeSupport
クラスであったり、古いだの衰退しているだのと言われJavaFxと常に比較されるSwingのレイアウトマネージャーの一つであるjava.awt.CardLayout
クラスであったり・・・。String型を用いる手法では、その値に誤字脱字を含んでいたり、実装の変更に伴いキーの名前が変更したりすると、オブジェクトを正しく参照できずに不具合が生じることがあります。
キー名の誤字による値の取得失敗例Map<String, String> entries = new HashMap<>() {{ put("hoge", "ほげ"); put("huga", "ふが"); put("piyo", "ぴよ"); }}; System.out.println(entries.get("hoge")); // 出力:ほげ System.out.println(entries.get("hige")); // 出力:null(取得失敗)実装の変更に伴う値の取得失敗例Map<String, String> entries = new HashMap<>() {{ put("foo", "ふー"); // 実装の変更 put("huga", "ふが"); put("bar", "ばー"); // 実装の変更 }}; System.out.println(entries.get("hoge")); // 出力:null(取得失敗) System.out.println(entries.get("huga")); // 出力:ふがこのような危険性は、マーカーインタフェース1を導入することで除去することができます。今回は先程紹介したCardLayoutクラスの利用を例に、説明したいと思います。
導入前
次に折りたたんであるコードAによりレイアウトされる画面を考えます。
コードA
public class App { public static void main(String[] args) { new JFrame("Sample") {{ setSize(350, 300); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); // CardLayoutの生成 CardLayout cards = new CardLayout(); // フレーム中央 JLabel[] labels = { new JLabel("Card 1"), new JLabel("Card 2"), new JLabel("Card 3"), new JLabel("Card 4") }; Font fnt = new Font("メイリオ", Font.PLAIN, 40); JPanel center = new JPanel(); center.setLayout(cards); for (int i = 0; i < labels.length; i++) { labels[i].setFont(fnt); labels[i].setVerticalAlignment(JLabel.CENTER); labels[i].setHorizontalAlignment(JLabel.CENTER); center.add(labels[i], "Card" + (i + 1)); } // フレーム下部 JButton[] buttons = { new JButton("Card 1"), new JButton("Card 2"), new JButton("Card 3"), new JButton("Card 4") }; JPanel south = new JPanel(); for (int i = 0; i < buttons.length; i++) { int l_i = i; buttons[i].addActionListener(e -> cards.show(center, "Card" + (l_i + 1))); south.add(buttons[i]); } // フレームへのパネルの追加 add(center, BorderLayout.CENTER); add(south, BorderLayout.SOUTH); }}; } }このコードを実行し各ボタンをクリックすると、次のように4つのラベルを切り替えて表示することができます。
ここで、コードA内にある
buttons[i].addActionListener(e -> cards.show(center, "Card" + (l_i + 1)));の
"Card"
を"Cerd"
と間違えた瞬間、次のように切り替えは正常に動作しなくなります。これは、第2引数に4つのラベルの識別子を指定したaddメソッドによりフレーム中央のパネルに紐付けられた各ラベルを、誤字のせいでshowメソッドにより参照できなくなったためです。
center.add(labels[i], "Card" + (i + 1)); buttons[i].addActionListener(e -> cards.show(center, "Cerd" + (l_i + 1))); // "Card"じゃない!コンパイルエラーにならないので、自分で気がつくか誰かに教えてもらわない限り、その不具合の原因がわからずじまいとなってしまってもおかしくはありません。
導入
ということで、マーカーインタフェースを導入します。
マーカーインタフェースを導入するだけでは意味がないので、列挙体を用意してキーを定義し、その列挙体にマーカーインタフェースを実装させます。
// マーカーインタフェース interface CardKey {} // マーカーインタフェースを実装した列挙体 enum Card implements CardKey { CARD1, CARD2, CARD3, CARD4; }また、先程登場したaddメソッドやshowメソッドでは定義したキーをそのまま引数に指定することができないため、次に折りたたんであるユーティリティクラスを定義し、キーと切り替えて表示するコンポーネントを紐付けられるようにします。
ユーティリティクラス
class CardLayoutUtil { private CardLayout cards = new CardLayout(); private JPanel panel = new JPanel(); private CardLayoutUtil(JPanel panel) { this.panel = panel; cards = (CardLayout) panel.getLayout(); } /** * 引数にとるパネルに基づき、このクラスのインスタンスを生成する。 * * @param panel インスタンスに紐付けるパネル * @return このクラスのインスタンス * @throws NullPointerException 引数がnullである場合 * @throws IllegalArgumentException 引数にとるパネルにCardLayoutが設定されていない場合 */ static CardLayoutUtil of(JPanel panel) { Objects.requireNonNull(panel); if (!(panel.getLayout() instanceof CardLayout)) throw new IllegalArgumentException("引数にとるパネルにCardLayoutが設定されていません。"); return new CardLayoutUtil(panel); } /** * インスタンスに紐付けているパネルに、コンポーネントを登録する。 * * @param comp 登録するコンポーネント * @param key コンポーネントに紐付けるキー * @throws NullPointerException いずれかの引数がnullである場合 */ void addCard(Component comp, CardKey key) { Objects.requireNonNull(comp); Objects.requireNonNull(key); panel.add(comp, key.toString()); } /** * 引数にとるキーと紐付くコンポーネントを表示する。 * * @param key コンポーネントに紐付けているキー * @throws NullPointerException 引数がnullである場合 */ void showCard(CardKey key) { Objects.requireNonNull(key); cards.show(panel, key.toString()); } }ユーティリティクラスと紐付けるパネルのレイアウトマネージャがCardLayoutでなかったり、各メソッドの引数にとるキーやコンポーネントがnullであると期待する動作は実現できないので、例外処理を施しています。
このユーティリティクラスのオブジェクトを生成し、ついでにループで回せるように各キーの配列も用意しておきます。
// CardLayoutUtilオブジェクトの生成 CardLayoutUtil util = CardLayoutUtil.of(center); Card[] cardKeys = Card.class.getEnumConstants();あとは、コードA内にある
center.add(labels[i], "Card" + (i + 1)); buttons[i].addActionListener(e -> cards.show(center, "Cerd" + (l_i + 1))); // "Card"じゃない!の部分を
util.addCard(labels[i], cardKeys[i]); buttons[i].addActionListener(e -> util.showCard(cardKeys[l_i]));と置き換えれば、誤字や実装の変更によって先程のような不具合が発生することを心配する必要はなくなります。
注意点
今回の手法は、キー(列挙子)の文字列表現を
toString()
によって取得したものを用いることで実現しています。したがって、同じキーを別のコンポーネントに紐付けて使いまわしていたり、別々の列挙体間で同じ名前の列挙子を定義していたりすると、うまく動作しません2。列挙体間で同じ名前の列挙子を定義している例enum CardsA implements CardKey { CARD1, CARD2, CARD3, CARD4; } enum CardsB implements CardKey { CARD1, CARD2, CARD3, CARD4; }
正常に動作しないコード例
// フレーム中央 JLabel[] labelsA = { new JLabel("Card 1"), new JLabel("Card 2"), new JLabel("Card 3"), new JLabel("Card 4") }; JLabel[] labelsB = { new JLabel("Card 5"), new JLabel("Card 6"), new JLabel("Card 7"), new JLabel("Card 8") }; ... // 省略 // CardLayoutUtilオブジェクトの生成 CardLayoutUtil util = CardLayoutUtil.of(center); CardsA[] cardKeysA = CardsA.class.getEnumConstants(); CardsB[] cardKeysB = CardsB.class.getEnumConstants(); for (int i = 0; i < labelsA.length; i++) { ... // 省略 util.addCard(labelsA[i], cardKeysA[i]); } for (int i = 0; i < labelsB.length; i++) { ... // 省略 util.addCard(labelsB[i], cardKeysB[i]); } // フレーム下部 JButton[] buttonsA = { new JButton("Card 1"), new JButton("Card 2"), new JButton("Card 3"), new JButton("Card 4") }; JButton[] buttonsB = { new JButton("Card 5"), new JButton("Card 6"), new JButton("Card 7"), new JButton("Card 8") }; JPanel south = new JPanel(); for (int i = 0; i < buttonsA.length; i++) { ... // 省略 south.add(buttonsA[i]); } for (int i = 0; i < buttonsB.length; i++) { ... // 省略 south.add(buttonsB[i]); }ただ、これはキーの識別を列挙子名だけで行っているゆえに起きる現象であるため、ユーティリティクラス内の
cards.show(panel, key.toString()); panel.add(comp, key.toString());の部分を
cards.show(panel, key.getClass().getName() + "." + key.toString()); panel.add(comp, key.getClass().getName() + "." + key.toString());と書き換えることで、キーの識別が列挙体の完全修飾クラス名3+列挙子名で行われるようになり、正常に動作するようになります。
また、キーとコンポーネントの数が合わなかったりするとループを回しているときに
ArrayIndexOutOfBoundsException
を吐いたりもします。その辺はユーティリティクラスをもう少しゴニョゴニョすれば改善できるかもしれません。
- 投稿日:2020-09-20T15:35:46+09:00
Mavenプロジェクトの作り方
Mavenプロジェクトの作り方
$mvn archetype:generate
上記のコマンドをターミナル・コマンドプロンプトで実行すると、Mavenプロジェクトが作られ始めます。(*Mavenをインストールしておく必要がある。私の場合はhomebrewでインストールしました。)
そして以下の事を聞かれるので、以下のように答えていきます。
Choose a number or apply filter
上記のメッセージが出てくる。これはグループIDの指定。
フィルターを設定する必要がないのであればEnter。
Choose org.apache.maven.archetypes:maven-archetype-quickstart version:
リストの中から使いたいバージョンを選択。
特にないならEnter。
Define value for property ‘groupId’::
これはプロジェクトのグループID。
基本的に作成するプログラムを配置するパッケージを指定。
パッケージ名を何か適当に入力
(例: jp.tuyano.spring.sample1)
Define value for property ‘artifactId’::
アーティファクトIDを入力。
プロジェクトの名前を入力。
(例:MySampleApp1)
Define value for property ‘version’: 1.0-SNAPSHOT::
バージョン名。1.0-SNAPSHOTがデフォルトで指定されている。
特に変える必要がないのであればEnter。
Define value for property ‘package’:
さっきグループIDで入力したものがデフォルトで設定されているので、Enter。次に内容の確認。Y/Nで聞かれるから間違っていなかったら、Y
MavenによるBuild
$ cd プロジェクト名(MySampleApp1)
$mvn install
プログラムをパッケージ化し、ローカルリポジトリにインストールする命令。
必要なライブラリをダウンロードして、プロジェクトを指定のパッケージファイルにまとめてくれる。プログラムをコンパイルするならmvn compile,
パッケージを作成するならmvn package
などのコマンドがある。
ここら辺は適宜調べてみるといいと思います。
Mavenによる実行
$cd target
$java -classpath ./MySampleApp1-1.0-SNAPSHOT.jar jp.tuyano.spring.sample1.App
"/"を使っているが"/"の部分をOSによって変える必要がある。
https://stackoverflow.com/questions/4528438/classpath-does-not-work-under-linux
- 投稿日:2020-09-20T14:31:20+09:00
Javaに初めて触れてみた②
Javaに触れてみました。
自己満の備忘録ですのでご容赦ください。
プログラムの基本構造
プログラムの動きは3つだけ。
①順次進行
②条件分岐
③繰り返し順次進行
Greeting.javaclass Greeting { public static void main(String args[]) { System.out.println("Good morning"); System.out.println("Good afternoon"); System.out.println("Good evening"); } }ターミナル$ javac Greeting.java $ java Greeting Good morning Good afternoon Good eveningjavac Greeting.javaでGreeting.classというフォルダが作成されるようです。
配列
Array.javaclass Array { public static void main(String[] args) { String[] arr = {"sato", "suzuki", "takahashi"}; System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); } }ターミナルsato suzuki takahashi繰り返し
For.javaclass For { public static void main(String[] args) { for (int i = 0; i <= 2; i++) { for (int j = 0; j <= 2; j++) { System.out.println(i + "-" + j); } } } }ターミナル0-0 0-1 0-2 1-0 1-1 1-2 2-0 2-1 2-2ネストさせているので、このような処理になります。
実践
あるサラリーマンの5ヶ月間の平均給料を求め、すごいかどうかを判定処理する機能を作ってみます。
Lesson.javaclass Office_worker { String name; public int calculateAVG(int[] data) { int sum = 0; for (int i = 0; i < data.length; i++) { sum += data[i]; } int avg = sum / data.length; return avg; } public String judge(double avg) { String result; if (avg >= 250000) { result = "すごい!"; } else { result = "まだまだ!"; } return result; } } public class Lesson { public static void main(String[] args) { Office_worker a001 = new Office_worker(); a001.name = "sato"; int[] data = {270000, 265000, 250000, 290000, 230000}; int avg = a001.calculateAVG(data); String result = a001.judge(avg); System.out.println(avg); System.out.println(a001.name + "の平均給料は" + result); } }ターミナル261000 satoの平均給料はすごい!
- 投稿日:2020-09-20T14:02:03+09:00
【Java】read()メソッドで取得するテキストファイルの文字の整数情報
参考図書
Javaの絵本 第3版 Javaが好きになる新しい9つの扉
https://www.amazon.co.jp/Java%E3%81%AE%E7%B5%B5%E6%9C%AC-%E7%AC%AC3%E7%89%88-Java%E3%81%8C%E5%A5%BD%E3%81%8D%E3%81%AB%E3%81%AA%E3%82%8B%E6%96%B0%E3%81%97%E3%81%849%E3%81%A4%E3%81%AE%E6%89%89-%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BE%E3%82%A2%E3%83%B3%E3%82%AF/dp/4798150371※サンプルプログラムに手を加えて引用しています。
文字情報を読み込むサンプルプログラム
ReadText.javaimport java.io.FileReader; import java.io.IOException; public class ReadText { public static void main(String[] args) { try { FileReader in = new FileReader(args[0]); int c; String s = new String(); System.out.println("****文字整数のそのまま表示****"); while((c = in.read()) != -1) { System.out.print(c); // 文字の整数情報を出力 System.out.println(); s = s + (char)c; // 整数情報を文字に変換 } System.out.print(s); // 文字に変換して結合した文字列を出力 in.close(); } catch (IOException ie) { // 入出力時の例外を表す例外クラス System.out.println("ファイルがありません"); } catch (Exception e) { System.out.println("ファイル指定がありません"); } } }aiueo.txtあいうえお かきくけこ さしすせそ
最後の「そ」の後にも改行を入れています
実行結果
実行結果****文字整数のそのまま表示**** 12354 12356 12358 12360 12362 10 12363 12365 12367 12369 12371 10 12373 12375 12377 12379 12381 10 あいうえお かきくけこ さしすせそひらがな一つは5つ、改行は10で表す
10が3回出てきてます。aiueo.txtのそれぞれの行の最後に改行を入れているので改行は10のようですね。
PHPより入出力がめんどくさい
多分PHPだけじゃなくてRubyももっと簡単に記述ができるのでしょう。
しかしテキストファイルとのデータのやりとりの方法などは、いちいち記述しないといけないが故に、中身も見えて理解が深まる気がします。
- 投稿日:2020-09-20T10:32:01+09:00
【Java】例外処理の練習【Exception】
対象
初学者向けです。
「例外処理って何?」というレベルの内容です。整数をゼロで割ってエラーを発生させてみる
Main.javapublic class Main { public static void main(String[] args) { int a = 5; int b = 0; System.out.println(div(a,b)); // ここでエラーが発生 System.out.println("終了します"); } static int div(int a, int b) { return a / b; } }コンパイルエラーは発生せず、コンパイルできます。
そして実行。実行結果Exception in thread "main" java.lang.ArithmeticException: / by zero at Main.div(Main.java:19) at Main.main(Main.java:9)エラーが発生している行で処理が終わる
System.out.println(div(a,b));という部分で
java.lang.ArithmeticException: / by zeroこのエラーの後に
System.out.println("終了します");という処理があるにもかかわらず一行上のエラーが発生している部分で処理が終わっています。
プログラムが強制終了していることになるので、「エラーが発生したらこうしてね」という処理を書く必要があります。これが例外処理。
例外ってなに?
エラーのことです。単純にエラーだと考えて差し支えないと思います。厳密には、というか人によって捉え方が違ってくるものらしいので、とりあえず例外処理はエラー処理だと考えておけば大丈夫。
例外処理の書き方try~catch
Main.javapublic static void main(String[] args) { int a = 5; int b = 0; try { System.out.println(div(a,b)); } catch(Exception e) { // エラーをeというオブジェクトで受け取る(オブジェクト名はなんでも良い) System.out.println("0で割っています!"); System.out.println(e); // eを出力 } finally { System.out.println("終了します"); } } // エラーが発生しそうな計算。エラーがある場合はExceptionクラスにエラー情報を渡す。 static int div(int a, int b) throws Exception { return a / b; } }try{ エラーが予測される処理 } catch (Exception e) { エラーが発生した場合の処理 } finally { エラーが発生してもしなくてもする処理 }エラーが予測される処理を書く場合に、そこで終了させず、エラーが発生したらcatchの部分エラーが発生した場合の処理をする。これで処理が止まることなく最後まで動いてくれるというもの。
実際実行してみると
0で割っています! // エラーが発生した場合の処理 java.lang.ArithmeticException: / by zero // エラーメッセージ 終了します // finallyまで実行されている
- 投稿日:2020-09-20T09:55:24+09:00
gradleでjlinkを使う
はじめに
jlinkを使うとアプリ専用のJREができて、配布に便利。
というわけでgradleで使う方法を調べてみた。プロジェクトの準備
とりあえず普通に
gradle init --type=java-application
で作る。
build.gradleの書き換え
最低限必要なのは、pluginsの項目に以下の行を追加すること。
id 'org.berix.jlink' version '2.22.0'
これで、gradle jlink が使えるようになる。
また、デフォルトで入っている dependencies は消しておくと警告が消える。(数字で終わるモジュール名がいけないらしい)
その他、applicationの項目には
mainModule = 'module名'
を書いておくと、警告が消える。(module名は module-info.java で指定したもの)
jlink の項目を作って、optionを設定することも可能。例えば以下のように書ける。
jlink { options = ['--compress', '2', '--no-header-files', '--no-man-pages'] }targetPlatformで複数のプラットフォーム用アプリを作ることもできる。この場合、各プラットフォーム用のJDKを用意しておく。
jlink { targetPlatform("mac") { jdkHome = "/usr/java/jdk-mac/Contents/Home" } targetPlatform("linux-x64") { jdkHome = "/usr/java/jdk" } targetPlatform("windows-x64") { jdkHome = "/usr/java/jdk-win" } options = ['--compress', '2', '--no-header-files', '--no-man-pages'] }ビルド
gradle build
では、jlinkを使わずにjarができる。jlinkする場合は、以下のようにする。
gradle jlink
これで build ディレクトリの下に image ができ、その下のディレクトリがアプリになっている。
targetPlatformを指定している場合は、<プロジェクト名>-<ターゲットプラットフォーム名> のディレクトリができ、その下に置かれる。アプリ実行のためには、bin ディレクトリで <プロジェクト名> の実行スクリプトがあるので、それを実行すれば良い。(Windows用の場合はbatファイル)
アプリの配布は bin, conf, legal, libの各ディレクトリをまとめて配布すればOK。
そのままコピーして、binの実行ファイルを実行すれば起動できる。
- 投稿日:2020-09-20T08:43:17+09:00
AndroidのHiltをJavaで実装してみる
はじめに
Hiltのアルファ版が出たので調査しました。
あのわかりづらかったDagger2が非常にわかりやすくなっていました。導入の敷居がさがったと思います。そこで、「DIしたい、Hiltを使ってみたい、基本的なことを知りたい」という方に向け、サンプルアプリを作成しながら基本的なことを説明したいと思います。
なお、Dagger2からHiltへの移行方法などは記載していません。いきなり実装
概要等の説明は省き、いきなり実装です。DIやHiltについては公式ページを参照してください。
今回作成するサンプルは、画面の中のボタンを押すとデータベースを検索し結果を表示するアプリです。作成するクラスの構成は以下のようにします。
MainActivity --> SampleUseCase --> SampleRepository --> SampleDao --> DBこのアプリを
Hilt
を使って実装します。
データベースまわりはRoom
を使用しますがRoom
については触れません。環境は以下のようになっています。
- Android Studio 4.0.1
- Android Virtual Device - android10.0 / 1080 x 2160ライブラリの追加
まずは、ルートの
build.gradle
の設定です。buildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' // ・・・(省略)・・・ } }次に、
app/build.gralde
の設定です。apply plugin: 'dagger.hilt.android.plugin' android { // ・・・(省略)・・・ } dependencies { implementation "com.google.dagger:hilt-android:2.28-alpha" annotationProcessor "com.google.dagger:hilt-android-compiler:2.28-alpha" // ・・・(省略)・・・ }Applicationクラスの作成
次に、Applicationクラスを作成します。
Applicationクラスに@HiltAndroidApp
をつけるだけです。今まではDaggerApplication
を継承するか、HasAndroidInjector
をimplしていましたが、HiltではアノテーションをつけるだけでOKです。Applicationクラスを作成したら、AndroidManifest.xmlに追加します。
@HiltAndroidApp public class SampleApplication extends Application { }AndroidManifest.xml<application android:name=".SampleApplication" android:icon="@mipmap/ic_launcher"> ・・・(省略)・・・ </application>今まではAppComponentを作成していましたが、Hiltでは不要になりました。Dagger2からの移行の場合はバッサリ削除します。
// @Singleton // @Component(modules={AndroidInjectionModule.class}) // public interface AppComponent extends AndroidInjector<SampleApplication> { // ・・・(省略)・・・ // }Activityに注入する
Activityに
@AndroidEntryPoint
アノテーションを付けるとinjectできるようになります。
今までは、HasAndroidInjector
のimplやAndroidInjection.inject(this);
の実行などを行っていましたが、Hiltではアノテーションをつけるだけです。今回のサンプルでは、MainActiviytにSampleUseCaseを注入します。
MainActivity.java@AndroidEntryPoint // ・・・(1) public class MainActivity extends AppCompatActivity { @Inject // ・・・(2) SampleUseCase useCase; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.execute); button.setOnClickListener(v -> useCase.execute()); // ・・・(3) } }(1)
AndroidEntryPoint
アノテーションをつけます。
(2)Inject
アノテーションをつけます。 このuseCase
変数にHiltによってインスタンスが注入されます。
(3)ボタン押下時にuseCase
を実行します。 コード上はSampleUseCaseはnew していないのに実行できます。Hiltによってインスタンスが生成されて注入されているからです。次に、SampleUseCaseです。
まずは、ログを出力するだけの実装です。SampleUseCase.javapublic class SampleUseCase { private static String TAG = SampleUseCase.class.getName(); @Inject // ・・・(1) public SampleUseCase() { } public void execute() { Log.d(TAG, "実行!!"); } }(1) コンストラクタに
Inject
アノテーションをつけます。これがないとHilt管理のオブジェクトにならず、ActivityにInjectされません。ここまでで実際に動きます。DIができています。
非常に簡単ですね。Dagger2のときと比べると圧倒的に簡単になっています。Hiltモジュールの作成
上のサンプルではInjectするクラス(SampleUseCase)では、コンストラクタに
@Inject
を指定しています。しかし、@Inject
を付与できないことがあります。たとえば、インターフェースの場合や、外部ライブラリのクラスなどです。
このような場合は@Module
アノテーションを付与したクラス作成し、インスタンスの生成方法をHiltに知らせます。今回のサンプルでは、SampleUseCaseにインターフェースの
SampleRepository
を呼び出すところが該当します。
SampleUseCaseクラスに実装を追加します。SampleUseCase.javapublic class SampleUseCase { private static String TAG = SampleUseCase.class.getName(); @Inject SampleRepository repository; // インターフェース @Inject public SampleUseCase() { } public void execute() { Log.d(TAG, "実行!!"); // 実行します List<SampleEntity> results = repository.find(); // 結果をログに表示 results.forEach(result -> { Log.d(TAG, result.getName()); }); }
Binds
を使用してインスタンスを注入する
SampleRepository
の実装です。インターフェースと実体クラスは以下のようになります。SampleRepository.javapublic interface SampleRepository { List<SampleEntity> find(); }SampleRepositoryImpl.javapublic class SampleRepositoryImpl implements SampleRepository { public static final String TAG = SampleRepositoryImpl.class.getName(); @Inject // ・・・(1) public SampleRepositoryImpl() { } public List<SampleEntity> find() { Log.d(TAG, "find!"); return null; } }(1) 実体クラスのコンストラクタには
@Inject
を付与しますこれだけではInjectできません。
このインターフェースのインスタンスを生成する方法をHiltに教える必要があります。Hiltモジュールは、
@Module
アノテーションが付けられたクラスです。Daggerのモジュールとは異なり、Hiltは@InstallIn
アノテーションを付けて依存関係を指定します。DataModule.java@Module // ・・・ (1) @InstallIn(ApplicationComponent.class) // ・・・ (2) abstract public class DataModule { @Binds // ・・・ (3) public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl); }(1)
@Module
アノテーションを付与しHiltモジュールクラスであることを宣言します。クラス名はなんでもよいです。
(2) このModuleの依存関係を指定します。この例では、ここで宣言したクラスたちは、アプリ内のどのクラスにでもInjectできます。この指定は以下の表のようにいろいろと指定することができます。
例えば、FragmentComponentを指定した場合は、FragmentにインジェクションできますがActivityにはインジェクションできません。今回はアプリのどのクラスにでも注入できるように、ApplicationComponentを指定します。
コンポーネント インジェクションの対象 ApplicationComponent Application ActivityRetainedComponent ViewModel ActivityComponent Activity FragmentComponent Fragment ViewComponent View ViewWithFragmentComponent WithFragmentBindingsアノテーションが付いた View ServiceComponent Service (3)
Binds
アノテーションを付与し、どの実体を生成するか宣言します。メソッドの戻り値には、インターフェースを指定します。メソッドのパラメータで、生成したい実体を指定します。
Provides
を使用してインスタンスを注入するBinds以外にもインスタンスの生成方法を指定することができます。
外部ライブラリはコンストラクタにインジェクションを付与できません。そのような場合にはProvides
を利用します。今回のサンプルでは、SampleRepositoryImplがDaoを呼び出すところが該当します。
Room関連をDIするときの実装ですね。(Roomに関する説明はしません。別の記事でする予定です)上のサンプルコードの
DataMudule.java
に追加します。DataModule.java@Module @InstallIn(ApplicationComponent.class) abstract public class DataModule { @Provides // ・・・ (1) @Singleton // ・・・ (2) public static SampleDatabase provideDb(Application context) { return Room.databaseBuilder(context.getApplicationContext(), SampleDatabase.class, "sample.db") .addCallback(SampleDatabase.INITIAL_DATA) .allowMainThreadQueries() .build(); } @Provides // ・・・ (3) @Singleton public static SampleDao provideSampleDao(SampleDatabase db) { return db.getSampleDao(); } @Binds public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl); }(1)
Provides
アノテーションを付与し、どの実体を生成するか宣言します。
メソッドの戻り値には、生成したインスタンスです。パラメータは、Hiltが管理しているインスタンスを渡すことができます。
(2)このメソッドにはSingleton
アノテーションがついています。これはスコープの設定です。
通常、Hiltはリクエストがあるたびに、毎回、新しいインスタンスを作成します。これをアノテーションを付与することにより制御することができます。今回のサンプルは、Singleton
ですので、アプリケーションで1つのインスタンスの状態を実現します。(毎回、新しいインスタンスはつくりません)。どのクラスの注入しても同じインスタンスになります。スコープは以下のようなものが用意されています。
Android クラス 生成されたコンポーネント スコープ Application ApplicationComponent Singleton View Model ActivityRetainedComponent ActivityRetainedScope Activity ActivityComponent ActivityScoped Fragment FragmentComponent FragmentScoped View ViewComponent ViewScoped WithFragmentBindings ViewWithFragmentComponent ViewScoped Service ServiceComponent ServiceScoped 例えば、今回のサンプルの
DataModule.java
のInstantRunをActivityComponent
の変更し、SampleDaoをActivityScoped
に変更すると、Activityが存続する間は同じインスタンスになります。
SampleActivity、SampleUseCase、SampleRespositoryにDaoをInjectした場合、そのDaoはすべて同じインスタンスです。サンプルアプリの実装に戻ります。
SampleRepositoryImplにDaoを注入して実装を完成させます。SampleRepositoryImpl.javapublic class SampleRepositoryImpl implements SampleRepository { public static final String TAG = SampleRepositoryImpl.class.getName(); @Inject // ・・・(1) SampleDao dao; @Inject public SampleRepositoryImpl() { } public List<SampleEntity> find() { Log.d(TAG, "find!"); return dao.find(); // ・・・(2) } }(1)SampleDaoを注入します。スコープは
Singleton
なので毎回同じインスタンスが注入されます。
(2)newしていませんが、Hiltによって注入されるのでNullPointerExceptionにはなりません。その他のコード
上記で説明していないサンプルアプリの、DaoとEntityを載せておきます。
SampleEntity.java@Entity(tableName = "sample") public class SampleEntity implements Serializable { @PrimaryKey @NonNull private String code; private String name; //setter/getter省略 }SampleDao.java@Dao public interface SampleDao { @Insert long save(SampleEntity dto); @Query("select * from sample") List<SampleEntity> find(); }完成!!
ここまでで、画面のボタンを押すと、ログに検索結果が表示されます。
Dagger2で必要だったものがほとんど不要となり、シンプルに実現できるようになりました。非常に簡単ですね。まとめ
今回のサンプルを通じてHiltのポイントを整理します。
1. Activityに@AndroidEntryPoint
を付与する
2. Inject対象のクラスのコンストラクタに@Inject
を忘れずにつける
3. インターフェースや他のライブラリをInject対象とするときは、@Binds
または@Provides
を利用する
以上です。
簡単ですね。次回はHiltを利用しViewModelを使って検索結果を画面に表示させたいと思います。
では、また!参考
- Android デベロッパーガイド - Hiltを使用した依存関係の注入