20201017のJavaに関する記事は8件です。

Doma入門 - トランザクション

はじめに

データベースアクセスに欠かせないトランザクション機能についてポイントを紹介します。

この記事では、Doma 2.44.0を前提とします。

Domaの他の機能紹介についてはDoma入門もお読みください。

最初に検討すること

トランザクションに関して最初に検討すべきことがあります。
それは、利用しているフレームワーク(SpringFrameworkなど)がトランザクション機能を提供しているかどうかです。提供している場合、まずはそちらの利用を検討ください。

SpringFrameworkを使う場合

SpringFrameworkはトランザクション機能を提供します。

org.seasar.doma.jdbc.Configの実装クラスのgetDataSourceメソッドでは、org.springframework.jdbc.datasource.TransactionAwareDataSourceProxyを使ってラップしたDataSourceを返すように設定してください。これが非常に重要です

上記対応を行った上でこのガイドを参考にSpringのコンポーネントのクラスやメソッドに@Transactionalを付与すればトランザクションを利用できます。

doma-spring-bootを使えば上記のDataSourceのラッピングは自動で行われます。doma-spring-bootを使ってSpringFrameworkのトランザクション機能を利用しているサンプルアプリとしてはspring-boot-jpetstoreがあります。

Quarkusを使う場合

Quarkusはトランザクション機能を提供します。

org.seasar.doma.jdbc.Configの実装クラスのgetDataSourceメソッドでは、Quarkusのコネクションプール実装であるAgroalで管理されたDataSourceを返してください。

上記対応を行った上でこのドキュメントにあるようにCDIのコンポーネントのクラスやメソッドに@Transactionalを付与すればトランザクションを利用できます。

Quarkus Extension for Domaを使えば上記のDataSourceの設定は自動で行われます。Quarkus Extension for Domaを使ってQuarkusのトランザクション機能を利用しているサンプルアプリとしてはquarkus-sampleがあります。

トランザクション機能を提供するフレームワークを使わない場合

Domaのローカルトランザクションの利用を検討してください。

Domaのローカルトランザクションの特徴は次の通りです。

  • ThreadLocalを使ってスレッドごとにコネクションを管理する
  • 名前の通りローカルトランザクションなので扱えるリソースは1つだけ(グローバルトランザクションのように2フェーズコミットの機能はない)
  • 手続的なAPIであるorg.seasar.doma.jdbc.tx.TransactionManagerを提供する(@Transactionalのような宣言的なAPIではない)
  • JDBCのセーブポイント機能を提供する

使い方は次の節で述べます。

Domaのローカルトランザクション

動くコードはgetting-startedにありますが、ここでは重要な部分を抜粋して示します。

Main.java
public class Main {

  public static void main(String[] args) {
    var config = createConfig();
    var tm = config.getTransactionManager();

    // setup database
    var appDao = new AppDaoImpl(config);
    tm.required(appDao::create);

    // read and update
    // ④トランザクションマネージャーのメソッドにラムダ式を渡す
    tm.required(
        () -> {
          var repository = new EmployeeRepository(config);
          var employee = repository.selectById(1);
          employee.age += 1;
          repository.update(employee);
        });
  }

  private static Config createConfig() {
    var dialect = new H2Dialect();
    // ①トランザクション対応のデータソースを作る
    var dataSource =
        new LocalTransactionDataSource("jdbc:h2:mem:tutorial;DB_CLOSE_DELAY=-1", "sa", null);
    var jdbcLogger = new Slf4jJdbcLogger();
    // ②トランザクションマネージャーを作る
    var transactionManager = new LocalTransactionManager(dataSource, jdbcLogger);
    // ③Configの実装クラスから上記の①や②で作ったインスタンスを返せるようにする
    return new DbConfig(dialect, dataSource, jdbcLogger, transactionManager); 
  }
}

①トランザクション対応のデータソースを作る

LocalTransactionDataSourceをインスタンス化します。
この例では接続URLなどをコンストラクタで受け取っていますが、DataSourceインスタンスを受け取るコンストラクタも持っています。

②トランザクションマネージャーを作る

上記の①で作ったLocalTransactionDataSourceのインスタンスをコンストラクタに渡してLocalTransactionManagerをインスタンス化します。

③Configの実装クラスから上記の①や②で作ったインスタンスを返せるようにする

①と②で作ったインスタンスをConfigの実装クラスであるDbConfigのコンストラクタに渡してインスタンス化します。

④トランザクションマネージャーのメソッドにラムダ式を渡す

ここのtmは②で作ったLocalTransactionManagerのインスタンスです。
tmrequiredメソッドにトランザクション内で扱いたい処理をラムダ式で渡すことでトランザクションを実行できます。

requiredメソッドはトランザクションがまだ開始されていなかったら開始するメソッドで、他に常に新規にトランザクションを開始するrequiresNewメソッドやトランザクションを一旦停止するnotSupportedメソッドなどがあります。これらのメソッドはネストして使えます。

ラムダ式から例外をスローするかsetRollbackOnlyメソッドを呼び出すかするとトランザクションはロールバックされます。それ以外ではコミットされます。

1つ注意点ですが、ローカルトランザクションの設定をした場合、Domaによる全てのデータベースアクセスは原則的にTransactionManager経由で行う必要があります。そうしない場合、例外が発生します。

おわりに

Domaでトランザクションを利用するポイントを紹介しました。

Domaを使っていてトランザクションがうまく動いていないなと思ったら、この記事をはじめリンク先の記事やサンプルも参考にしてもらえればと思います。

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

Java/JavaScript/C#で暗号化する

各言語で暗号化。
やってみると、暗号化後の文字列が各言語で違ってしまったりして、意外とハマったりします。
ここでは、Java/JavaScript/C#を取り上げてみます。

暗号化

Java

まずは、Java。

Crypto.java
import java.nio.charset.StandardCharsets;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import java.util.Base64;

public final class Crypto {

    private static final String KEY = "1234567890abcdef";
    private static final String IV = "abcdef1234567890";

    private Crypto() {
    }

