「ThreeTenABP」で捨てれないのか android.text.format.DateUtils

Androidで日付表記をお手軽に国際化する « LINE Engineers' Blog

読んでみると, なんかいろいろややこしそう.

イギリス式: Fri, 18 Nov 2016
アメリカ式: Fri, Nov 18, 2016

イギリス英語: Fri, 18 Nov 2016
アメリカ英語: Fri, Nov 18, 2016
日本語: 2016年11月18日(金)

イギリス英語: 18/11/2016
アメリカ英語: 11/18/2016
日本語: 2016/11/18

Android 6.0: 2016年11月18日(金)
Android 4.1: 2016/11/18 (金)

Android 6.0: 2016年11月18日金曜日
Android 4.1: 2016年11月18日 (金)

日本語: 3 日前
英語: 3 days ago
フランス語: Il y a 3 jours

日本語: 11月18日
英語: on Nov 18
フランス語: le 18 nov.

挙動にぶれがあります(エミュレータにて動作確認)。小さいTextViewに表示する際にはOS versionによって見切れてしまっていないかなどの検証をしましょう。

OS version間での挙動差分や、ビジネス上アプリで必要な言語をAPIが対応してくれるかの検証は必要

Android 7.0でのICU4Jの導入と共に下記クラスがICU4JでのDateUtilsに対応するクラスとして案内されています。

うーむ...「日付表示」て, 書いときゃ世界のどこでもまあまあいけるやつはねえのかなあ, と.

ThreeTenABP で DateTimeFormatter を使う

「ThreeTenABP」て何なのかは, 作者であるぼくらの Jake Wharton神 の書いていることを読めば分かります.

JakeWharton/ThreeTenABP: An adaptation of the JSR-310 backport for Android.

なぜ JSR-310 なのか?

JSR-310 は, java.time.* パッケージとして Java8 に含まれています. これは, Java と Android の両方で Date/Calendar API群をすべて置き換えるものです.
Java6 の開発者であるStepahen Colebourne によって調整され, それにバックポートされました.

なぜ ThreeTenBP を使わないのか?

Android 上で Joda-Time を使う問題のように ThreeTenBP はタイムゾーン情報の読込みにjar リソースを利用しています. これが Android 上では非常に非効率です.
このライブラリでは, タイムゾーン情報を Android標準で使えるようして, 効率的にパースできるローダーを提供しています.

なぜ Joda-Time を使わないのか?

Joda-Time は非常に大きいバイナリサイズと多くのメソッド数をもつ非常に大きいAPIです. また, JSR-310 と Joda-Time の作者は「壊れてはいないが, 設計に欠陥がある」と言っています.
すでに Joda-Time を使っているなら, サイズとメソッド数を除けば, 置き換える理由はほとんどありません. 新しいプロジェクトのために, このライブラリはバイナリサイズとメソッド数だけでなくAPIの数をかなり削減したJava8の標準APIの形 で提供しています.

Joda Time's Memory Issue in Android

Date/Calendar API + JodaTime の機能をJava8 で合成した java.time.* (JSR-310) を 現Android開発向けに適正化し利用できるようにしたものが「TheeTenABP」ということになります.

コードの記述は Java8 の記述と同じなので Java8 Date and Time APIドキュメントや記事が参考にできます.

続・今日から始めるJava8 - JSR-310 Date and Time API - Taste of Tech Topics

13 章 : Date and Time API · Java Study

Java 8 の DateTimeFormatter の曜日等のフォーマットについて - tokuhirom's blog

Javaで日時を扱う(Java8) - Qiita

同梱されている国際化可能そうな日時フォーマットを試してみる.

...
DateTimeFormatter formatter =
    DateTimeFormatter
        .ofLocalizedDateTime(formatStyle, formatStyle)
        .withLocale(Locale.getDefault());
Timber.d(zonedDateTime.format(formatter) + " : " + formatStyle.name());
...

日付と時刻, それぞれ FormatStyle.SHORTからFULLまでの4種類ある.


2016年12月14日水曜日 10時13分36秒 日本標準時 : FULL
2016年12月14日 10:13:36 JST : LONG
2016/12/14 10:13:36 : MEDIUM
2016/12/14 10:13 : SHORT

Wednesday, 14 December 2016 01:13:36 Greenwich Mean Time : FULL
14 December 2016 01:13:36 GMT+00:00 : LONG
14 Dec 2016 01:13:36 : MEDIUM
14/12/2016 01:13 : SHORT

