ModelMapperのcustom mappingについて

エキサイト株式会社 メディアサービスエンジニアの中尾です。 前回に続いてModelMapperのちょっとした内容を説明します。 現場ではほとんど使うことはないと思うので、「こんなこともできるんだ」ぐらいでお願いします。

コードは以下になります。

@ExtendWith(MockitoExtension.class)
public class ModelMapperTest {

    @Test
    public void testModelMapper() {
        final ModelMapper modelMapper = new ModelMapper();
        modelMapper.addMappings(new PropertyMap<TestModel, Model>() {
            @Override
            protected void configure() {
                using(toUserId).map(source, destination.getUserId());
                using(toFirstname).map(source, destination.getFirstname());
                using(toLastname).map(source, destination.getLastname());
            }
        });

        final TestModel testModel = new TestModel();
        testModel.setUserId("10000");
        testModel.setUserName("naka sho");

        final Model map = modelMapper.map(testModel, Model.class);
        Assertions.assertEquals(
                10000,
                map.getUserId()
        );
        Assertions.assertEquals(
                "naka",
                map.getFirstname()
        );
        Assertions.assertEquals(
                "sho",
                map.getLastname()
        );
    }

    private Converter<TestModel, Integer> toUserId = context ->
            Integer.valueOf(context.getSource().getUserId());

    private Converter<TestModel, String> toFirstname = context ->
    {
        final String[] s = context.getSource().getUserName().split(" ");
        return s[0];
    };

    private Converter<TestModel, String> toLastname = context ->
    {
        final String[] s = context.getSource().getUserName().split(" ");
        return s[1];
    };

    @Data
    public static class Model {
        private int userId;
        private String firstname;
        private String lastname;
    }

    @Data
    public static class TestModel {
        private String userId;
        private String userName;
    }
}

解説

addMappingsを使って、PropertyMapで元のモデルと先のモデルの設定をします。

configureをOverrideして、usingを使ってConverterを各プロパティごとに設定してください。

設定していない場合はModelMapperがよしなにmappingします。

mapping処理をModelMapper内に内包することができますが、SpringBootでbean化している場合はシングルトンになっておりますので予期せぬところで障害が発生するかもしれません。

ModelMapperのbeanを大量に作ることで避けられるかもしれませんが、そもそもmappingをModelMapper頼りにしてもいいのでしょうか?

現場と相談して使ってください。

TabBarの選択ボタンを中央寄せにするアニメーション付き無限スクロール化するTips2

エキサイト株式会社の高野です。
前回 TabBarの選択ボタンを中央寄せにするアニメーション付き無限スクロール化するTips を書いたのですが、また別の方法で実装しましたのでその方法を紹介します。
(前回と同様にTabBarをListViewで実装する際の一例です。)

各バージョン

Flutter: 2.0.6
iOS: 14.5
Android: 11.0

使用ライブラリ

scrollable_positioned_list

前回の実装

前回の実装ではAndroidの各種端末とiPhoneの指定すべきinitialOffsetが違うのでListViewが生成途中に中央に寄せるメソッドが発火していたので以下のようになっていました。
f:id:exRyusei1026:20210617163231g:plain

修正後実装方法

まず以下のようにControllerのインスタンスを生成します。今回はListenerを使用していないので必要な場合は同様に生成してください

// Controllerの生成
final ItemScrollController itemScrollController = ItemScrollController();
// Listenerの生成
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();

そうしましたら、以下のようにbuilderを用いてTabの生成を記述します。

ScrollablePositionedList.builder(
     itemScrollController: scrollController,
     initialScrollIndex: (allTabQuantity / 2) + (allTabQuantity % 2),
     initialAlignment: alignment,
     scrollDirection: Axis.horizontal,
     itemCount: allTabQuantity,
     itemBuilder: (_, index) {
         return Button();
     },
),

initialScrollIndex で中央の要素を指定し、initialAlignment で指定した要素の位置を指定することができます。
alignmentで指定する際にコメントで

  /// * 0 aligns the left edge of the item with the left edge of the view
  /// * 1 aligns the left edge of the item with the right edge of the view.
  /// * 0.5 aligns the left edge of the item with the center of the view.