    private static Cipher createCipher(int mode) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
        IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
        cipher.init(mode, key, iv);
        return cipher;
    }

    public static String encrypt(String text) {
        try {
            Cipher cipher = createCipher(Cipher.ENCRYPT_MODE);
            Base64.Encoder encoder = Base64.getEncoder();
            return encoder.encodeToString(cipher.doFinal(text.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String decrypt(String text) {
        try {
            Cipher cipher = createCipher(Cipher.DECRYPT_MODE);
            Base64.Decoder decoder = Base64.getDecoder();
            return new String(cipher.doFinal(decoder.decode(text)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

JavaScript

次にJavaScript。実行環境はNode.jsです。

Crypto.js
const crypto = require('crypto')

const KEY = '1234567890abcdef'
const IV = 'abcdef1234567890'

function createCipher(mode) {
    return crypto[mode]('aes-128-cbc', KEY, IV)
}

function encrypt(text) {
    const cipher = createCipher('createCipheriv')
    const encrypted = cipher.update(text);
    return Buffer.concat([encrypted, cipher.final()]).toString('base64')
}

function decrypt(text) {
    const buf = Buffer.from(text, 'base64')
    const cipher = createCipher('createDecipheriv')
    const decrypted = cipher.update(buf);
    return Buffer.concat([decrypted, cipher.final()]).toString('utf-8')
}

C#

最後にC#。

Crypto.cs
using System;
using System.Security.Cryptography;
using System.Text;

public sealed class Crypto
{
    private const string KEY = "1234567890abcdef";
    private const string IV = "abcdef1234567890";

    private Crypto()
    {
    }

    private static AesManaged CreateAesManaged()
    {
        AesManaged aes = new AesManaged();
        aes.KeySize = 256;
        aes.BlockSize = 128;
        aes.Mode = CipherMode.CBC;
        aes.IV = Encoding.UTF8.GetBytes(IV);
        aes.Key = Encoding.UTF8.GetBytes(KEY);
        aes.Padding = PaddingMode.PKCS7;
        return aes;
    }

    public static string Encrypt(string text)
    {
        AesManaged aes = CreateAesManaged();
        byte[] byteText = Encoding.UTF8.GetBytes(text);
        byte[] encryptText = aes.CreateEncryptor().TransformFinalBlock(byteText, 0, byteText.Length);
        return Convert.ToBase64String(encryptText);
    }

    public static string Decrypt(string text)
    {
        AesManaged aes = CreateAesManaged();
        byte[] src = System.Convert.FromBase64String(text);
        byte[] dest = aes.CreateDecryptor().TransformFinalBlock(src, 0, src.Length);
        return Encoding.UTF8.GetString(dest);
    }
}

結果

暗号化(encrypt)すると、以下の結果になります。

  • ""(空文字)=> 0e8Yyuobp55/giEyC01lbg==
  • alz1590-'$ => cMgaJ1X/cVENp+rnMo/8Kw==
  • あむんアムンアムン亜無漚錡?? => jXw9bzISBZsndhcrJx/gJU7ltBzSp34xn6CL3xBbNE2yC0FhQBisP27WOXan/W2o

復号(decrypt)すると、元の文字列が得られます。

補足

  • IVは初期化ベクトルです。同じデータであっても違う暗号文にするための文字列です。
  • KEY/IVは長さに決まりがあるので注意。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Log4J Asyncやったら遅くなるんだけど

みんな大好きLog4J。この記事は非同期の設定
作者はロギングのシステムについてはあんまりわかってない。
WEBでまよったりして動かなかったので記事まとめる。

結論

なんか私の環境ではAsyncじゃないほうが速い。

動機

WEBアプリケーションのボトルネックはロギングとDBなので速いものを求めた。

公式

http://logging.apache.org/log4j/2.x/manual/async.html
6-68倍速とのこと。すごい。
監査証跡ログとかExceptionログとかに使わないほうが良いよ!っていってるけどまぁ大丈夫だよね。使う。

要求

IPAの文書に従うと(プロジェクトメンバーが読んでくれてスペック教えてくれた)、
* SQLのログ
* アクセスログ
* アプリのログ(自由形式?)
を作っておけとのこと。
アクセスログはWEBサーバーで出しても良いんだろうけどアプリ側でも出すことにする
よって一旦3つのロガーを設定する例を考えた

書き方

Asyncで囲むAsyncLoggerと書く

前者だと私が試した感じでうまく行かなかった。なんで?→AsyncLogger使う

RollingRandomAccessFileAppenderとFileAppenderとRollingFileAppender

RandomAccessFileが速いらしいので採用

log4j2.xml

実際わからんよね。むずい。

の前にlog4j.properties

log4j.properties作って置かないとおこられるので雑に置く。多分FWのVert.xの要求だとおもうんだけど追いかけていない

log4j.rootLogger=ERROR, console
log4j.logger.xxx=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d [%-5p-%c] %m%n

log4j.xml

愚直に書くよ

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
    <RollingRandomAccessFile name="AppLog" fileName="logs/app.log" filePattern="logs/old/app-%d{yyyy-MM-dd}.log.gz"
                 ignoreExceptions="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <TimeBasedTriggeringPolicy/>
    </RollingRandomAccessFile>
    <RollingRandomAccessFile name="AccessLog" fileName="logs/access.log" filePattern="logs/old/access-%d{yyyy-MM-dd}.log.gz"
                 ignoreExceptions="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <TimeBasedTriggeringPolicy/>
    </RollingRandomAccessFile>
    <RollingRandomAccessFile name="ErrorLog" fileName="logs/error.log" filePattern="logs/old/error-%d{yyyy-MM-dd}.log.gz"
                 ignoreExceptions="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <TimeBasedTriggeringPolicy/>
    </RollingRandomAccessFile>
    <RollingRandomAccessFile name="SQLLog" fileName="logs/sql.log" filePattern="logs/old/sql-%d{yyyy-MM-dd}.log.gz"
                 ignoreExceptions="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <TimeBasedTriggeringPolicy/>
    </RollingRandomAccessFile>
    <Async name="AppAsync">
      <AppenderRef ref="Console"/>
    </Async>
  </Appenders>
  <Loggers>
    <Root level="info">
      <AppenderRef ref="AppAsync"/>
    </Root>
    <AsyncLogger level="debug" name="access-log">
      <AppenderRef ref="AccessLog"/>
    </AsyncLogger>
    <AsyncLogger level="debug" name="error-log">
      <AppenderRef ref="ErrorLog"/>
    </AsyncLogger>
    <AsyncLogger level="debug" name="sql-log">
      <AppenderRef ref="SQLLog"/>
    </AsyncLogger>
  </Loggers>
</Configuration>

パフォーマンス

abコマンドで雑に叩く
セレロンの超遅いファイルサーバーマシンで確認

非同期

Requests per second:    347.96 [#/sec] (mean)

同期

Requests per second:    2830.04 [#/sec] (mean)

同期のほうが速いですやん。嘘やん。なんか間違っている気がする。

計測大事

同期のほうが速いという状況。よくわからん。ちゃんと計測しないとだめですねー
以上です。

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

JavaでTODOアプリを制作しよう11 存在しないIDのTODOにアクセスした時の例外処理

こんにちは。

Java + SpringでTODOアプリを作るシリーズですが、今回からいよいよ例外処理を実装していきます。

まずは例外処理に慣れるためにも比較的簡単なところから始めたいので、存在しないIDにアクセスした時の例外処理を見てみましょう!

TODOアプリ作成リンク集

1: [超基礎の理解] MVCの簡単な説明
2: [雛形を用意する] Spring Initializrで雛形を作ってHello worldしたい
3: [MySQLとの接続・設定・データの表示] MySQLに仮のデータを保存 -> 全取得 -> topに表示する
4: [POST機能] 投稿機能の実装
5: [PATCH機能] TODOの表示を切り替える
6: [JpaRepositoryの簡単な使い方] 検索機能の実装
7: [Thymeleaf テンプレートフラグメントで共通化] Headerの作成
8: [PUT機能] 編集機能の実装
9: [微調整]TODOの表示を作成日時が新しい順にソートする + 期日のデフォルトを今日の日付にする
10: [springで例外処理] 例外処理についての簡単なまとめ
11: [springで例外処理] 存在しないIDのTODOにアクセスした時の例外処理(今ここ)

例外処理がどう処理されるか考えてみる

さて前回の記事では例外処理とは何なのかについて簡単に説明しました。

要約すると

ユーザーが製作者の意図しないリクエストをした時(TODO登録時に規定数以上の文字を入力する、存在しないURLにアクセスしようとする ...etc)、特定のエラーページへ誘導するすることでセキュリティ面・ユーザビリティの向上を図る。

ということになります。

例外処理の流れ

今回実装するのは存在しないIDのTODOにアクセスをリクエストした際ですので・・・

まずはコントローラー確認

com/example/todo/TodoController.java
    @GetMapping("/edit/{id}")
    public String showEdit(Model model, @PathVariable("id") long id ) {
        TodoEntity editTarget = todoService.findTodoById(id);
        model.addAttribute( "editTarget" , editTarget);
        return "edit";
    }

このshowEdit呼び出し時の例外処理となります。todoService.findTodoById(id)で Serviceクラスを経由してTODOを取得するわけですが、findTodoById()をみてみましょう。

ServiceクラスのfindTodoById()の確認

com/example/todo/TodoService.java
    public TodoEntity findTodoById(Long todoId) {
        Optional<TodoEntity> todoResult = todoRepository.findById(todoId);
        return todoResult.get();
    }

RepositoryからID検索をしてTODOを取得してOptional型で返しています。

しかしここで存在しないIDで検索しようとするとエラーで落ちてしまうので、このタイミングで

もしtodoResultが空だったら特定のExceptionクラスへ誘導する

様にすればいいでしょう!

特定のExceptionクラス?

com/example/todo/exception/TodoNotFoundException.java
package com.example.todo.exception;
public class TodoNotFoundException extends RuntimeException{
}

exceptionというディレクトリを新しく作り、その中にTodoNotFoundExceptionというクラスを作ります。

extendsによってこのクラスはRuntimeExceptionを継承していることになります。
継承したことによってこのクラスでもRunttimeExceptionのメソッドが使用できるようになります。(今回は使用しないですが・・・)

findTodoById()内でtodoResultが空だった時にTodoNotFoundExceptionクラスへ誘導する

com/example/todo/TodoService.java
    public TodoEntity findTodoById(Long todoId) {
        Optional<TodoEntity> todoResult = todoRepository.findById(todoId);
        todoResult.orElseThrow(TodoNotFoundException::new);
        return todoResult.get();
    }

returnの前に追加した1行を見てみましょう!

Optional.orElseThrow(Exception名::new)とすることでOptional型が空だった時に指定したクラスへ処理を飛ばすことが出来ます。(今回は先ほどつくったTodoNotFoundException)

ちなみにException名::newの部分はメソッド参照と呼ばれていて、メソッドの引数内としてメソッドを参照するものとなります。

こちらの記事が参考になるので見てみてください。

TodoNotFoundExceptionを管理するクラスを作る。

先ほど作ったTodoNotFoundExceptionRuntimeExceptionを継承しただけで中身は空でしたね。

com/example/todo/exception/TodoNotFoundException.java
package com.example.todo.exception;
public class TodoNotFoundException extends RuntimeException{
}

ここで@ControllerAdviceというアノテーションを使って、TodoNotFoundExceptionが呼ばれた時(=スローされた時)の処理を実装していきます。

com/example/todo/exception/TodoControllerAdvice.java
package com.example.todo.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@Slf4j
@ControllerAdvice
public class TodoControllerAdvice {
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(TodoNotFoundException.class)
    public String idNotFound() {
        log.warn("指定されたTODOが見つかりません。");
        return "error/404.html";
    }
}

exceptionディレクトリ内にこんなクラスを作ってみましょう。

上から順に説明すると・・・

@Slf4j
このアノテーションを使う事で、ログを表示できる様になります。log.warn("内容")やlog.info("内容")でターミナルにログを表示できます。

@ControllerAdvice
このクラスがControllerAdviceである事を示します。TodoControllerで例外が発生した時にどのように処理するかを設定できるようになります。

@ResponseStatus(HttpStatus.NOT_FOUND)
Not_FoundとしてHttpStatusを返します。

@ExceptionHandler(TodoNotFoundException.class)
ここが大事なのですが、このアノテーションを用いることによってTodoController内の処理でTodoNotFoundExceptionが発生した際にこのidNotFound()に処理がくるように指定しています。この関数内ではエラーログを出力しつつ、error/404.htmlを表示させるようにしています。

エラーページの作成

templates/error/404.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>
404!
</body>
</html>

本来ならエラー内容を表示させるべきですが、今回は簡素に404!と表示させるだけにします。

今回実装した例外処理の流れまとめ

さて一気に説明したので混乱しているかもしれませんが、今回の処理をまとめると以下になります。

・ユーザーがTodo編集をリクエスト(800/edit/100)

・コントローラがリクエストに応じてサービスクラスへ処理をまわす(findTodoById(100))

・ID = 100のTODOは存在しないのでTodoNotFoundExceptionが呼ばれる。

@ControllerAdviceがついたTodoControllerAdviceが「指定されたTODOが見つかりません」というログを出すと共に404.htmlへ遷移させる。

こんな感じです!

ちょっと文章だとわかりづらいかもしれないので、こちらの記事を読んでみると良いと思います。

次回も続いて例外処理の実装を進めていきます!

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

【Java】 キャッシュの影響でアップロードした画像が更新されない問題

概要

作成中のwebアプリで画像をアップロードしたはいいものの、アップロードした画像が画面で中々更新されない問題で詰まったので、自分用にメモします。
原因としてはキャッシュが悪さをしていて、更新前の画像をそのまま読み込んでしまっているためと思われます。

解決方法

結論から言ってしまえば、jsp側で表示している画像の"<img src=~"に?(何かしらの値)を付けて、ブラウザに違う画像と認識させるだけで解決する事ができました。

test.jsp
//変更前
<img src="static/img/test.jpg" alt="test.jpg">
//変更後
<img src="static/img/test.jpg?(任意の動的パラメーター)" alt="test.jpg">

画像のURLがまったく同じで何も変更がないと、ブラウザは「キャッシュを使用すればいい」と判断してしまうので、更新が反映されなくなっていたようです。

パラメーターをlastModifiedで付与する

任意の固有のパラメーターなので何でも良いのですが、せっかくなので自動で値を取得して付与するようにしてみます。
アップロードした日付と時間を、lastModifiedで取得し付与します。

Controller.java
// 対象とするファイルを選択する
File file = new File("static/img/test.jpg");
// ファイルの最終更新日時を取得し、jspで使用可能にする
model.addAttribute("file", file.lastModified());
test.jsp
<img src="/static/img/test.jpg?${file}" alt="test.jpg">

これでファイルを更新しても、更新後のものが読み込まれるようになります。

参考

Spring Bootで静的リソースのブラウザキャッシュ対応
「キャッシュのせいです」とは言わせない!更新されたファイルだけ確実にキャッシュを無効化させる方法
更新されたJS、CSS、画像のみブラウザキャッシュを破棄して読み込ませる

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

ARC104のD Multiset MeanをScala、Java、C++、Ruby、Perl、Elixirで解く

AtCoder ARC104の4問目をScala、C++、Java、Ruby、Perl、Elixirで解きました。言語ごとの処理速度の比較ができました。

問題へのリンク

結果

言語 結果
Scala TLE
C++ AC 1830ms
Java AC 2995ms
JavaっぽいScala AC 2991ms
Ruby TLE
Perl TLE
Elixir TLE

競技プログラミングにおける言語の選択と処理速度の関係は以下のツイートに言及がありました。

https://twitter.com/chokudai/status/1171321024224186369

このツイートによると、C++、Javaは大丈夫だけど、他の言語は計算量がシビアな問題には厳しいようです。

競技中は解法を見つけ出すのに時間がかかり、わかったあとはScalaで書いたものの、バグ取りに手間取って、AC(正解)に至りませんでした。競技後も続けたものの、どうしても実行時間制限4秒のTLEを解消できませんでした。

あきらめて同じロジックをC++で書いたらあっさりACを取れました。

そこで、せっかくなのでいろんな言語で書いてみて、AC取れるかを比較した結果です。

Scalaは最初はTLEだったものの、JavaでAC取ったあとにJavaを翻訳したようなScalaを書いたらScalaでもACとれました。

この記事の残りは各言語のソースコードです。

Scala

Arrayを使うなど中途半端にJavaっぽいScalaコードですが、TLEを解消できずにあきらめたコードです。

object Main extends App {
  val sc = new java.util.Scanner(System.in);
  val n, k, m = sc.nextInt();

  val table = (0 until n).map(i => Array.fill(i * (i+1) * k / 2 + 2)(0));
  table(0)(0) = 1;
  (1 until n).foreach { i =>
    val j_max = i * (i+1) * k / 2;
    val t1 = table(i - 1);
    val t2 = table(i);
    (0 to j_max).foreach { j =>
      val a1 = (((j - (i - 1) * i * k / 2) + i - 1) / i) max 0;
      val a2 = (j / i) min k;
      val s = (a1 to a2).map { a =>
        t1(j - a * i).toLong;
      }.sum % m;
      t2(j) = s.toInt;
    }
  }

  val table2 = Array.fill(n+1)(0);
  (1 to n).foreach { x =>
    if (x <= (n + 1) / 2) {
      val (n1, n2) = (x - 1, n - x);
      val s = (0 to (n1 * (n1+1) * k / 2)).map { i =>
        table(n1)(i).toLong * table(n2)(i) % m;
      }.sum % m;
      val answer = ((m.toLong + s * (k+1) - 1) % m).toInt;
      table2(x) = answer;
      println("%d".format(answer));
    } else {
      val answer = table2(n + 1 - x);
      println("%d".format(answer));
    }
  }
}

後述のJavaでAC取れたあと、JavaをScalaに翻訳したようなコードを書いたところ、Javaとほぼ同じ2991msでした。Scalaでこういうコードを書くぐらいならJavaでいいわけで、Scalaは計算量が厳しい問題には向いていないという結論です。Scalaは私にとって書きやすいので今後も使い分ければよいです。

object Main extends App {
  val sc = new java.util.Scanner(System.in);
  val n, k, m = sc.nextInt();

  val table = new Array[Array[Int]](n);
  table(0) = new Array[Int](2);
  table(0)(0) = 1;
  var i: Int = 1;
  while (i < n) {
    val j_max = i * (i+1) * k / 2;
    val t1 = table(i - 1);
    val t2 = new Array[Int](i * (i+1) * k / 2 + 2);
    table(i) = t2;
    var j: Int = 0;
    while (j <= j_max) {
      val a1 = (((j - (i - 1) * i * k / 2) + i - 1) / i) max 0;
      val a2 = (j / i) min k;
      var s: Long = 0L;
      var a: Int = a1;
      while (a <= a2) {
        s += t1(j - a * i).toLong;
        a += 1;
      }
      t2(j) = (s % m).toInt;
      j += 1;
    }
    i += 1;
  }

  val table2 = new Array[Int](n+1);
  var x: Int = 1;
  while (x <= n) {
    if (x <= (n + 1) / 2) {
      val (n1, n2) = (x - 1, n - x);
      var s: Long = 0L;
      var i: Int = 0;
      var max = (n1 * (n1+1) * k / 2);
      while (i <= max) {
        s += table(n1)(i).toLong * table(n2)(i) % m;
        i += 1;
      }
      s = s % m;
      val answer = ((m.toLong + s * (k+1) - 1) % m).toInt;
      table2(x) = answer;
      println("%d".format(answer));
    } else {
      val answer = table2(n + 1 - x);
      println("%d".format(answer));
    }
    x += 1;
  }
}

C++

同じロジックをC++で書き直したところ、1830msでAC取れました。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n, k, m;
  cin >> n >> k >> m;

  vector<vector<int>> table(n);
  table[0] = vector<int>(2);
  table[0][0] = 1;
  for (int i = 1; i < n; i++) {
    auto t1 = table[i-1];
    auto t2 = vector<int>(i * (i+1) * k / 2 + 2);
    int j_max = i * (i+1) * k / 2;
    for (int j = 0; j <= j_max; j++) {
      int a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i;
      if (a1 < 0) a1 = 0;
      int a2 = j / i;
      if (a2 > k) a2 = k;
      long s = 0;
      for (int a = a1; a <= a2; a++) {
        s += t1[j - a * i];
      }
      s = s % m;
      t2[j] = (int)s;
    }
    table[i] = t2;
  }

  auto table2 = vector<int>((n + 1) / 2 + 1);
  for (int x = 1; x <= (n + 1) / 2; x++) {
    int n1 = x - 1;
    int n2 = n - x;
    long s = 0;
    for (int i = 0; i <= n1 * (n1+1) * k / 2; i++) {
      s += (long)table[n1][i] * table[n2][i] % m;
    }
    int answer = (int)(((long)m + s * (k+1) - 1) % m);
    table2[x] = answer;
    printf("%d\n", answer);
  }
  for (int x = (n + 1) / 2 + 1; x <= n; x++) {
    printf("%d\n", table2[n + 1 - x]);
  }
}

Java

C++でACが取れ、解法が間違ってないことがわかりましたので、次はJavaで書きました。2995msでAC取れました。C++よりは遅いものの、同じJVMのScalaよりも高速です。JVMでできるということは、ScalaでもJavaっぽい書き方にすればできるはずと考えたところ、ScalaでもACが取れたのは先述のとおりです。

import java.util.Scanner;

class Main {
  public static void main(String[] args) {
    var sc = new Scanner(System.in);
    var n = sc.nextInt();
    var k = sc.nextInt();
    var m = sc.nextInt();

    var table = new int[n][];
    table[0] = new int[2];
    table[0][0] = 1;
    for (int i = 1; i < n; i++) {
      var t1 = table[i-1];
      var t2 = new int[i * (i+1) * k / 2 + 2];
      int j_max = i * (i+1) * k / 2;
      for (int j = 0; j <= j_max; j++) {
        int a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i;
        if (a1 < 0) a1 = 0;
        int a2 = j / i;
        if (a2 > k) a2 = k;
        long s = 0;
        for (int a = a1; a <= a2; a++) {
          s += t1[j - a * i];
        }
        s = s % m;
        t2[j] = (int)s;
      }
      table[i] = t2;
    }

    var table2 = new int[(n + 1) / 2 + 1];
    for (int x = 1; x <= (n + 1) / 2; x++) {
      int n1 = x - 1;
      int n2 = n - x;
      long s = 0;
      for (int i = 0; i <= n1 * (n1+1) * k / 2; i++) {
        s += (long)table[n1][i] * table[n2][i] % m;
      }
      int answer = (int)(((long)m + s * (k+1) - 1) % m);
      table2[x] = answer;
      System.out.printf("%d\n", answer);
    }
    for (int x = (n + 1) / 2 + 1; x <= n; x++) {
      System.out.printf("%d\n", table2[n + 1 - x]);
    }
  }
}

Ruby

TLEでした。

n, k, m = gets.strip.split(" ").map(&:to_i)

table = [[1]]
for i in 1..n-1
  t1 = table[i-1]
  t2 = []
  for j in 0 .. i * (i+1) * k / 2
    a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i;
    a1 = 0 if a1 < 0
    a2 = j / i;
    a2 = k if a2 > k
    s = 0;
    for a in a1 .. a2
      s += t1[j - a * i]
    end
    s = s % m
    t2.push(s)
  end
  table.push(t2)
end

table2 = [0]
for x in 1 .. (n + 1) / 2
  n1 = x - 1
  n2 = n - x
  s = 0
  for i in 0 .. n1 * (n1+1) * k / 2
    s += table[n1][i] * table[n2][i] % m;
  end
  answer = (m + s * (k+1) - 1) % m
  table2.push(answer)
  p answer
end
for x in (n + 1) / 2 + 1 .. n
  p table2[n + 1 - x]
end

Perl

TLEでした。

use strict;
use warnings;
use integer;

my $nkm = <STDIN>;
$nkm =~ s/\n\z//;
my ($n, $k, $m) = split(/ /, $nkm);

my $table = [[1]];
for (my $i = 1; $i < $n; $i++) {
    my $t1 = $table->[$i - 1];
    my $t2 = [];
    my $j_max = $i * ($i + 1) * $k / 2;
    for (my $j = 0; $j <= $j_max; $j++) {
        my $a1 = (($j - ($i - 1) * $i * $k / 2) + $i - 1) / $i;
        $a1 = 0 if $a1 < 0;
        my $a2 = $j / $i;
        $a2 = $k if $a2 > $k;
        my $s = 0;
        for (my $aa = $a1; $aa <= $a2; $aa++) {
            $s += $t1->[$j - $aa * $i];
        }
        $s = $s % $m;
        push(@$t2, $s);
    }
    push(@$table, $t2);
}

my $table2 = [0];
for (my $x = 1; $x <= ($n + 1) / 2; $x++) {
    my $n1 = $x - 1;
    my $n2 = $n - $x;
    my $s = 0;
    for (my $i = 0; $i <= $n1 * ($n1+1) * $k / 2; $i++) {
        $s += $table->[$n1]->[$i] * $table->[$n2]->[$i] % $m;
    }
    my $answer = ($m + $s * ($k+1) - 1) % $m;
    push(@$table2, $answer);
    printf("%d\n", $answer);
}
for (my $x = ($n + 1) / 2 + 1; $x <= $n; $x++) {
    printf("%d\n", $table2->[$n + 1 - $x]);
}

Elixir

TLEでした。

defmodule Main do
  def main do
    [n, k, m] = IO.read(:line) |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1)
    table = Enum.reduce(1 .. n, [[1, 0]], fn i, acc ->
      [t1 | _] = acc
      j_max = div(i * (i+1) * k, 2);
      t2 = Enum.map(0 .. j_max, fn j ->
        a1 = max(div((j - div((i - 1) * i * k, 2)) + i - 1, i), 0)
        a2 = min(div(j, i), k)
        rem(Enum.reduce(a1 .. a2, 0, fn a, acc3 ->
          acc3 + Enum.at(t1, j - a * i)
        end), m)
      end)
      [Enum.reverse(t2) | acc]
    end) |> Enum.reverse

    table2 = (1 .. div(n + 1, 2)) |> Enum.map(fn x ->
      [n1, n2] = [x - 1, n - x]
      s = Enum.reduce(0 .. div(n1 * (n1 + 1) * k, 2), 0, fn i, acc ->
        rem((table |> Enum.at(n1) |> Enum.at(i)) * (table |> Enum.at(n2) |> Enum.at(i)) + acc, m)
      end)
      rem(m + s * (k+1) - 1, m)
    end)
    (1 .. n) |> Enum.each(fn x ->
      if x <= div(n + 1, 2) do
        IO.puts(Enum.at(table2, x - 1))
      else
        IO.puts(Enum.at(table2, n - x))
      end
    end)
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】com.sun.glass.WindowEventがimportされていてウインドウが閉じない

開発環境

IDE: Eclipse photon 4.8
MacBook: Catalina 10.15.7
言語: java8

閉じるボタンでウインドウを閉じるためのコード

        addWindowListener(new WindowAdapter() {
//          @SuppressWarnings("unused")
            public void windowClosing (WindowEvent e) {
                dispose();
                System.exit(0);
            }
        });

@SuppressWarnings("unused")のところは、Eclipseが提案してきたコメント。これを挿入すればエラーは表示されなくなったけど、使用していないよと明示してJVMに教えたげてるので、使われていない変数があってもエラーとして認識しなくてもいいよ。書いた人もわかっているよということでエラーを表示させなくするためのコメント。

つまり、解決してないってこと!

当然、ウインドウを閉じることはできない。でも教材通りに書いてるつもりなんだけど。

Eclipseに頼ってたらちゃんと理解できない

// この部分でエラーが発生
public void windowClosing (WindowEvent e) {

Eclipseのエディターにはcom.sun.glass.events.WindowEventをインポートするっていう提案が一番上に出てきたので、import文を追加しました。

import com.sun.glass.events.WindowEvent;

これが元凶でした。

次に表示されたエラーが
型 new WindowAdapter(){} からのメソッド windowClosing(WindowEvent) はローカルで使用されません

Eclipseの言われた通りに修正したのにあかんやん。どゆこと?

上のエラーを簡単に言ったらメソッドの使い方がちゃうよってことだけど、、、なんでEclipseの言った通りにやったのにさ、解決してへんやんって感じでした。

そこから次のようなコメントを挿入することが提案される

@SuppressWarnings("unused")

いや、これって、使ってないよって話じゃん。。。

んで教材をみて、サンプルと比較しても該当する部分は完全に一致しているように見える。サンプルの該当部分をコピペして貼り付けても同じエラーが起こるので、スペルミスや、見えないコードが混じり込んでいることも考えられないし。。。。

というのでどんどん沼にハマって行きました。。。。

同じ名前のクラスが異なるパッケージに複数ある

また最初のエラーが出ている状態のコードに戻して、Eclipseに表示される提案部分を確認したら、スクロールして下の方にも別の提案がありました。

そこにjava.awt.Eventをインポートするっていうような提案がありました。

ひょっとしてそれじゃねぇのか?って思ったらビンゴでした。

経験が浅いとEclipseの一番上に出てくる提案部分を採用して修正すればいいんじゃないかと思っていたのが間違い。だめ絶対。

結局インポート文が間違っていた

import java.awt.Event;

とすれば一瞬で解決しました。

じゃあcom.sun.glass.WindowEventはなんなの?

micromagic_ouyou_src_myApp_BufferedStream_java_-__Users_onoharamakoto_Desktop_work_java_-_Eclipse_IDE.png

ここにありました。違うパッケージにあるんですね。ややこしい。

結局これでめっちゃ時間がかかってしまった。。。。

2時間くらいかかったけど実りはあったかなと

エラーの解決手順が身についた?

これは自分で思っていることですけど、

  1. Eclipseに頼りっきりにしない
  2. 公式ドキュメントを確認する
  3. ググってみて英語のページしかでてこないけど、ちゃんと読む
  4. JREライブラリを確認するようになった
  5. スペルミス以外のミスの経験が得られた
  6. 自分で解決した経験が得られた

たかだかインポート文が間違っていたというしょうもないミスだったのに2時間くらいかかってしまったのはアホらしいですけど、こういう経験が身になるのかなと信じています。

基本的にはエラーは自分で解決できていますが、やっぱり同様のエラーが出てきた時には教えてもらったことよりも解決するのが断然早い。

そして、なんとなくエラー解決の手順が自分なりに形になってくるので、新しいエラーに関しても手順自体は生かせるかなと感じました。

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

Dagger Hilt (DevFest 2020 資料)

DevFestの資料の記事版です。ステップごとにサンプルアプリの差分付きで説明します。
なぜDependency Injectionが必要なのか から始め、Dagger Hiltの説明、実践的なプラクティスまで説明していきます!

Dependency Injection(DI)とはなにか

なぜDIが必要なのか

DI、ちょっと難しいイメージありますが、そもそもなんで必要なんでしょうか?
作っているのが動画再生するアプリでVideoPlayerというクラスがあるとしましょう。
VideoPlayerのクラスの中にデータベースやcodecなどがハードコードされています。

コード: https://github.com/takahirom/hilt-sample-app/commit/8c36602aaa4e27d8f10c81e2808f0ff452f1c8a4#diff-bbc9d28d8bcbd080a704cacc92b8cf37R19

class VideoPlayer {
    // データベースにビデオの一覧が保存されている (Roomというライブラリを使っている)
    private val database = Room
        .databaseBuilder(
            App.instance,
            VideoDatabase::class.java,
            "database"
        )
        .createFromAsset("videos.db")
        .build()
    // 使えるコーデック一覧
    private val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)

    private var isPlaying = false

    fun play() {
...
    }
}

使うときにはこれだけで、十分シンプルに見えます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val videoPlayer = VideoPlayer()
        videoPlayer.play()
    }
}

しかし、これだけだと開発を進めていく中で以下のような問題が起こります。

  • VideoPlayerを再度使いたい場合、データベースやコーデックがハードコードされているので、データベースを変えて再生したりなどができない。
  • 実行時やデバッグで交換できない。
  • テストでも交換できない。例えばテストではメモリ内のDB使いたい場合などは困る。
  • さまざまな依存関係がコードの中に入ってくるのでコードが見にくくなり、リファクタリングを難しくする。

この問題を簡単に回避するために簡単なDIを試してみましょう!

簡単にDIしてみよう

ここでDependency Injection(DI)です。日本語だと依存性の注入です。
最初は簡単にコンストラクタインジェクションというコンストラクタを使って依存性を注入する方法を紹介します。
簡単にVideoPlayerのコンストラクタに依存関係を渡してあげるだけです。
他にもsetterによって注入する方法をsetter injectionといいます。

コンストラクタインジェクションの例
差分: https://github.com/takahirom/hilt-sample-app/commit/a1fdef28515d158577313b90f7c2590bd5905366

VideoPlayerは依存関係が交換可能で、シンプルになった!

class VideoPlayer(
    private val database: VideoDatabase,
    private val codecs: List<Codec>
) {
    private var isPlaying = false

    fun play() {
...
    }
}

コンストラクタインジェクションを使うと使う側で、そのクラスの依存関係を先に作ってからでないとインスタンス化することができません。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val videoDatabase = Room
            .databaseBuilder(
                this,
                VideoDatabase::class.java,
                "database"
            )
            .createFromAsset("videos.db")
            .build()
        val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)
        val videoPlayer = VideoPlayer(videoDatabase, codecs)
        videoPlayer.play()
    }
}

いろんな画面で、VideoPlayerを作るたびにこのようなコードを書く必要があるので、これは ボイラープレートとなりえます
この処理をよく見ると"VideoPlayerを作るための処理"と"VideoPlayer#playを呼び出す処理"があることが分かります。

VideoPlayerを作るための処理

"VideoPlayerを作るための処理"は、ただ、他の型やクラスを作るためのロジックで、これを"コンストラクションロジック"と呼びます。

val videoDatabase = Room
    .databaseBuilder(
        this,
        VideoDatabase::class.java,
        "database"
     )
     .createFromAsset("videos.db")
     .build()
val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)
val videoPlayer = VideoPlayer(videoDatabase, codecs)

VideoPlayer#playを呼び出す処理

"VideoPlayer#playを呼び出す処理"は、アプリの価値を作るためのロジックとなり、"ビジネスロジック"と呼びます。ここではコンストラクションロジック以外をこう呼ばせてください。

videoPlayer.play()

コンストラクションロジックとビジネスロジックが一緒になっていると、コードを追ったり、読んだりするのを難しくします。
またそれはクラスを読む人にとってあまり意味のないものであることが多いです。
Dependency Injection(DI)を行うライブラリでは、このコンストラクションロジックとビジネスロジックを分離することができます。

DIについてまとめ

  • さまざまな良い影響があるのでDIを行おう!
  • インスタンス化のためのボイラープレートを避けるためにDIライブラリを使おう!

AndroidではDIが難しい

フレームワークがActivityなどのインスタンスを作ってしまいます。例えば、startActivityなどするとインスタンスが作られてしまいます。(コンストラクタをいじれません。)

// Androidのフレームワークによって勝手にActivityがインスタンス化される
startActivity(Intent(context, MainActivity::class))

AndroidのAPI Level 28からFactoryでActivityを作れるようになるなど、改善はされていますが、Android 9以上でないと動作しないので、現状では現実的ではないです。

Daggerがこれまでの解決策でした。

上位1万のアプリで74%がDagger使っており、今の解決策としてはこれがメインです。
しかし、アンケートによると49%のユーザーがより良いDIの解決策を必要としていたようです。

どんなDIの解決策が必要だったか?

  • Opinionated(意見を持った。)
    決めるのを楽にして、使うのを楽にする (後述)
  • セットアップが簡単
    (DaggerはAndroidではなく、Java用だったため、Daggerは設定が難しかった)
  • 重要な部分にフォーカスできる

これがDagger Hiltが生まれたメインの理由のようです。

Dagger Hilt

Dagger HiltはDaggerの上に構築されたライブラリです。
Daggerの良いところを使うことができます。
GoogleのAndroidXチームとDaggerチームで共同で作られています。

  • AndroidにおけるDIの標準化
    どのようにAndroidでDIするのかを標準化(standardize)します。
  • Dagger上に構築
    Daggerのためのコードを生成します。
  • アノテーションベース
    Hiltに対して、アノテーションで何をしたいのかを伝えます。
  • ツールサポート
    Android Studio 4.1以降では左側にガターアイコンが表示される。例えば依存関係がどこから来たのかがわかります。
  • AndroidX Extension
    ViewModelとWorkManagerで使える。他のライブラリも追加予定です。

Dagger HiltをVideoPlayerの例で使ってみよう

どうにかして、Dagger HiltにVideoPlayerの作り方を教えないといけないですが、どのように教えたら良いでしょうか?
一番基本的なDagger Hiltへのインスタンスの作り方の教え方は、コンストラクタに@Injectをつけることでです。
現状はVideoPlayerに依存関係をもたせていません。
Dagger Hiltはこのクラスをただインスタンス化するだけなので、作ることができます。

class VideoPlayer @Inject constructor() {
    private var isPlaying = false

    fun play() {
...
    }
}

次にこのアプリはHiltで動くということをHiltに教えないといけません。
AndroidのApplicationクラスで@HiltAndroidAppを使うと、このアプリがHiltで動くということを教えられます。また、@HiltAndroidAppを使うと内部的にはComponentが作られます。このComponentとは、コンストラクションロジックと、作ったインスタンスを保持する部分です。(Componentについては後でもう一度触れます。)

@HiltAndroidApp
class VideoApp : Application()

Activtyのコンストラクタがいじれないということでしたが、その対応としてActivityに@AndroidEntryPointをつけます。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@AndroidEntryPointをつけることで、3つのことをHiltに対して教えます。

  • "このActivityはInjectionを行う。このActivityはDependency Injectionを使う"
  • "ActivityのComponentをこのActivityに追加。"
  • "HiltからDependencyを持ってくる"

変数に@Injectアノテーションを付けています。
これはHiltからInjectされることを意味します。"Actiivtyが作られたときに、VideoPlayerをInjectして" とHiltに教えている形です。
そして、onCreateメソッドではVideoPlayerを呼ぶなど好きなことができます。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // ↓ Dagger HiltによってonCreateでInjectされる
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayer.play()
    }
}