Tuesday, December 13, 2016 8:13:36 PM Eastern Standard Time : FULL
December 13, 2016 8:13:36 PM EST : LONG
Dec 13, 2016 20:13:36 : MEDIUM
12/13/16 20:13 : SHORT

2016年12月14日星期三 香港標準時間上午9時13分36秒 : FULL
2016年12月14日 GMT+08:00上午9時13分36秒 : LONG
2016年12月14日 09:13:36 : MEDIUM
2016/12/14 09:13 : SHORT

2016년 12월 14일 수요일 오전 10시 13분 36초 대한민국 표준시 : FULL
2016년 12월 14일 오전 10시 13분 36초 GMT+09:00 : LONG
2016. 12. 14. 10시 13분 36초 : MEDIUM
16. 12. 14. 10:13 : SHORT

なんか微妙...

FormatStyle.MEDIUM くらいが良さげだが, 曜日が含まれてないし.

別個, 曜日の文字列としては別に


localDateTime.getDayOfWeek()
    .getDisplayName(TextStyle.FULL, Locale.getDefault());

のようにして文字列として取れるっちゃあとれるが並び順やセパレータの件がある.

カスタムで言語別にフォーマットつくるのもなあ.

まとめ

android.text.format.DateUtils で.


DateUtils.formatDateTime(
       context,
       timestamp, 
       FORMAT_SHOW_YEAR | FORMAT_SHOW_DATE | FORMAT_SHOW_WEEKDAY | FORMAT_SHOW_TIME | FORMAT_ABBREV_ALL);


2016年12月14日(水) 10:13
Wed, 14 Dec 2016 00:13
Tue, Dec 13, 2016, 20:13
2016年12月14日週三 09:13
2016년 12월 14일 (수) 10:13

結局, これが簡単で自然,

表示言語やタイムゾーンの設定は context から取得してくれるし.

モバイル画面上の表示としてその他海外で自然かどうかは実際知らんけども.

日時を多言語化するというのは思ったより複雑.

モバイル端末というのは, いろいろな制限の中で動いているのだなあと実感.

Date/Calendar のことはもう忘れていいのだよな?

(つづく)


AGPBI 関連の多量のエラーを排除する

意味不明な Gradle の Warning や Error を消して激ビルド時間短縮

解決法がなく, 待っていたがビルド時間が長すぎて非効率すぎる.

Issue リストに新しい書き込み.

A temporary workaround I managed was to rollback to 2.1.3 of gradle in my project level build.gradle. No more errors and app builds fine. Going to keep it like this until a fix is pushed out. You will be prompted to upgrade to v2.2.0 when you open AS for the project, but you can ignore it.

classpath 'com.android.tools.build:gradle:2.1.3'

Issue 215748 - android - AGPBI reports errors instead of warnings - Android Open Source Project - Issue Tracker - Google Project Hosting

