20200920のJavaに関する記事は7件です。

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つのラベルを切り替えて表示することができます。

ok.gif

ここで、コードA内にある

buttons[i].addActionListener(e -> cards.show(center, "Card" + (l_i + 1)));

"Card""Cerd"と間違えた瞬間、次のように切り替えは正常に動作しなくなります。

ng.gif

これは、第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]);
}

ng2.gif

ただ、これはキーの識別を列挙子名だけで行っているゆえに起きる現象であるため、ユーティリティクラス内の

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+列挙子名で行われるようになり、正常に動作するようになります。

ok2.gif

また、キーとコンポーネントの数が合わなかったりするとループを回しているときにArrayIndexOutOfBoundsExceptionを吐いたりもします。その辺はユーティリティクラスをもう少しゴニョゴニョすれば改善できるかもしれません。


  1. マーカーインタフェースとは、中身に何も実装していないインタフェースのことです。実装するクラスに意味付けを行うときによく用いられます。 

  2. toString()をオーバーライドさせてすべて同一の文字列表現が返されるようにしていても、キーが判別できなくなるため駄目です。 

  3. インポート文で使用されるパッケージ名を含むクラス名のことです。 

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

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

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

Javaに初めて触れてみた②

Javaに触れてみました。

自己満の備忘録ですのでご容赦ください。

プログラムの基本構造

プログラムの動きは3つだけ。
①順次進行
②条件分岐
③繰り返し

順次進行

Greeting.java
class 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 evening

javac Greeting.javaでGreeting.classというフォルダが作成されるようです。

配列

Array.java
class 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.java
class 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.java
class 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の平均給料はすごい!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.java
import 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ももっと簡単に記述ができるのでしょう。

しかしテキストファイルとのデータのやりとりの方法などは、いちいち記述しないといけないが故に、中身も見えて理解が深まる気がします。

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

【Java】例外処理の練習【Exception】

対象

初学者向けです。
「例外処理って何?」というレベルの内容です。

整数をゼロで割ってエラーを発生させてみる

Main.java
public 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.java
    public 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まで実行されている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の実行ファイルを実行すれば起動できる。

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

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.java
public 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.java
public 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.java
public interface SampleRepository {
    List<SampleEntity> find();
}
SampleRepositoryImpl.java
public 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.java
public 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を使って検索結果を画面に表示させたいと思います。
では、また!

参考

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