差分: https://github.com/takahirom/hilt-sample-app/commit/6a8a3711808e806e5953712adeb19b11cb73c3a9#diff-bbc9d28d8bcbd080a704cacc92b8cf37R24

魔法?どこでフィールドに代入されるの?

少しだけ中身の仕組みを知っておいたほうが分かりやすいと思うので、説明しておきます。
@AndroidEntryPointがついているActivityはHiltによって変換されます。
Hiltによって変換後にMainActivityとAppCompatActivityの間に
生成されたHilt_MainActivityが入ります。
Hilt_MainActivityのonCreateの中でフィールドにInjectされます。

image.png

Hiltによって変換後のコード

@AndroidEntryPoint
class MainActivity : Hilt_MainActivity() { // Hilt_MainActivityになっている
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        // この中でフィールドにInjectされる
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayer.play()
    }
}


VideoPlayerにVideoDatabaseをInjectさせるには?

これで一応MainActivityでVideoPlayerが使えるようになりました。ただ、これではVideoPlayerがDatabaseなどにアクセスできません。どのようにしていけばいいでしょうか?

// ↓ Databaseなどを持っていない
class VideoPlayer @Inject constructor() {
    private var isPlaying = false

    fun play() {
...
    }
}

もし、VideoDatabaseのコンストラクタを変更することができるのであれば、同様にVideoDatabaseに @Injectをつけて、Hiltにインスタンスの作り方を教えられます。Daggerが勝手に依存関係のインスタンス、VideoDatabaseを先に作ってからVideoPlayerをインスタンス化してくれます。