なので 2.2.0 から 2.1.3 に下げる.


  dependencies {
      //classpath 'com.android.tools.build:gradle:2.2.0'
      classpath 'com.android.tools.build:gradle:2.1.3'

com.android.tools.build.gradle

再度, clean build のあと, re-start や rebuild しても以下でできず.


Error:Cause: org.gradle.api.internal.tasks.DefaultTaskInputs$TaskInputUnionFileCollection cannot be cast to org.gradle.api.internal.file.collections.DefaultConfigurableFileCollection
Possible causes for this unexpected error include:<ul><li>Gradle's dependency cache may be corrupt (this sometimes occurs after a network connection timeout.)
<a href="syncProject">Re-download dependencies and sync project (requires network)</a></li><li>The state of a Gradle build process (daemon) may be corrupt. Stopping all Gradle daemons may solve this problem.
<a href="stopGradleDaemons">Stop Gradle build processes (requires restart)</a></li><li>Your project may be using a third-party plugin which is not compatible with the other plugins in the project or the version of Gradle requested by the project.</li></ul>In the case of corrupt Gradle processes, you can also try closing the IDE and then killing all Java processes.

gradle-wrapper.properties で gradle のバージョンを下げる.


#distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

これでいけます. ビルド時間は, 5分の1程度に短縮された感じ.

意味がなかった proguard 記述は 無効化しておく.

proguard-rules_pro

まとめ

AGPBI 関連のエラーを消すには com.android.tools.build:gradle を 2.1.3 にダウングレードする(build.gradle root の書き換え).

com.android.tools.build:gradle:2.1.3 に対応している最新バージョンの gradle は 2.14.1(gradle-wrapper.properties の書き換え).

Gradle Distributions

2016-10-14 追記

修正された模様.

Yesterday (28 hours ago) #8 [email protected]
it seems to be fixed in Android Studio 2.2.1 with the gradle wrapper 2.2.1

Yesterday (25 hours ago) #9 [email protected]
A.S. 2.2.1 fixed it for mee too

Issue 222989 - android - Android Studio 2.2 : Build successful with 1944 errors - Android Open Source Project - Issue Tracker - Google Project Hosting

実際, 試してみたが確かに修正されていた.


  dependencies {
      classpath 'com.android.tools.build:gradle:2.2.1'

gradle 3.1 でも問題はない感じ.


「これより速いのがあったら言ってこい」Google非公式DIフレームワーク Tiger 登場!!

Google製のライブラリが集まっていいるGitHubリポジトリがありますが.

Google

Google

DIフレームワークが新しく登場しています.

google_tiger

google/tiger

DISCLAIMER: This is not an official Google product. Google's production dependency injection frameworks are Dagger and Guice.

Dagger や Guice があるので, このフレームワークは「非公式」ということですが, 速さの面で気持ちいい啖呵を切っているようにも見えます.

Challenge

If one found a DI framework faster, let me know. I will drop the title as long as it is proved.

「これより速いの見つけたら教えてね♥」

Group

[email protected]

と, メールアドレスもこだわっています.

サンプルを見た感じアノテーションを利用した Dagger とよく似た雰囲気になっておりますが, それぞれのスコープに対してひとつのインジェクターを生成するしくみのようです.


public class PseudoActivity {
  @Inject
  Foo foo;

  @Inject
  @BarAnnotation
  Bar bar;

  @Inject
  @BarAnnotation
  Bar bar2;

  @Inject
  Baz baz;
  
  @Inject
  Shboom shboom;

  void onCreate(DaggerApplicationComponent applicationComponent) {
    DaggerActivityComponent activityComponent =
        new DaggerActivityComponent.Builder().daggerApplicationComponent(applicationComponent).build();
    activityComponent.injectPseudoActivity(this);
  }

  @Override
  public String toString() {
    return "PseudoActivity[foo: " + foo + ", bar: " + bar + " bar2: " + bar2 + ", baz: " + baz
        + " shboom: " + shboom + "]";
  }
}

こんなかんじで最近はGoogleから実験的なライブラリが公開されるようになってきていますが, どれも微妙な雰囲気はあります.

Include the "not an official Google product" disclaimer by gk5885 · Pull Request #3 · google/tiger

これ, 今後どうなっていくのでしょうかー.

Google発リアクティブなライブラリ「Agera」の評判を眺める


DiffUtil で簡単に RecyclerView を更新する

list_mail

例えば, RecyclerView で一覧を作成して表示しているとして


1. 田中一郎
2. 佐藤二郎
3. 鈴木三郎
....

サーバなどに問い合わせて取得された以下データ


1. 田中一郎
2. ジローラモ
3. 鈴木三郎
...

を一覧画面に反映させたい場合.

RecyclerView.Adapter に更新を通知する場合,

RecyclerView.Adapter | Android Developers

notify*() のうちアイテムの更新に関するものは


notifyDataSetChanged()
notifyItemChanged(int position, Object payload)
notifyItemChanged(int position)
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
notifyItemRangeChanged(int positionStart, int itemCount)

ですが, それぞれ以下の面倒な部分がありました.


notifyDataSetChanged()

→ 変更部分のみに関してのアニメーションが表示されない.


notifyItemChanged(int position, Object payload)
notifyItemChanged(int position)
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
notifyItemRangeChanged(int positionStart, int itemCount)

→ アニメーションは表示されるが, 変更部分の position を計算して渡す必要がある.

その他 hasStableIds() と Loader を使ったりなど面倒でしたよね.

DiffUtil を使う

support-library-v7 24.2.0 で登場の DiffUtil を使いましょう.

DiffUtil | Android Developers

コールバッククラス作成後, それを使った更新メソッドを Adapter 内に記述します.

MyDiffUtilCallback.java


public class MyDiffUtilCallback extends DiffUtil.Callback{

    List<Person> oldPersons;
    List<Person> newPersons;

    public MyDiffUtilCallback(List<Person> oldPersons, List<Person> newPersons) {
        this.oldPersons = oldPersons;
        this.newPersons = newPersons;
    }

    @Override
    public int getOldListSize() {
        return oldPersons.size();
    }

    @Override
    public int getNewListSize() {
        return newPersons.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).id == newPersons.get(newItemPosition).id;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).equals(newPersons.get(newItemPosition));
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
	// some additional information
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

MyRecyclerViewAdapter.java


public void updateList(ArrayList<Person> newList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallback(this.persons, newList));
        diffResult.dispatchUpdatesTo(this);
}

これで, アイテムのポジションの計算はおまかせにして, 同時に綺麗なアニメーションが変更部分に適用されます.

実際, データ量が多い場合は厳しいようなので, RxJava なら メインスレッドにて以下.


@Override
public void onNext(DiffUtil.DiffResult result) {
    result.dispatchUpdatesTo(mProductAdapter);
}

kotlinなら同様に以下。


internal class DiffUtilCallback(
    private val oldItems: List<Item>,
    private val newItems: List<Item>
) : DiffUtil.Callback() {

  override fun getOldListSize() = oldItems.size

  override fun getNewListSize() = newItems.size

  override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
      = oldItems[oldItemPosition].id == newItems[newItemPosition].id

  override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
      = oldItems[oldItemPosition] == newItems[newItemPosition]
}

以下のように使うと便利かもしれません.

リアクティブにデータ更新検知して自動画面更新させる


リアクティブにデータ更新検知して自動画面更新させる

Square 生まれのライブラリを使います.

SQLBrite

square/sqlbrite: A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations.

SQLiteOpenHelper をラップするように使い, ContentProvider(ContentResolver) のように更新を通知する役割を担当します.

SQLBrite__A_Reactive_Database_Foundation

SQLBrite: A Reactive Database Foundation

RxJava と連携させて使いましょう.


SqlBrite sqlBrite = SqlBrite.create();
BriteDatabase db = sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io());