このように書かれています。0.5を指定すると要素の左端が中央に揃えられるとのことですが、自分の期待しているのは要素の中央を画面の中央に揃えて欲しいのです。
なのでalignmentを中央に来るように指定しなければなりません。
そのために MediaQuery.of(context).size.width とボタンサイズを用いて画面のWidthに対するボタンの半分のWidthの割合を導出していきます。式としては、

halfButtonRatio = ButtonWidth / 2 / MediaQuery.of(context).size.width;

以上のようになります。
この halfButtonRatio を0.5から引くことによってボタンの中央が画面の中央に来るalignmentを導出することができました。
また、タップイベントは

itemScrollController.scrollTo(
  index: index,
  duration: duration,
  alignment: 0.5 - halfButtonRatio,
);

以上でタップの際に中央にアニメーションをすることができます。
f:id:exRyusei1026:20210617163320g:plain

まとめ

一部の例としてTabBarを無限スクロールにする方法でした。
ライブラリで良さそうなものや別の実装方法がありましたらコメントしていただけると幸いです。

現在、エキサイト株式会社では採用に絶賛力を入れています。
インターンでは以下のものが公開されています。
▼入門コース
https://www.wantedly.com/projects/630117
▼実戦コース
https://www.wantedly.com/projects/644989

よければお気軽にご応募ください。

Android 12での通話に関する通知アクション

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

今回はAndroid 12から追加となった、新しい通知スタイルの種類についてお話しします。

※ 今回紹介する内容は Android 12 Beta におけるものです。正式版では仕様が変わっている可能性もあるため詳細については公式のドキュメントを参照してください。

今までの通知アクション

Androidアプリでは通知に対してアクションを設定することができます。

例えば通話の着信に対して応答するなどです。

しかし、基本的にはアクションの見た目をカスタマイズすることはできません。

コード上ではアイコンを設定することもできますが、実際にはAndroid 7.0以降の場合には表示されません。

Android Developers Blog: Notifications in Android N

You’ll note that the icons are not present in the new notifications; instead more room is provided for the labels themselves in the constrained space of the notification shade. However, the notification action icons are still required and continue to be used on older versions of Android and on devices such as Android Wear.

そのため、下記のスクリーンショットのようにぱっと見ただけではどちらが期待するアクションなのか分かりにくいような見た目になってしまいます。

f:id:katsuhiro-ito:20210615184356p:plain

コードはこちらです。

val notification = NotificationCompat.Builder(this, "notification_channel")
    .setSmallIcon(R.drawable.ic_launcher_foreground)
    .setContentText("着信中")
    .addAction(
        NotificationCompat.Action.Builder(
            null,
            "応答する",
            createDeclineIntent(),
        ).build()
    )
    .addAction(
        NotificationCompat.Action.Builder(
            null,
            "拒否する",
            createAnswerIntent(),
        ).build()
    )
    .build()

private fun createDeclineIntent(): PendingIntent {
    TODO("Not yet implemented")
}
private fun createAnswerIntent(): PendingIntent {
    TODO("Not yet implemented")
}

これからの通知アクション

Android 12では新たに通話に関する通知のスタイルが追加されました。

Features and APIs Overview  |  Android 12 Beta  |  Android Developers

こちらを利用すると下記のような見た目になります。

f:id:katsuhiro-ito:20210616102007p:plain

コードはこちらです。 以下では実装のポイントについて説明していきます。

// ①
val person = Person.Builder()
    .setName("太郎")
    .build()

val notification = Notification.Builder(this, "notification_channel")
    .setSmallIcon(R.drawable.ic_launcher_foreground)
    .setStyle(
        // ②
        Notification.CallStyle.forIncomingCall(
            person,
            createDeclineIntent(),
            createAnswerIntent()
        )
    )
    .build()

① 通話相手の設定

Personクラスを用いて、通話相手の情報を設定します。

名前の設定は必須で、オプションとしてアイコンや連絡帳のURIを設定することも可能です。

Person.Builder  |  Android Developers

② 通話用の通知スタイルの設定

通話用の通知スタイルであるNotification.CallStyleはファクトリメソッドから生成します。

通話中の通知スタイルなども使えますが、今回は着信通知を実装するのでforIncomingCallを使います。