class VideoPlayer @Inject constructor(
  private val database: VideoDatabase
) {
    private var isPlaying = false

    fun play() {
...
    }
}
class VideoDatabase @Inject constructor() {
...
}

ただ今回はRoomによって作られるインスタンスになるので、コンストラクタをいじって@Injectをつけることができません。そのためどうにかしてDagger Hiltに以下のVideoDatabaseの作り方を教えないといけません。

val videoDatabase = Room
    .databaseBuilder(
        this,
        VideoDatabase::class.java,
        "database"
     )
     .createFromAsset("videos.db")
     .build()

そこでModuleを使います。Moduleを使うことで、Hiltにインスタンスの作り方を教えることができます。
@Module@InstallInというアノテーションを付けるただのクラスです。
そのModuleにはメソッドを追加します。
Moduleにあるメソッドは料理のレシピだと考えると分かりやすいです。VideoDatabaseの型を作るレシピになり、このレシピをHiltに教えています。
このレシピを@InstallInSingletonComponentに置きます。SingletonComponent は ApplicationのComponentに追加するということです。

メソッドをよく見ると@Providesと書いてあることが分かります。HiltにどのようにVideoDatabaseを作るのかを教えるメソッドということを教えています。
HiltがVideoDatabaseを作る必要があるときに、このメソッドを実行してインスタンスを返します。
ちなみに、contextクラスのインスタンスが@ApplicationContextで提供されていますが、Hiltで事前定義されたクラスもいくつかあり、Hiltがいくつかのインスタンスを提供してくれます。
差分: https://github.com/takahirom/hilt-sample-app/commit/c85a6f668a0bf447c0a4b119f4f6d8cc8c2cff80

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
        return Room
            .databaseBuilder(
                context,
                VideoDatabase::class.java,
                "database"
            )
            .createFromAsset("videos.db")
            .build()
    }
}