// 対象テーブル名を含むSQLクエリーを登録
Observable<Query> users = db.createQuery("users", "SELECT * FROM users");
final AtomicInteger queries = new AtomicInteger();

// データ更新を監視して受け取る
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});


System.out.println("Queries: " + queries.get()); // Prints 1

db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
db.insert("users", createUser("strong", "Alec Strong"));

System.out.println("Queries: " + queries.get()); // Prints 4

このしくみを利用すると,

CursorLoader + ContentProvider

の部分を

RxJava + SQLBrite

で置き換えてリアクティブに記述できます.

MVP実装の例

googlesamples/android-architecture at dev-todo-mvp-rxjava

Observable を DataSource で作成して,

TasksLocalDataSource.java:77


    @Override
    public Observable<List<Task>> getTasks() {
        String[] projection = {
                TaskEntry.COLUMN_NAME_ENTRY_ID,
                TaskEntry.COLUMN_NAME_TITLE,
                TaskEntry.COLUMN_NAME_DESCRIPTION,
                TaskEntry.COLUMN_NAME_COMPLETED
        };
        String sql = String.format("SELECT %s FROM %s", TextUtils.join(",", projection), TaskEntry.TABLE_NAME);
        return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)
                .mapToList(mTaskMapperFunction);
    }

Repository 経由させて,

TaskRepository:96


    @Override
    public Observable<List<Task>> getTasks() {
        // ...
        if (mCacheIsDirty) {
            return remoteTasks;
        } else {

            // Query the local storage if available. If not, query the network.
            Observable<List<Task>> localTasks = mTasksLocalDataSource.getTasks();
            return Observable.concat(localTasks, remoteTasks).first();

        }
    }

Presenter 上で subscribe しておけば,

TaskPresenter:104


        Subscription subscription = mTasksRepository
                .getTasks()
                .flatMap(new Func1<List<Task>, Observable<Task>>() {
                    @Override
                    public Observable<Task> call(List<Task> tasks) {
                        return Observable.from(tasks);
                    }
                })
                .filter(new Func1<Task, Boolean>() {
		  // ...
                })
                .toList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<List<Task>>() {
                    @Override
                    public void onCompleted() {
                        mTasksView.setLoadingIndicator(false);
                    }

                    @Override
                    public void onError(Throwable e) {
                        mTasksView.showLoadingTasksError();
                    }

                    @Override
                    public void onNext(List<Task> tasks) {
                        processTasks(tasks);
                    }
                });

データベースで対象テーブルに更新されるとすぐに自動的に再読み込みが実行されて画面が更新されます.

MVP に CursorLoader を利用すると, Presenter で受け取るコールバックが煩雑になりやすいのでこれを使うと便利です.

DiffUtil で簡単に RecyclerView を更新する