①で用意したPersonクラスを最初の引数に渡し、応答時と拒否時に呼び出すPendingIntentも引数に渡します。

実装としてはこれだけで、あとは自動でアクションボタンを用意してくれます。

Notification.CallStyle  |  Android Developers

カスタマイズについて

現状では通話用の通知スタイルにより追加されるアクションボタンはカスタマイズができません。

そのため、文言やアイコンをアプリのブランドに合わせて変更するということができなくなっています。

まだベータ版ですし、今後そういった機能が追加されていくのかもしれません。

まとめ

AndroidはOSのバージョンアップごとにカスタマイズの制約が厳しくなってきている傾向があり、通知も例外ではありません。

その中で公式が表現を広げるための機能を追加してくれることはありがたいことです。

通知というのはAndroidの中でもシステムよりの機能なので、ある程度の統一感があった方がユーザも混乱せずに安心して使うことができます。

統一された表現の中でも、アプリごとの特色を可能な限りで出していけたらいいですね。

MySQL5.6におけるdatetimeの挙動を実際に試してみた

はじめに

MySQL5.6におけるdatetimeの挙動で???ってなったので、仕様を確認しつつ、実際に挙動を確かめてみました。

datetimeの仕様

デフォルト値に関しては、ドキュメントにこのように記載されています。

DATETIME は、NOT NULL 属性で定義されていないかぎり (この場合、デフォルトは 0 です)、デフォルトは NULL です。

SELECTに関しては、ドキュメントにこのように記載されています。

NOT NULL として宣言された DATE および DATETIME カラムでは、次のようなステートメントを使用することで、特殊な日付 '0000-00-00' を検索できます。
SELECT * FROM tbl_name WHERE date_column IS NULL

datetimeの挙動

先程の仕様を実際にテーブルを作って、データを入れて確かめてみます。

datetimeがNOT NULLでデフォルト値なしの場合

CREATE TABLE `datetime_not_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_not_null` (`id`)
VALUES(1);
// created_atには0000-00-00 00:00:00が入ります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(2, NULL);
// エラーになります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

SELECT * FROM datetime_not_null WHERE created_at IS NULL
// idが1, 3, 4のデータを取得できます

datetimeがNOT NULLでデフォルト値ありの場合

CREATE TABLE `datetime_not_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_not_null` (`id`)
VALUES(1);
// created_atにはデフォルト値の現在時刻が入ります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(2, NULL);
// エラーになります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

datetimeがNULL許容で、デフォルト値なしの場合

CREATE TABLE `datetime_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_null` (`id`)
VALUES(1);
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(2, NULL);
// created_atにはNULLが入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

datetimeがNULL許容で、デフォルト値ありの場合

CREATE TABLE `datetime_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_null` (`id`)
VALUES(1);
// created_atにはデフォルト値の現在時刻が入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(2, NULL);
// created_atにはNULLが入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

おわりに

MySQLのdatetimeに 0000-00-00 を入れると、思わぬ動作をする可能性があるので、注意が必要です。
また、この機能はSQL標準ではなく、MySQL独自のもので、DB移行時に移行先のDBが 0000-00-00 に対応していなかったり、ORMがデフォルトで対応していなかったりして面倒なのでMySQLがdatetimeでサポートしている値(1000-01-01 00:00:00 ~ 9999-12-31 23:59:59)をデフォルト値として入れる、モードの設定で 0000-00-00 を入れらないようにするなどしたほうがいいと思います。

MyBatisによるコード自動生成で、Javaの予約語を回避する方法

こんにちは。 エキサイト株式会社の三浦です。

JavaにはMyBatisというライブラリがあり、それを使うことでDBとの接続用コードを自動で生成することができます。 DBとのやり取りをするアプリケーションにとっては非常に便利な機能であり、使っている方も多いと思うのですが、この「自動」というところに落とし穴があります。

例えば、Javaでは予約語となっている package というカラム名が存在する時、どうすればよいのでしょうか?

今回は、テーブル内でJava予約語が使われている場合に、いかにそれを回避してMyBatisによる自動コード生成をうまく機能させるかを説明します。

MyBatisとは