さて、この SingletonComponentが出てきましたが、 Componentについてもう少し触れておきましょう。

Component

Componentは以下のことができます。

  • どのようにオブジェクトをビルドするのかのロジックを持っています。
    VideoDatabaseはこうやって作る。。など
    VideDatabase : Room.databaseBuilder() … .build()
  • インスタンス生成の順序のロジックを持っています。

    "VideoDatabaseの後にVideoPlayerを生成する。"など

  • スコープによってインスタンスを使い回します。 (後述)

Dagger Hilt標準のComponent

HiltはOpnionedであると言いましたが、Dagger Hiltには標準のComponentが存在し、Componentの構造について迷わなくて良くなっています。
この図はComponentの階層を表しており、アプリケーション全体のSingletonComponent、画面回転でも生き残るActivityRetainedCompoent、Activityと紐づくActivityComponentなどのComponentがある構造になっています。
上についているアノテーションはスコープアノテーションです。これについては後ほど説明します
例えば、SingletonComponentやConfigration Changeでも生き残るActivityRetainedComponent、ActivityComponent、そしてFragmentComponentなどが付随します。

image.png
https://dagger.dev/hilt/components より

今回の例ではSingletonComponentにVideoPlayerの作り方とVideoDatabaseの作り方が入っており、その作成順序も入っています。
image.png

インスタンスを共有したいときはどうするのか

例えば現状ではVideoDatabaseは現在使われるたびにインスタンス化されてしまいます。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var videoPlayer: VideoPlayer
    @Inject
    lateinit var videoPlayer2: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println(videoPlayer.database)
        // VideoDatabase_Impl@764b474 ← ハッシュコードが違う
        println(videoPlayer2.database)
        // VideoDatabase_Impl@a945d9d ← ハッシュコードが違う
    }
}

class VideoPlayer @Inject constructor(
    val database: VideoDatabase
)
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
        return ...
    }
}

コネクションを使いまわしたいなど様々な理由で、インスタンスを使いまわしたいことがあります。また今回の例とは関係ないですが、Androidの通信で一般的に使われるOkHttpはインスタンスをアプリ全体で使いまわすとパフォーマンスが良くなります。 (https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/ より)

Activity内で共有したい場合は、@ActivityScopedを使うことでActivityComponentで保持されるため、
Activity内で同じインスタンスを使い回せます。この@ActivityScopedをScope Annotationといいます

これでActivity内でインスタンスを共有できます。
差分: https://github.com/takahirom/hilt-sample-app/commit/f895dfac123a0317b9e0e247af3a48b57388ad5d

@Module
@InstallIn(ActivityComponent::class)
object DataModule {
    @ActivityScoped
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
...
    }
}

@ActivityScopedではActivityが違えば別のインスタンスになってしまいます。アプリ全体で使いまわしたいときは@SingletonのScope Annotationを使うことでSingletonComponentで保持されるため、アプリ全体で使えます。
差分: https://github.com/takahirom/hilt-sample-app/commit/64cc1b50388cf9c79fba26a774ae86efb1f093bc

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Singleton
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
...
    }
}

VideoDtabaseはSingletonComponent内でインスタンスも保持されるようになりました。

image.png

Hiltが管理していないクラスから、Hiltの依存関係を使いたい時

Hiltが管理しているMainActivityやVideoPlayerクラスではHiltから依存関係を取得できますが、Hiltが管理していないクラスでは取得が難しい場合があります。
例えば、ContentProviderクラスや、他のライブラリが生成するクラス、Dagger Hiltにマイグレーションしているときの既存のクラスなどです。
ここで、EntryPointという仕組みが使えます。EntryPointを使うことで、HiltのもつComponentが持つ依存関係にアクセスすることができます。

これはHiltで管理できていないActivityでHiltがコンストラクションロジックをもつVideoPlayerを使う例となります。
差分: https://github.com/takahirom/hilt-sample-app/commit/d66fb46b395b0c9b6a98ff91bd55f3c4f12c99c9

class NonHiltActivity : AppCompatActivity() {
    @EntryPoint // @EntryPointをつける。
    @InstallIn(SingletonComponent::class)
    interface NonHiltActivityEntryPoint {
        fun videoPlayer(): VideoPlayer
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            NonHiltActivityEntryPoint::class.java
        )
        val videoPlayer = entryPoint.videoPlayer()
        videoPlayer.play()
    }
}

テスト

一般的なテストの書き方と同じように自分で対象のオブジェクトnewしてテストを書くことができます。
この場合、自分で依存関係を先に作らなくてはいけません。
差分: https://github.com/takahirom/hilt-sample-app/commit/068082bf7bcb20ecbb1258ac6a3027988d624303

    @Test
    fun normalTest() {
        // メモリ内にDBを作る
        val database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            VideoDatabase::class.java
        ).build()
        val videoPlayer = VideoPlayer(database)

        videoPlayer.play()

        assertThat(videoPlayer.isPlaying, `is`(true))
    }

Hiltを使うと、このように自分で依存関係を作らずにHiltにインスタンスを作らせることができます。
しかし、今回は実際のDatabaseを使わずにメモリ内にDatabaseを使ってそれを使いたいです。どのようにするでしょうか?

@HiltAndroidTest
class VideoPlayerTest {
    @get:Rule
    var hiltAndroidRule = HiltAndroidRule(this)

    @Inject
    lateinit var videoPlayer: VideoPlayer