MyBatisは、いわゆるORマッパーの一つで、これを使用することで簡単にアプリケーションをDBと接続できるようになります。 あらかじめ使用するテーブルを一通り揃えておき、かつMyBatisの設定ファイル( generatorConfig.xml )を用意することで、そのテーブル構成をもとに接続用のコードを自動生成してくれるので、エンジニアとしては生成されたコードを使って簡単にテーブル操作ができるようになるという代物です。

多くの場合は生成するだけですぐ使えるようになるのですが、テーブルのカラムにJava予約語package など)が使用されていると問題が発生します。

カラム名等に予約語が入っていた場合

あるテーブルに、 package というカラムが存在しているとしましょう。 その場合でも、コードの自動生成自体はエラーが発生することはなく、問題なく生成されます。

問題が起きるのは実際にコードを実行したときです。 packageJava予約語であり、あらかじめ指定された使い方しかできないのですが、自動生成コードでは package をプロパティ名等として使用するようになっているため、そこでエラーが起きてしまうのです。

では、どのようにこの問題を回避すればよいのでしょうか?

解決方法

MyBatisの設定ファイル( generatorConfig.xml )にはいろいろな設定項目があるのですが、その一つに columnRenamingRule というものがあります。 これは名前の通り、カラム名をリネームしたい時に、そのリネームするルールを定めるというものです。

例えば、 package というカラム名をコード内では packageCode として使用したい場合は、以下のように記述することで実現することができます。

...
<generatorConfiguration>
    <context ...>
            <table....>
                <columnRenamingRule searchString="^package$" replaceString="package_code" />
            </table>
    </context>
</generatorConfiguration>

searchString に変換したい文字列を、 replaceString にどのように変換したいかの文字列を入れることで、コード生成時に自動的に変換後の文字列で生成してくれます。

注意点として、

  1. searchString には正規表現を入れる必要があるので、例えばもし packageNamepackageCodeName にしたくないのであれば、始端・終端を定める必要がある
  2. replaceString について、スネークケースの文字列をコード生成時に自動でキャメルケースにするので、 package_codeにしないとコード上では packageCode になってくれない

があります。

終わりに

最初からJavaをアプリケーションとして使うことが前提であればカラム名等にJava予約語を使うことはあまりないと思いますが、もともと別の言語のアプリケーションで使っていたDBをJavaで使うようになったときは、このような状況は起こり得ると思います。 幸いにもMyBatisはその状況を考えてくれているので、ぜひ使っていきましょう。

statusBarの表示非表示ハンドリング

エキサイト株式会社の高野です。

今回はFlutterを書いていく中でiOSでstatusBarが表示されない(消えてる)問題の解決に当たっていたのでその辺りについて書いていこうと思います。

環境

iOS: 14.5 Flutter: 2.0.6 Android Studio: 4.1.3

解決方法

先にXcodeの方でProjectの設定を確認しておきます。 XcodeでProjectをクリック→TARGETSをクリック→Deployment Info こちらを確認して Status Bar StyleHide status barチェックボックスが外れていることを確認します。 外れていれば次にコードを紹介します。

import 'package:flutter/services.dart';

こちらをインポートし、以下のメソッドを使用していきます。

SystemChrome.setEnabledSystemUIOverlays([]);

こちらを使用することにより、statusBarを非表示にすることができます。 ですが、このまま使うとAndroidの方にも影響が出てしまうので[]のなかに SystemUiOverlay.bottom を追加します。

SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);

表示にする場合は、さらに SystemUiOverlay.top を追加してあげることにより表示することができます。

SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]);

自分の好きなタイミングで発火しましょう。

まとめ

これで柔軟にstatusBarの表示非表示をハンドリングできます。 他にもやる方法があるよという部分や疑問に思ったところがあればコメントしていただけると幸いです。

effective java の 防御的コピーの話を自分なりに実装してみた

エキサイト株式会社の中尾です。

effective java を最近読んでいて、気になった部分を自分でコーディングして理解を深めようと思っています。 その一部を紹介します。

public class Main {