    @Test
    fun play() {
        hiltAndroidRule.inject()

        videoPlayer.play()

        assertThat(videoPlayer.isPlaying, `is`(true))
    }

ここで@UninstallModules(DataModule::class)することで、Dagger HiltからDataModuleが持つVideoDatabaseの作り方を忘れさせることができます
そして、テストの中で、新たなModuleを定義することで、Databaseを提供することができます。 テストの外でModuleを宣言するとテスト全体でInstallされます。
差分: https://github.com/takahirom/hilt-sample-app/commit/4c862ee62e8dfc133ea6e7e3ff0735c0497cfb6a

@HiltAndroidTest
@UninstallModules(DataModule::class)
@RunWith(RobolectricTestRunner::class)
class VideoPlayerTest {
    @InstallIn(SingletonComponent::class)
    @Module
    class TestDataModule {
        @Provides
        fun provideVideoDatabase(): VideoDatabase {
            return Room.inMemoryDatabaseBuilder(
                ApplicationProvider.getApplicationContext(),
                VideoDatabase::class.java
            ).build()
        }
    }

Daggerからの Dagger Hiltマイグレーションについて

Daggerを知っている人向けの話になりますので、Dagger分からんという方は、へーっていうぐらいで見てください。

DaggerからDagger Hiltに一歩ずつマイグレーションしていく方法について触れておきます。

Hiltへの導入準備

Daggerのライブラリをバージョンアップしておく

→ 普通にマイグレーションするだけです。

Daggerのコンポーネントの状況を見てみる

Daggerのコンポーネントの図を出すツールは昔からいろんなプラグインがあります。
Dagger SPI(Service provider interface)というDagger内部の
情報を取り出せるDaggerのAPIのようなものがあるので、それを使っているツールで確認することをおすすめします。

https://github.com/arunkumar9t2/scabbard
https://github.com/Snapchat/dagger-browser
など

そしてある程度、どのコンポーネントが、どのコンポーネントに対応付けられそうかを見てみます。
scabbardを使った図の例
image.png

今回の状況の前提

例とする状況はApplicationレベルのコンポーネントのAppComponentがあり、その下にActivityごとのComponentがたくさんあるような形になっているとします。
(Daggerだとコンポーネントの形が標準化されていないので、そもそものComponentの形もさまざまになります。)

AppComponentの作り方でModuleを引数を渡す形で作っている場合はそれをやめる

Dagger Hiltはこのモジュールを渡し方して作る作り方をサポートしていないためやめる必要があります。Moduleの引数をなくして参照してあげることで可能です。
Daggerのドキュメント的にもそれは良くないらしいです。(don't do thisって書いてあります https://dagger.dev/dev-guide/testing )

NG

    DaggerAppComponent
      .builder()
      .networkModule(networkModule)
    .build()

OK

    DaggerAppComponent.factory()
      .create(application)

Hiltを導入してAppComponentを置き換える

ApplicationレベルのComponentをDagger HiltのSingletonComponentに置き換える

Dagger Hiltのライブラリを導入します。これは基本的なドキュメントをご確認ください。
https://dagger.dev/hilt/migration-guide

少しずつマイグレーションしていく場合はdisableModulesHaveInstallInCheckを入れる必要があります。

Dagger Hiltで標準だと@InstallInが入っていない既存のモジュールがあるときにエラーになってしまいます。このオプションを入れるとそのエラーを出さなくして、ただライブラリを入れるだけということができます。

 javaCompileOptions {
      annotationProcessorOptions {
           // ↓ **ここが+=になっていないとDagger Hiltのプラグインがargumentを追加するのでハマるので注意**
           arguments += [
               "dagger.hilt.disableModulesHaveInstallInCheck": "true"
           ]
      }
  }

Dagger HiltのEntryPointはComponentから依存関係をとってくるだけでなく、サブコンポーネントを作ることもできるので、その機能を使って置き換えます。 (ここは説明飛ばします。)
差分: https://github.com/takahirom/hilt-sample-app/commit/8e542f191bb50ce50db30cb2a72a569f7d17b178

@Subcomponent
interface JustDaggerComponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(): JustDaggerComponent
    }

    fun inject(justDaggerActivity: JustDaggerActivity)
}

@InstallIn(SingletonComponent::class)
@EntryPoint
interface JustDaggerEntryPoint {
    fun activityComponentFactory(): JustDaggerComponent.Factory
}

class JustDaggerActivity : AppCompatActivity() {
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        // old: appComponent.justDaggerComponent().inject(this)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            JustDaggerEntryPoint::class.java
        )
        entryPoint.activityComponentFactory().create().inject(this)

        videoPlayer.play()
    }
}

既存のActivityのComponentをHiltに置き換えていく

基本的に @AndroidEntryPointをつけて、既存のDaggerの処理を外していくということになります。
JustDaggerActivityの形をMainActivityの形に変えていきます。

マイグレーションについては、他にもいろいろありますが、コードラボが詳しいので考えている方はCodelabをやってみてください。

Dagger HiltとJetpackの連携

よく開発で利用されるViewModelやWorkManagerといったJetpackのComponentと連携するライブラリが提供されていて、それを利用することができます。
差分: https://github.com/takahirom/hilt-sample-app/commit/1bec3370fec0fd5b4233db1884e8427bcf91a540
Androidアプリの開発ではViewModelをよく使います。まずはViewModelについて見ていきましょう。
ViewModelではコンストラクタに変更が加えられますが、通常、ProviderやFactoryなどを経由して作成するため難しいですが、この部分をDagger Hiltはうまく隠蔽して簡単にViewModelを作ってくれます。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val videoPlayerViewModel: VideoPlayerViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayerViewModel.play()
    }
}

class VideoPlayerViewModel @ViewModelInject constructor(
    private val videoPlayer: VideoPlayer
) : ViewModel() {
    fun play() {
        videoPlayer.play()
    }
}

Dagger Hilt 実践プラクティス

Google(or Googler)のサンプルを参照する

迷ったら、サンプルアプリ達を見てみましょう。

Architecture Samples
https://github.com/android/architecture-samples/tree/dev-hilt
Goole I/Oアプリ
https://github.com/google/iosched
Sunflower
https://github.com/android/sunflower
chrisbanes/tivi
https://github.com/chrisbanes/tivi

fastInitモードが有効になるので、影響を確認する