    public static void main(String[] args) {

        System.out.println("元のクラス");
        Counter counter = new Counter(100);

        System.out.println("変更可能なクラス");
        MutableCounter mutableCounter = new MutableCounter(counter);
        System.out.println("初期値 : " + mutableCounter.getCounter().get());

        counter.set(10);
        System.out.println("10で更新される : " + mutableCounter.getCounter().get());
        System.out.println("理由はcounterとmutableCounterが等しいため");
        System.out.println(counter.hashCode() + " = " + mutableCounter.getCounter().hashCode());
        System.out.println("trueになる : " + counter.equals(mutableCounter.getCounter()));

        System.out.println();
        System.out.println("不完全な不変クラス");
        counter = new Counter(100);
        BlockCounter blockCounter = new BlockCounter(counter);

        counter.set(10);
        System.out.println("10で更新されない : " + blockCounter.getCounter().get());
        System.out.println("理由はcounterとblockCounterが等しくないため");
        System.out.println(counter.hashCode() + " = " + blockCounter.getCounter().hashCode());
        System.out.println("falseになる : " + counter.equals(blockCounter.getCounter()));
        blockCounter.getCounter().set(20);
        System.out.println("blockCounter.getCounter() から counterを参照できるため、更新することができる : " + blockCounter.getCounter().get());
        System.out.println("当たり前だけど一致する");
        System.out.println(blockCounter.getCounter().hashCode() + " = " + blockCounter.getCounter().hashCode());
        System.out.println("trueになる : " + blockCounter.getCounter().equals(blockCounter.getCounter()));
        System.out.println();

        System.out.println("完全な不変クラス");
        counter = new Counter(100);
        ImmutableCounter immutableCounter = new ImmutableCounter(counter);

        System.out.println(immutableCounter.getCounter().get());
        counter.set(10);
        System.out.println("10で更新できない : " + immutableCounter.getCounter().get());
        immutableCounter.getCounter().set(20);
        System.out.println("20で更新できない : " + immutableCounter.getCounter().get());

        System.out.println("immutableCounter.get()するたびに新しいオブジェクトが作成されるため、ハッシュコードが毎回変わる");
        System.out.println(immutableCounter.getCounter().hashCode()
                + " != " + immutableCounter.getCounter().hashCode()
                + " != " + immutableCounter.getCounter().hashCode());
    }
}

setterとgetterのみを持つクラスです。 数値を設定できます。

public class Counter {
    private int count;

    public Counter(int count){
        this.count = count;
    }

    public int get(){
        return this.count;
    }

    public void set(int i){
        this.count = i;
    }
}

上記のカウンタークラスを使うMutableCounterクラスです。 変更可能なクラスです。

public class MutableCounter {

    private final Counter counter;

    public MutableCounter(Counter sampleCounter){
        this.counter = sampleCounter;
    }

    public Counter getCounter(){
        return this.counter;
    }
}

上記のカウンタークラスを使うBlockCounterクラスです。 少しだけ防御力があがっています。

public class BlockCounter {
    private final Counter counter;

    public BlockCounter(Counter sampleCounter){
        this.counter = new Counter(sampleCounter.get());
    }

    public Counter getCounter(){
        return this.counter;
    }
}

上記のカウンタークラスを使う ImmutableCounterクラスです。 完全に防御されているクラスです。

public class ImmutableCounter {
    private final Counter counter;

    public ImmutableCounter(Counter sampleCounter){
        this.counter = new Counter(sampleCounter.get());
    }

    public Counter getCounter(){
        return new Counter(this.counter.get());
    }
}

実行結果です。

元のクラス
変更可能なクラス
初期値 : 100
10で更新される : 10
理由はcounterとmutableCounterが等しいため
951007336 = 951007336
trueになる : true

不完全な不変クラス
10で更新されない : 100
理由はcounterとblockCounterが等しくないため
834600351 = 471910020
falseになる : false
blockCounter.getCounter() から counterを参照できるため、更新することができる : 20
当たり前だけど一致する
471910020 = 471910020
trueになる : true

完全な不変クラス
100
10で更新できない : 100
20で更新できない : 100
immutableCounter.get()するたびに新しいオブジェクトが作成されるため、ハッシュコードが毎回変わる
1418481495 != 303563356 != 135721597

effective javaに書いている通り、不変クラスが作ることができました。 不変クラスを作れるとかっこいいですね!!! かっこいいけど、柔軟性も欲しい気がするのが私の気持ちです。