Dagger HiltはComponentの形が標準化されたことによって、たくさんのSingletonComponentなどのコンポーネントに、この型はこう作るなどのバインディングが入るようになります。
通常、Daggerではこのバインディングの数が増えると増えるだけインスタンス化に時間がかかります。Dagger Hiltを入れたタイミングで通常のモードではなくfastInitモードが有効になることで、これが時間がかからなくなります。しかし、この処理にはトレードオフもあるようなので、リリース後にFirebase PerformanceやAndroid Vitalsなどで確認してみましょう。

    val PROCESSOR_OPTIONS = listOf(
      "dagger.fastInit" to "enabled",

https://github.com/google/dagger/blob/d3c1d2025a87201497aacb0a294f41b322767a09/java/dagger/hilt/android/plugin/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt#L108


生成されるコードの比較

fastInitがdisabledになっている場合
public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC {
  private Provider<Context> provideContextProvider;

  private Provider<VideoDatabase> provideVideoDBProvider;

  private DaggerApp_HiltComponents_SingletonC(
      ApplicationContextModule applicationContextModuleParam) {

    initialize(applicationContextModuleParam);
  }
...

  @SuppressWarnings("unchecked")
  private void initialize(final ApplicationContextModule applicationContextModuleParam) {
    this.provideContextProvider = ApplicationContextModule_ProvideContextFactory.create(applicationContextModuleParam);
    this.provideVideoDBProvider = DoubleCheck.provider(DataModule_ProvideVideoDBFactory.create(provideContextProvider));
  }


  @Override
  public VideoPlayer videoPlayer() {
    return new VideoPlayer(provideVideoDBProvider.get());
  }

fastInitがenabled
Providerが値を保持する代わりにComponentが値を保持するようになる。

public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC {
  private final ApplicationContextModule applicationContextModule;

  private volatile Object videoDatabase = new MemoizedSentinel();

  private DaggerApp_HiltComponents_SingletonC(
      ApplicationContextModule applicationContextModuleParam) {
    this.applicationContextModule = applicationContextModuleParam;
  }

  private VideoDatabase videoDatabase() {
    Object local = videoDatabase;
    if (local instanceof MemoizedSentinel) {
      synchronized (local) {
        local = videoDatabase;
        if (local instanceof MemoizedSentinel) {
          local = DataModule_ProvideVideoDBFactory.provideVideoDB(ApplicationContextModule_ProvideContextFactory.provideContext(applicationContextModule));
          videoDatabase = DoubleCheck.reentrantCheck(videoDatabase, local);
        }
      }
    }
    return (VideoDatabase) local;
  }

  @Override
  public VideoPlayer videoPlayer() {
    return new VideoPlayer(videoDatabase());
  }


詳細画面などでIDを渡していきたい場合はどうするのか?

Dagger HiltではComponentの構造が標準化されているため、例えばEpisodeDetailComponentを作って、そこで画面詳細IDを配布というようなことは難しいです。
これに対してさまざまなやり方が考えられますが、Googleのサンプルでのやり方は一つのようです。
Daggerを使って配布せず、直接渡す方法です。
HiltのComponentを作るための公式のページに、バックグラウンドタスクの話で少し文脈は違うのですが、"だいたいは自分で渡したほうがシンプルで十分"という話が出てきます。
一番渡しがちなAssisted InjectについてはただViewModelに渡すときは少しだけ工夫することができます。

https://dagger.dev/hilt/custom-components

for most background tasks, a component really isn’t necessary and only adds complexity where simply passing a couple objects on the call stack is simpler and sufficient.

コールスタックで引数を渡すだけのほうがシンプルで十分で、Componentは複雑さを増すだけ。ということを言っています。

Architecture Samples
https://github.com/android/architecture-samples/blob/dev-hilt/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt#L90
Iosched
https://github.com/google/iosched/blob/b428d2be4bb96bd423e47cb709c906ce5d02150f/mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerViewModel.kt#L101
Sunflower
https://github.com/android/sunflower/blob/2bbe628f3eb697091567c3be8f756cfb7eb7258a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt#L55
chrisbanes/tivi
https://github.com/chrisbanes/tivi/blob/27348c6e4705c707ceaa1edc1a3080efa06109ae/ui-showdetails/src/main/java/app/tivi/showdetails/details/ShowDetailsFragment.kt#L60

ViewModelにコンストラクタで値を渡す

“IDなどを直接渡していく”ということでしたが、ViewModelのコンストラクタで渡せないと、lateinitにしたり、nullableにして少し安全じゃない形になっちゃいますよね?
AssistedInjectというライブラリで、コンストラクタ引数を渡す実装ができる。(今後Daggerにも組み込まれるそうです)

差分: https://github.com/takahirom/hilt-sample-app/commit/6584808f8fe13cc92317df50d413f828d1dfdf00

DaggerはAssistedInjectと呼ばれるものを対応しようとしています。これはInjectするときに、プログラムから引数で値を渡せるというものです。
https://github.com/google/dagger/issues/1825

これを先に使えるライブラリがあります。
https://github.com/square/AssistedInject

具体的にはGooglerの以下のgistの内容を使ってViewModelにコンストラクタで値を渡すことができます。
https://gist.github.com/manuelvicnt/437668cda3a891d347e134b1de29aee1

本質的に理解しようとすると大変なので、仕組みが気になる方は以下を読んでみてください。
https://qiita.com/takahirom/items/f28ceb7a6d4e69e4dafe

EntryPointの定義場所

EntryPointは、基本的にはGoogleのサンプルでは使われていないようです。
大きいアプリのマイグレーションなどでは使われると思われるので、紹介しておきます。
EntryPointはどこにでも書くことができますが、どこに書くのがいいでしょうか?
依存関係を取得するときに使うEntryPointは、使う場合には必要な依存関係のみを取得したほうが依存するオブジェクトを少なくできるので、基本的には取得する場所に定義して利用していきましょう

class NonHiltActivity : AppCompatActivity() {
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface NonHiltActivityEntryPoint {
        fun videoPlayer(): VideoPlayer
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            NonHiltActivityEntryPoint::class.java
        )
        val videoPlayer = entryPoint.videoPlayer()
        videoPlayer.play()
    }
}

マルチモジュール

まだちゃんとしたベストプラクティスがあるわけではないと思いますが、Dagger Hiltとマルチモジュールについて考えておきます。以下のようなモジュール構成があったとします。
image.png

Applicationクラスをコンパイルする Gradleモジュールは、
すべての Hilt モジュールおよびコンストラクタ インジェクションで注入するクラスを
推移的依存関係に含める必要があります

https://developer.android.com/training/dependency-injection/hilt-multi-module?hl=ja より

ということなので、以下のようにルートのモジュールからDaggerのModuleを持つGradleモジュールへの参照が必要になります
この部分に関して、真ん中の形が色んなパターンで無駄に依存関係を増やさずに動くので、いいのかなとは思うのですが、まだベストプラクティスと言えるものはないです。
ただ、モジュールを作ったときにクラスパスに含めるだけで@InstallInされたModuleがコンポーネントにインストールされて使えるので、すごく楽で、本当に使いやすいです。

image.png

参考
Googleの方のchrisbanes/tiviのアプリのappモジュールからの参照
ルートのGradleモジュールから各モジュールに参照していることが分かる。
image.png

Hiltでビルドできる?できない?みたいな実験する環境作るのめんどうですよね?Hiltが導入済みのアプリで実験したくなったらこのサンプルプロジェクトを用意しているので、実験してみてください。
https://github.com/takahirom/dagger-transitive-playground/tree/hilt

テストのプラクティス

実際の依存関係を使おう

Hilt Testing Philosophyというページが有り、Dagger Hiltを使ってテストをする際のプラクティスが書いてあります。これがかなり主張を含んだもので面白いのでぜひ読んでみてください。
https://dagger.dev/hilt/testing-philosophy
こちらに自分のメモがあります。
https://qiita.com/takahirom/items/a3e406b067ad645605da

これによると2つ言いたいことがあるようです。

  • ユーザーからの観点でテストする
    ユーザーとは実際のユーザーもクラスのユーザーも、APIのユーザーも含む。internalなメソッド名や実装になど依存せず、テストが壊れることがユーザーの観点から変更があったことを意味する
  • 実際の依存関係を利用する

なぜ実際の依存関係を使うか?

  • 実際の依存関係は本当の問題を捕捉しやすい。モックのように古いまま残されたりしない。
  • "ユーザーからの観点でテスト"と組み合わせることで、同じカバレッジでもっと少ないテストの量で書くことができる
  • テストが壊れることが、FakeやMockの設定ミスによる問題による問題の代わりに、実際の問題を指し示す (そして、逆に言えばテストがパスすることはコードがちゃんと動くことを意味する)
  • "ユーザーからの観点でテスト"と"実際の依存関係を使う"は相性が良い。依存関係を入れ替えないため。

どうやってDagger Hiltで実際の依存関係を使うか?

普通にテストして実際の依存関係を使おうとすると依存関係を作るためのボイラープレートが発生します
PlayerFragmentを作るためにViewModelのFactoryが必要で、ViewModelを作るにはVideoPlayerが必要で、VideoPlayerを作るにはVideoDatabaseが必要になるなどを書いていくと。。大変になります。

launchFragment {
    // たいへん!
    PlayerFragment().apply {
        videoPlayerViewModelAssistedFactory =
            object : VideoPlayerViewModel.AssistedFactory {
                override fun create(videoId: String): VideoPlayerViewModel {
                    return VideoPlayerViewModel(
                        videoPlayer = VideoPlayer(
                            database = Room.inMemoryDatabaseBuilder(
                                ApplicationProvider.getApplicationContext(),
                                VideoDatabase::class.java
                            ).build()
                        ),
                        videoId = "video_id"
                    )
                }
            }
    }
}
onView(withText("playing")).check(matches(isDisplayed()))

紹介したように、Dagger Hiltを使ってテストでもInjectを行うことで実際の依存関係を使ってテストをすることが可能になります。

@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@UninstallModules(DataModule::class)
class AndroidPlayerFragmentTest {
    @InstallIn(SingletonComponent::class)
    @Module
    class TestDataModule {
        @Provides
        fun provideVideoDatabase(): VideoDatabase {
            return Room.inMemoryDatabaseBuilder(
                ApplicationProvider.getApplicationContext(),
                VideoDatabase::class.java
            ).build()
        }
    }

    @get:Rule
    var hiltAndroidRule = HiltAndroidRule(this)

    @Test
    fun play() {
        hiltAndroidRule.inject()
        launchFragmentInHiltContainer<PlayerFragment> {
        }
        onView(withText("playing")).check(matches(isDisplayed()))
    }

さまざまなデメリットがあるのでTestでのCustom Applicationクラスはやめよう

一応@CustomTestApplicationでカスタムアプリケーションを使ってテストできます。
テスト用のTestAppクラスなどを作っているのであればHiltと組み合わせると
さまざまなデメリットが発生します。また一般的にやめておいたほうが良さそうです。

  • テストでは以下の問題があるため、:カスタムアプリケーションでは@Injectのフィールドを使えない*。(コンパイルエラー)
  • Applicationはテストをまたいで生存してしまうので、テストをまたいだ状態のリークを起こす
  • 子が親に依存するテストになってしまう ので、テストの独立性を高めるためにやめておくべき。など

Dagger Hiltの細かいTips

HiltAndroidAutoInjectRuleなどを用意しておくと自分でInjectを呼ばなくても動くようになります。

@get:Rule val hiltAndroidAutoInjectRule = HiltAndroidAutoInjectRule(this)

class HiltAndroidAutoInjectRule(testInstance: Any) : TestRule {
  private val hiltAndroidRule = HiltAndroidRule(testInstance)
  private val delegate = RuleChain
    .outerRule(hiltAndroidRule)
    .around(HiltInjectRule(hiltAndroidRule))

  override fun apply(base: Statement?, description: Description?): Statement {
    return delegate.apply(base, description)
  }
}
class HiltInjectRule(val rule: HiltAndroidRule) : TestWatcher() {
  override fun starting(description: Description?) {
    super.starting(description)
    rule.inject()
  }
}

まとめ

以下について話してきました。

  • なぜDIを使うのか
  • なぜDagger Hiltなのか
  • 基本的な使い方や概念
  • テスト
  • マイグレーション
  • 実践プラクティス
  • Tips

Dagger Hiltを使うことでさまざまないい効果が見込めるので、Dagger Hiltを使ってアプリを作ってみましょう!

参考

公式ウェブページ
https://dagger.dev/hilt/
https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

Android Dependency Injection
https://www.youtube.com/watch?v=B56oV3IHMxg
Dagger Hilt Deep Dive
https://www.youtube.com/watch?v=4di2TTqeCrE

Architecture Samples
https://github.com/android/architecture-samples/tree/dev-hilt
Goole I/Oアプリ
https://github.com/google/iosched
Sunflower
https://github.com/android/sunflower
chrisbanes/tivi
https://github.com/chrisbanes/tivi

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