20200327のAndroidに関する記事は8件です。

Retrofit データクラスで地味にハマったこと

前提

APIは仮に以下のようにします。

{
  "items": [
     {
        "id": 100,
        "number": 10,
        "content": "sample",
        "created_at": "2020/03/26 21:30",
        "updated_at": "2020/03/26 21:30"
     }
  ],
  "delete_questions": [
    {"id": 10}
  ],
  "status": "ok"
}

今まで毎回同じフォーマットで帰ってくるAPIのレスポンスデータしか扱って来なかったので、データモデルといえば以下のように実装していました。

data class Item {
    val items: List<ItemInfo>,
    @Json(name = "delete_items")val deleteItems: List<DeleteItemInfo>,
    val status: Int
}

APIのフォーマットがレスポンスによって変わる場合

ところが実際にはAPIのレスポンスとしては以下のような場合もあるとします。

{
  "items": [
    {
      "id": 1,
      "number": 1,
      "content": "sample",
      "created_at": "2020/03/26 21:30",
      "updated_at": "2020/03/26 21:30"
    },
    {
      "id": 2,
      "number": 2,
      "content": "sample",
      "created_at": "2020/03/26 21:30",
      "updated_at": "2020/03/26 21:30"
    }
  ],
  "status": "ok"
}

このようにdelete_itemsはあったりなかったりするAPIレスポンスの場合、上記のデータモデルではdeleteItemsがnullになりエラーとなりアプリが落ちてしまいます。

その場合、データモデルを以下のように変更します。

data class Item {
    val items: List<ItemInfo>,
    // deleteItemsをnull許可にする
    @Json(name = "delete_items")val deleteItems: List<DeleteItemInfo>?,
    val status: Int
}

これでレスポンスにdelete_itemsが含まれていても、いなくても、問題ありません。

自分だけかもしれませんが、データクラスの型にnull許可にするというのがあまり意識したことがありませんでした。

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

Android Status Barのカラーの変更

Windowクラスに各種設定することでカラーの変更だけでなく色々と実現可能となっているようです。

Status Barの色の変更

window.statusBarColor = ContextCompat.getColor(this, R.color.Black)

Full Screen表示,解除する

Full Screen表示

window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)

Full Screen解除

window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)

Status Barのアイコンのカラーを変更する

window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CoordinatorLayout.Behaviorを独自実装する上での前提知識

CoordinatorLayoutAppBarLayout を使うSampleはよく見かけますが、 CoordinatorLayout.Behavior を独自実装して制御しているサンプルはあまり見なかったので、すごくざっくりですが動かし方を書いてみようと思います。

CoordinatorLayoutとAttachedBehavior

スクロールできる DependedView と、それに伴って動く ComsumedScrollView が存在するViewがあるものとします。 AppBarLayout の代わりが ComsumedScrollView で、 xmlで layout_behavior を設定していたものが DependedView だと思ってください。この ComsumedScrollViewDependedViewCoordinatorLayout.AttachedBehavior を実装していて、 CoordinatorLayout は実装された behavior をスクロール検知時にいい感じに叩いてくれます。

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ComsumedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <DependedView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

スクロールを奪い取るView

ConsumedScrollView.kt
class ComsumedScrollView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), CoordinatorLayout.AttachedBehavior {

    override fun getBehavior(): CoordinatorLayout.Behavior<*> = Behavior(context)

   class Behavior(context: Context, attrs: AttributeSet? = null) : CoordinatorLayout.Behavior<ComsumedScrollView>(context, attrs) {

        override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: ComsumedScrollView, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
            // 1
            return true
        }

        override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: ComsumedScrollView, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
            // 2
        }
    }
} 

1でtrueを返答することで、そのScrollを奪い取る可能性があることを教えます。trueを返答した場合に、2が呼び出され、ここにスクロール時にカスタマイズしたい動作を記載します。スクロールでViewを円形に動かしたりとか、ある程度進んだ後に画面上部に固定するとか、動きを実装します。もし、このViewがある一定の動きをするまで他のスクロールを止めたいというのであれば、consumedでどれだけスクロールを消費したか?を教えます。

付随して動くView

DependedView.kt
class DependedView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null
) : ViewPager(context, attrs), CoordinatorLayout.AttachedBehavior {

    override fun getBehavior(): CoordinatorLayout.Behavior<*> = Behavior(context)

    class Behavior(context: Context, attrs: AttributeSet? = null) : CoordinatorLayout.Behavior<DependedView>(context, attrs) {

        override fun layoutDependsOn(parent: CoordinatorLayout, child: DependedView, dependency: View): Boolean {
            // 1
            return true
        }

        override fun onDependentViewChanged(parent: CoordinatorLayout, child: DependedView, dependency: View): Boolean {
            // 2
            return true
        }
    }
}

1にはスクロールによって動いたviewが通知されます。通知されたviewに付随してなにかを動かす必要がある場合は、ここでtrueを返答します。1でtrueを返答した場合に2が呼び出されます。ここで、スクロールしたやつの真下についていくとか、一緒に回転するとか独自の動きを実装します。

まとめ

AppBarLayoutの中を見るともっと詳細なことはわかると思いますが、scrollを察知して奪い取って動く側と、奪い取られた動きを元に動くやつの実装をすればいい感じに動かせるっていう大前提がわかっているとちょっとは捗るんじゃないかと思います。具体的なサンプルは時間があれば書きます。。。

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

非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照

この記事は、Javaにおける「非staticネストクラスが、エンクロージングオブジェクトを暗黙的参照として握っちゃうから、気を付けましょう」について述べます。

なぜ、気を付けなければならないのか、と言えば、「メモリーリークを引き起こす可能性を高めてしまうから」です。

そして結論は、「ネストクラスは、static修飾子を付けた方がイイ(さすれば、エンクロージングオブジェクトを暗黙的参照として握ることはないから)」です。エンクロージングオブジェクトは短命で、ネストクラスのインスタンスの方が長命である場合は特にです!

事のきっかけはAndroidアプリ開発時

(この記事は、Androidアプリ開発に特化したものではありません。が、読み進めていけばJavaに関わることになっていきますので、当初は我慢してお読みください)

ある日のことでございます。AndroidアプリをJavaで作っていた時に、Android Studio1が、こんな警告を出してきました。

thisasynctaskclassshouldbestaticorleaksmightoccur.png

This 'AsyncTask' class should be static or leaks might occur

CouponAsyncTaskと名付けたこのクラスは、ネストクラス(インナークラス・内部クラス)として定義しているのですが、「static修飾子を付けなさいよ、さもなければメモリリークを引き起こすかもよ」と吹き出しに書かれています。まあ、怖い。

Android Studioが備えているコードチェック機能「Lint」が教えてくれる詳しい説明を見ますと、

Static Field Leaks
A static field will leak contexts.
Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected.
Similarly, direct field references to activities and fragments from these longer running instances can cause leaks.
ViewModel classes should never point to Views or non-application Contexts.
Issue id: StaticFieldLeak

Google翻訳に頼んでみます。

静的フィールドリーク
静的フィールドはコンテキストをリークします。
非静的内部クラスには、外部クラスへの暗黙的な参照があります。 その外部クラスがたとえばフラグメントまたはアクティビティである場合、この参照は、長時間実行されるハンドラー/ローダー/タスクがガベージコレクションの取得を妨げるアクティビティへの参照を保持することを意味します。
同様に、これらのより長時間実行されているインスタンスからのアクティビティおよびフラグメントへの直接フィールド参照は、リークを引き起こす可能性があります。
ViewModelクラスは、ビューまたは非アプリケーションコンテキストを指すことはありません。
問題ID:StaticFieldLeak

難しい英文を、難しい日本語に翻訳されてもねえ...チンプンカンプンです。

開発者サイトに書いてありました

ちゃんとAndroidアプリ開発者向け公式サイトに「スレッド化されたオブジェクトを使ったコード設計によく見られる欠陥の例」として掲載されていました。あるあるなんでしょうね。
スレッド化によるパフォーマンスの向上[非明示的参照]

この公式サイトの日本語ページでは「非明示的参照」と訳されていますが、原文は「Implicit references」と書かれています。
当記事では「暗黙的参照」と五字熟語で語っていきます。

ネストクラス

いきなり「非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照」について語ろうにも、このお題目に乗っかっている情報量が多いので、前提知識を一つ一つ説明していきます。
急がば回れ、です。ただ、ちょっともどかしいかもしれませんが。

「ネストクラス(nested class)」は、「インナークラス(inner class)」とも呼ばれます。当記事は「ネストクラス」で統一します。

Fooクラスには2つのネストクラスがある
public class Foo {
    class Bar {
        void methodBar() {
            System.out.println("methodBar");
        }
    }

    static class Baz {
        void methodBaz() {
            System.out.println("methodBaz");
        }
    }

    void methodFoo() {
        System.out.println("methodFoo");
    }
}

この場合、

  • BarクラスとBazクラスは、「Fooクラスのネストクラス」と言います。
  • Fooのことは、「Barクラスのエンクロージングクラス」と言います。

「エンクロージングクラス(enclosing class)」は、「アウタークラス(outer class)」とも呼ばれます。当記事は「エンクロージングクラス」で統一します。

BarとBazの違いは、static修飾子が付いているか否かなので、

  • Barクラスは、「Fooクラスの非staticネストクラス」と言います。面倒くさいと思う人は「Fooクラスのネストクラス」で済ませてしまいますが。
  • Bazクラスは、「Fooクラスのstaticネストクラス」と言います。

ネストクラスのインスタンスを生成するには

Fooの2つのネストクラスのインスタンスを生成する
class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Foo.Bar bar = foo.new Bar();

        Foo.Baz baz = new Foo.Baz();
    }
}

非staticネストクラスのインスタンスを生成する場合

Barクラスのインスタンスを生成するには、2行を要しました。どうしても1行で済ませたい場合は、以下のコードとなります。

1行でBarクラスのインスタンスを生成
Foo.Bar bar = new Foo().new Bar();

いずれにせよ、非staticネストクラスのインスタンスを生成する場合は、エンクロージングのインスタンスが必要です。
そして、new演算子の前に.が付くのも特徴的です。

staticネストクラスのインスタンスを生成する場合

事前にエンクロージングのインスタンスなんて要りません。いきなりnewできます。

特徴的なのは、クラス名が「Baz」ではなく、「Foo.Baz」という名前だということです。クラス名の合間に.が含まれているのが特徴的です。

書籍「Effective Java」に当たる

Effective javaは、第1版(2001年刊行)から「非staticのメンバークラスよりもstaticのメンバークラスを選ぶ」べしとのたまっております。それは2018年に刊行された第3版ですら残っている御成敗式目です。

メモリリークは壊滅的になり得ます。

とか 

メモリリークの検出がたいてい難しいです。

とか

余分な参照を保持しメモリと時間を無駄に使います。

など、怖いことが書かれています。なぜなら、

static修飾子を省略すると、個々のインスタンスはエンクロージングオブジェクトへの関係のない参照を持ってしまいます。その参照を保存することは時間とメモリを必要とします。深刻なのは、その参照がなければガベージコレクションの対象になる場合、そのエンクロージングインスタンスが残ってしまう可能性があることです。

と書かれてあります。

エンクロージングのインスタンスはもう不要なのに、天国に召されない

前出のプログラムをちょっと改変してみました。staticネストクラスであるBazはちょっと退かせました。

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Foo.Bar bar = foo.new Bar();

        foo = null;
        System.gc();
//        foo.methodFoo(); // NullPointerException発生
        bar.methodBar();
    }
}

エンクロージングクラスのFooのインスタンスにnullを代入してガベージコレクション(GC)を実行させてから、非staticネストクラスのBarインスタンスにメソッドを呼び出してみました。実行してみますと、以下の通りです。

実行結果
methodBar

へえ、ちゃんと動くんだぁ!と私は瞠目してしまいました。

Javaの仕様としては、私がいくらエンクロージングクラスのFooのインスタンスにnullを代入しても、その非staticネストクラスのBarインスタンスが動く限り、fooと名付けたオブジェクトはGCされない(GC対象と見做されず華麗にスルーされた)のです。なぜなら、 barfooを暗黙的参照として保持しているから です。

ファニーな例えとしてイラストにすると、こんなかんじでしょうか。

joubutudekinai.png

階段を一歩ずつ登っていってる女性が、barで、元気いっぱいです。

一方、成仏したがっているお爺ちゃんがfooなんですが、barが握ってるのでね、なかなか天国へ召されずにいるのですよ。地縛霊になりかねません。

  • 握っている=参照している
  • 天国へ召される=GCによりメモリから解放される
  • 地縛霊=メモリリーク

で読み替えてください。

では、短絡的にネストクラスにstaticを付けるだけで万事解決!といくのか?

FooとかBarとかBazの題材をやめて、以下のプログラムを見てください。

ビフォアー:ネストクラスが非staticだ
class Fizz {
    private Integer x = 12;
    private static Integer y = 34;

    class Buzz {
        void m() {
            x = 56;
            y = 78;
            System.out.println(x + y);
        }
    }
}

class FizzBuzzMain {
    public static void main(String[] args) {
        Fizz.Buzz fb = new Fizz().new Buzz();
        fb.m();
    }
}
ビフォアーの実行結果
134

(わざとらしく)やるなっつってんのに非staticネストクラスですよ。では、安直にこのBuzzと名付けたネストクラスにstatic修飾子を付けてあげましょう。

アフター
class Fizz {
    private Integer x = 12;
    private static Integer y = 34;

    static class Buzz {
        void m() {
            x = 56; // xがstaticフィールドではないのでコンパイル不可
            y = 78;
            System.out.println(x + y); // xがstaticフィールドではないのでコンパイル不可
        }
    }
}

class FizzBuzzMain {
    public static void main(String[] args) {
        Fizz.Buzz fb = new Fizz.Buzz();
        fb.m();
    }
}

途端にコンパイルできなくなりました。xがstaticフィールドではないために、ダメだとIDEが怒り出します。

なぜなのか。それは、 staticネストクラスは、エンクロージングのstaticメンバーにのみアクセス可能 だからです。

また再びのAndroidアプリ開発の話に戻ります

作りたかったAndroidアプリは、このアニメーションGIFをご覧ください。

countup.gif

起動したとたんに、勝手にカウントアップが始まります。このアニメーションGIFは、3まで至ったらまたSTART!を繰り返しているアニメーションですが、本当はこのアプリはSTART!から始まったら「9」まで数え上げたらお終い(「9」を出力したままキープ)なんです。

そしてそのプログラムは以下の通りです。Androidアプリ開発未経験者の方も我慢しておつきあいください。

画面のクラス
public class MainActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        new MyTask().execute();
    }

    class MyTask extends AsyncTask<Void, String, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            textView.setText(values[0]);
        }
    }
}

Androidで「画面のクラス」を作りたければ、AppCompatActivityを継承したクラスをコーディングしはじめます。

TextViewというクラスは、画面中央に配置して「START!」か「0」とか「1」とか表示している、文字列を表示する(ことしかできない)ビュー(部品)です。
このTextViewを非同期処理で「0」を「1」に、そして「2」へと1000ミリ秒きざみで変えさせたい。これがこのアプリの要件です。

非同期処理をしたければ、AsyncTaskのサブクラスを定義して、それにその処理をコーディングしておきます。

MainActivity_MyTask.png

その非同期処理をするMyTaskexecute()メソッドを呼び出すと、MyTaskが処理を始めます。
この画像の赤い囲みがTextViewです。MyTaskがこのTextViewに、"0"とか"1"とか"2"とか文字列を上書きします。これでカウントアップするように見せかけています。

それでは、MyTaskクラスにstaticを付けてみます。

Non-static field.png

IDEが真っ赤に怒り出しました。なぜならそのtextViewと名付けたフィールド、static修飾子を付けてませんからね!

MainActivityのインスタンスはもう不要なのに、天国に召されない

前出の階段を元気に登っている女性と地縛霊のお爺ちゃんのイラストを今一度見てください。

Androidでは、不要になった画面は、GCされます。「不要になった画面」とは、ユーザが戻るボタンを押すなどしてその画面を非表示にした(消しちゃう)時のことをいいます。
一般的にAndroid端末はPCと比較してメモリが少ないです2。なのでユーザが非表示にした画面=Activityクラスのインスタンスはランタイムにより速やかにGCされます。

ユーザが気軽に戻るボタンを押しちゃう、という所作ひとつで、その画面=Activityインスタンスは不要!んじゃGC!と短命なりにし運命になりがちです。
しかして、バックグラウンドで(否、「バックグラウンド」という用語は正鵠を射てなくて、正確には「ワーカースレッドにおいての非同期処理で」と言った方が適切です)MyTaskは、己の仕事(カウントアップ)を達成しきるまで生きることになります。

この、生存期間の比較で【 エンクロージングインスタンス < ネストクラスのインスタンス 】であることを危惧して警告を出してくれたのがAndroid StudioというIDEなのです。

この私が作ったアプリを起動して、カウントアップがMyTaskの仕業により始まっても、ユーザがもういいやと戻るボタンを押すなどして画面を非表示にする(消す、アプリをやめる)としても、 非staticネストクラスのインスタンスであるMyTaskオブジェクトは、MainActivityインスタンスの暗黙的参照を保持している ので、とっくに表示されてないんだからとっととGCされちゃえばいいのに、GCされない状態に陥ります。
ユーザが見えてないところで、MyTaskは脈々とカウントアップのお仕事をしているのです。
イラストの、女性であるMyTaskは1段1段階段を登って。でもその右手には、成仏できないお爺ちゃんMainActivityをしっかり握っちゃってて。ユーザはこのお爺ちゃんが見えてないのに。

解決したくとも、にっちもさっちも

私はここでジレンマに我が身が嘖(さいな)まれるのです。

  • このMyTaskは、このMainActivityでしか使われないクラスだから、ネストクラスにしたい。
  • しかしstaticネストクラスにせよとIDEが警告を出す。
  • ところがTextTview型のフィールドはstaticフィールドにはしたくない(すべきではないので)。
  • となると、このMyTaskとこのMainActivityは、別クラスに分けるか。
  • でもそうなると、TextTviewの参照の受け渡しが面倒だ(今度はこの受け渡しでヘタこくとTextTviewがメモリリークのタネとなりかねん)。
  • TextTviewを持っているのは(持つべきなのは)MainActivityだし、そのTextTviewの文字列を変えるのはMyTaskだ。
  • だったらやっぱり、このMyTaskは、このMainActivityのネストクラスにしたい。
  • java.lang.ref.WeakReference<T>を使うか...。

ずっとずっとこれに悩んでいます。どこかでなにかを妥協しなければいけないのかもしれません。

以上です。なんだかこれで終わりにしていいのか、良心の呵嘖(かしゃく)に悩む私に、福沢諭吉先生の御言葉でこの記事を綴じたいと思ひます。

されども不良の子に窘(くる)しめらるるの苦痛は、地獄の呵嘖よりも苦しくして、然(しか)も生前現在の身を以てこの呵嘖に当たらざるを得ず。

福沢諭吉「教育の事」1878(明治11)年

ちょっとユキチよ、「地獄の呵責よりも苦しくて」はシビアすぎて引くわ。
やめた。和辻先生に縋(すが)ることにします。

かく道元の説く慈悲の前には、「悪は必ずしも呵嘖すべきものでない」。

和辻哲郎「日本精神史研究」1922(大正11)年

以上です。


  1. IntelliJ IDEAを改造して作られたAndroidアプリ開発専用IDE 

  2. 10GBのRAMを搭載しているAndroid”ゲーミング”スマホとか市販されてますが、まあそれは置いておいて。 

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

コマンド(curl)でPushテストする方法(Android)

メモ書き

iOSに続き、Androidでもテストが必要になったのでメモします。
必要な方はご参考ください。

事前作業

  • Serverkey取得

Firebaseコンソールから確認できます。
スクリーンショット 2020-03-27 13.23.49.png

  • token取得

  • FirebaseInstanceId.getInstance().instanceId
        .addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.w(TAG, "getInstanceId failed", task.exception)
                return@OnCompleteListener
            }
    
            // Get new Instance ID token
            val token = task.result?.token
    
            // Log and toast
            val msg = getString(R.string.msg_token_fmt, token)
            Log.d(TAG, msg)
            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        })
    

スクリプト

push_test.sh
#!/bin/bash
token='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxWmdyQhVDolOSTp_XRjmv'
server_key='AAAAjfX2vXY:APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

read -r -d '' payload <<-'EOF'
{
     "title":"ホゲホゲ",
     "body":"hugahuga"
}
EOF

echo $payload

curl \
    --header "Authorization: key=${server_key}" \
    --header Content-Type:"application/json" \
    https://fcm.googleapis.com/fcm/send \
    -d "{
        \"to\" : \"${token}\",
        \"data\": $payload
    }"

実行

下記のスクリプト叩くだけ

/bin/bash push_test.sh

以上。

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

Android 11で令和に標準で対応予定

Androidのバージョンはグングン上がります。
今やそろそろAndroid 11がリリースされつつあります。
そのAndroid 11にて、ついに日本の元号「令和」が標準APIで対応するようです。

結論

結論を真っ先に述べます。

  • Android 11(R)にて、ついに日本の元号「令和」が標準APIで対応するようです。
  • すでにAPIレベル24からAndroid標準APIはICUを取り込んでいます。
  • でもAPIレベル24~29の間は「令和」には対応していません。「平成」どまりです。

[閑話]コードネームとかAPIレベルとか

ところで、お菓子の名前のコードネームですが、「Q」からやんなくなりました。

KitKatとかLollipopとかMarshmallowとかNougatとかOreoとかPieとか。時折、登録商標も混ざりながらも。
Androidっ子たちは、「Q」から綴りが始まるお菓子が何になるのかワクワクしてたのに、Googleが「お菓子の名前はもうやめた」と言い出して。

Android 10は「Q」。そろそろリリースされる11は「R」です。

Androidの「R」で「令和」に対応...まっまさか!その「R」は、「Reiwa」のRか?!なんちゃって。

そして、この10とか11というバージョン番号、そしてコードネームとは別に、開発者にとって重要な番号は「APIレベル」です。
Android 8.0はOreoであり、APIレベルは26です。が、Android 8.1となると、同じくOreoなんですが、APIレベルは27です。ややこしい!

そして「R」に割り当てられるAPIレベルの番号は...?それは当記事の最後の方に記しましたので、お楽しみに。(じらしてスミマセン)

IBM社製のICUというライブラリ

http://site.icu-project.org/home

「International Components for Unicode」の略だそうです。とにかく色々と便利なAPIが収録されています。
オープンソースソフトウェアです。

ICUのJapaneseCalendarクラス

com.ibm.icu.util.JapaneseCalendarクラスは、日本の元号に対応しています。
最初の元号「大化」から現代まで。南北朝時代をどう扱っているか、というマニアックなところまで私は検証していませんが。

本能寺の変
import java.util.Locale;

import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.DateFormatSymbols;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.JapaneseCalendar;

public class Honnoujinohen {
    public static void main(String[] args) {
        JapaneseCalendar jpCalendar = new JapaneseCalendar();
        DateFormatSymbols dfs = new DateFormatSymbols(jpCalendar, Locale.JAPANESE);
        DateFormat dateFormat = new SimpleDateFormat("Gyy年MM月dd日", dfs);
        dateFormat.setCalendar(jpCalendar);

        // 「本能寺の変」1582年6月2日
        int year = 1582;
        int month = 6;
        int day = 2;
        String s = dateFormat.format(new GregorianCalendar(year, month - 1, day).getTime());
        System.out.println(s);
    }
}

landmark_honnouji_fire.png

実行結果
天正10年06月02日

大河ドラマファンや歴女には堪りませんね。麒麟がくる。「天が正しい」と書く元号だったのか。
ああ、応仁の乱。天保の改革。安政の大獄。元号に思いを馳せるだけで脳内で時代を駆け巡れます。

このICUが「令和」に対応したのは、2019年4月17日リリースのバージョン64.2からです。

Android標準API

Android標準APIでは、APIレベル24(Android 7.0 Nougat)からICUを取り込みました。
取り込んだだけあって、パッケージも変わっております。例えば、前出のJapaneseCalendarクラスは、android.icu.util.JapaneseCalendarです。

そして。ついに。Android 11(R)のAPIレベルで、

public static final int REIWA

が追加されます!まさかのint enumパターーーン!!!いやーーーん!!!
しょうがねぇだろ、IBM社製のICUがそもそもint enumパターーーンなんだからーーーん!!!

サンプルアプリ

こんなアプリを作ってみました。

Screenshot_1585201324.png

「建武の新政」は、鎌倉幕府を倒した後醍醐天皇が、1333年7月17日からスタートさせた新政策ですが、当日はまだ元号が「元弘」だというヒッカケ問題ですね。今度の2学期中間試験に出そうだな。

nigaoe_godaigo_tennou.png

Screenshot_1585200766.png

平成から令和へ。

Screenshot_1585200774.png

開発環境は以下の通りです。

Android Studio 3.6.1
Build #AI-192.7142.36.36.6241897, built on February 27, 2020
Runtime version: 1.8.0_212-release-1586-b04 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
GC: ParNew, ConcurrentMarkSweep
Memory: 1237M
Cores: 8
Registry: ide.new.welcome.screen.force=true
Non-Bundled Plugins:

Gradleの設定

Android 11(R)のAPIレベルで作らなければなりません。
Gradle設定ファイルに以下のようにします(一部略)。

build.gradle
android {
    compileSdkVersion 'android-R'
    buildToolsVersion "29.0.2"

    defaultConfig {
        applicationId "com.example.gengou"
        minSdkVersion 24
    }

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
}

この記事執筆時点では、Android 11(R)のAPIレベルに番号が割り当てられていません。まだプレビュー版だからのようです。正式版がリリースされたら、おそらく30になると推測されます。

minSdkVersionは最低でも24にしなければなりません。なぜなら前述にありますように、ICUがAndroid標準APIに組み込まれたのが、APIレベル24からだからです。

Activity

MainActivity.java
package com.example.gengou;

import android.icu.text.DateFormat;
import android.icu.text.DateFormatSymbols;
import android.icu.text.SimpleDateFormat;
import android.icu.util.GregorianCalendar;
import android.icu.util.JapaneseCalendar;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText year = findViewById(R.id.year);
        EditText month = findViewById(R.id.month);
        EditText day = findViewById(R.id.day);
        TextView gengo = findViewById(R.id.gengou);

        findViewById(R.id.button).setOnClickListener(v -> {
            int y = Integer.parseInt(year.getText().toString());
            int m = Integer.parseInt(month.getText().toString());
            int d = Integer.parseInt(day.getText().toString());
            JapaneseCalendar jpCalendar = new JapaneseCalendar();
            DateFormatSymbols dfs = new DateFormatSymbols(jpCalendar, Locale.JAPANESE);
            DateFormat dateFormat = new SimpleDateFormat("Gyy年MM月dd日", dfs);
            dateFormat.setCalendar(jpCalendar);
            String s = dateFormat.format(new GregorianCalendar(y, m - 1, d).getTime());
            gengo.setText(s);
        });
    }
}

Android標準APIではなく、IBMのICUで令和に対応する

せっかくAndroid標準APIで令和に対応するってのに!それを使わずに、IBMのICUで令和に対応するって、なんか本末転倒!
と思われるかもしれませんが、以下の理由でそんなコトもしてみたいのです。

  • Android標準APIで令和に対応するのは、「R」からだ。それはいい。
  • ICUがAndroid標準APIに組み込まれたのは、APIレベル24(Android 7.0「N」)だ。
  • であれば、Gradleの設定でminSdkVersionは最低でも24でないと、令和うんぬん以前に、android.icuパッケージが使えない。
  • だけど、自分の作るアプリは、Androidのバージョン6とか5とか4とかにも対応したい!

ということで、そのやり方です。

Gradleの設定

build.gradle
dependencies {
    implementation 'com.ibm.icu:icu4j:64.2'
}

依存ライブラリとして追記してSyncNowですね。令和対応の64.2以上を指定します。

プログラム

名前衝突を避けるためにもimport文はこうなります。

パッケージにご注意
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.DateFormatSymbols;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.JapaneseCalendar;

import java.util.Locale;

Localeだけは、安定のjava.utilパッケージですね。
これでAndroid 6とか5とか4とかでも、令和対応できます。

[おまけ]当たり前ですが。

「光文」という元号は、ICUにはありませんので、ご安心を。

Screenshot_1585202068.png

以上です。

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

[Android]クリック中や Enable 中などの状態に応じて、見た目が変わるボタンを作成する

はじめに

今回は View の Click や Enable や Disable の状態の変化に応じて、
色 や 図形 や アイコン が変わる Button や ImageButton を作成してみます。

clipboard.png

色を変える Selector を定義する

状態によって色を変えるために 次の手順で Selector を定義します。
今回はClick中は赤、Enable中は黒、Disable中は青にする Selector を作成してみます。

  1. /res/color を右クリックし、New ➔ Color Resource File を選択する
  2. File Name に color_selector と入力して OK を押す。
  3. color_selector.xml を開いて、次の内容を記述する。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#0000ff" android:state_enabled="false"/>
    <item android:color="#ff0000" android:state_pressed="true"/>
    <item android:color="#000000"/>
</selector>

※ 上から順番に判定処理が実行されるます。例えば state_enable=false で state_pressed=true のときは state_enable=false で定義したものが優先されます。

図形を変える Selector を定義する

状態によって図形を変えるために 次の手順で Selector を定義します。
今回はClick中は四角、Enable中は角が丸い四角、Disable中は円の図形を表示する Selector を作成してみます。

  1. /res/drawable を右クリックし、New ➔ Color Resource File を選択する
  2. File Name に background_selector と入力して OK を押す。
  3. background_selector.xml を開いて、次の内容を記述する。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="false">
        <shape android:shape="oval">
            <solid android:color="#aaaaff"/>
            <size android:width="50dp" android:height="50dp"/>
        </shape>
    </item>

    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#ffaaaa"/>
            <size android:width="50dp" android:height="50dp"/>
        </shape>
    </item>

    <item>
        <shape android:shape="oval">
            <solid android:color="#aaaaaa"/>
            <size android:width="50dp" android:height="100dp"/>
        </shape>
    </item>
</selector>

※ 上から順番に判定処理が実行されるます。例えば state_enable=false で state_pressed=true のときは state_enable=false で定義したものが優先されます。

アイコンを変える Selector を定義する

状態によってアイコンを変えるために 次の手順で Selector を定義します。
今回はClick中は黄、Enable中は黒、Disable中は白のアイコンを表示する Selector を作成してみます。

  1. /res/drawable を右クリックし、New ➔ Color Resource File を選択する
  2. File Name に drawable_selector と入力して OK を押す。
  3. drawable_selector.xml を開いて、次の内容を記述する。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="false" android:drawable="@drawable/light_bulb_disabled"/>
    <item android:state_pressed="true" android:drawable="@drawable/light_bulb_pressed"/>
    <item android:drawable="@drawable/light_bulb_default"/>
</selector>

※ 上から順番に判定処理が実行されるます。例えば state_enable=false で state_pressed=true のときは state_enable=false で定義したものが優先されます。

定義した Selector を View に設定する

次のレイアウトを作成して、定義した Selector が本当に動作するか確認してみます。

全体のレイアウトを定義する

次のような LinearLayout の中に OKボタン、電球ボタン、Enable/Disableボタンを配置したシンプルなレイアウトを作成します。

Image from Gyazo

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/ok_button"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="16dp"
        android:text="OK" />


    <ImageButton
        android:id="@+id/ng_image_button"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="16dp"
        android:background="@drawable/background_selector"
        android:src="@drawable/drawable_selector" />

    <Button
        android:id="@+id/color_text_enable_button"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="16dp"
        android:background="@drawable/background_selector"
        android:text="Enable/Disable" />
</LinearLayout>

OKボタンは色と背景が変わるようにする

OKボタンの Background には background_selector、TextColor には color_selector を設定してやります。これでテキストと背景が View の状態にあわせて変わるようになります。

    <Button
        android:id="@+id/ng_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="NG"
        android:textColor="@color/color_selector"
        android:background="@drawable/background_selector"/>

電球ボタンはアイコンと背景が変わるようにする

電球ボタン の Background には background_selector、 src には drawable_selector を設定しています。これでテキストと画像が View の状態にあわて変わるようになります。

    <ImageButton
        android:id="@+id/ok_image_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/drawable_selector"
        android:background="@drawable/background_selector"/>

Disable/Enable ボタンに状態をトグルできるようにする。

また Disable と Enable が切り替わったときにどのように動作が変わるかも見たいので Disable/Enableボタンの Click 処理を追加し、状態をトグルできるようにしてやります。
```Kotlin
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)  
  color_text_enable_button.setOnClickListener {
      ok_button.isEnabled = !ok_button.isEnabled
      ng_image_button.isEnabled = !ng_image_button.isEnabled
  }
}

}
```

おわりに

これでアプリをビルドしてみると、次のように動作します。
Click中 Enable中 Disable中などの状態ごとにリソースが変わっていますね。

clipboard.png

作成したもの

本記事で作成したアプリは次のリポジトリで管理しています。
興味があれば見てみてください。

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

Android USBシリアル通信方法

USBシリアル通信

今回はAndroidを使ったUSBシリアル通信の方法を説明したいと思います。
サンプルとしてu-bloxを使用しています。

FTDIとは

FTDIとは会社の名前でUSBとシリアルインタフェースの変換チップを作っているような会社です。
http://www.ftdichip.com/

Githubにライブラリが公開されていますので、cloneして、プロジェクトに追加してください
https://github.com/mik3y/usb-serial-for-android/tree/master/usbSerialForAndroid

使い方

※前提としてServiceクラスを使って動かしてください。
初めにUsbManagerクラスのインスタンスを作成します。

mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

次にUSBにはベンダーIDとプロダクトIDというものが設定されていますので、ProbeTableクラスに設定します。
参考までにu-bloxの情報は下記に載っています。
https://devicehunt.com/view/type/usb/vendor/1546/

ProbeTable customTable = new ProbeTable();
customTable.addProduct(0x1546, 0x01a7, CdcAcmSerialDriver.class);
mUsbSerialProber = new UsbSerialProber(customTable);

続いて設定したIDを元に全てのドライバーを取得し、Portリストを取得します。

mUsbSerialProber.findAllDrivers(mUsbManager);
for (final UsbSerialDriver driver : drivers) {
   final List<UsbSerialPort> ports = driver.getPorts();
}

上記で取得したポートリストから1つ選んで、接続するポートを選び、各設定を加えます。

UsbDeviceConnection connection = mUsbManager.openDevice(sPort.getDriver().getDevice());
 try {
            sPort.open(connection);
            sPort.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
        } catch (IOException e) {
            Log.e(TAG, "Error setting up device: " + e.getMessage(), e);
            try {
                sPort.close();
            } catch (IOException e2) {
                // Ignore.
            }
            sPort = null;
            return Service.START_REDELIVER_INTENT;
        }

setPrametersではビットレート、データビット、ストップビット、パリティビットが設定可能です。
次にシリアル通信で受け取るためにSerialInputOutputManagerを設定します。

mSerialIoManager = new SerialInputOutputManager(sPort, mListener);
mExecutor.submit(mSerialIoManager);

mListernerはこんな感じです

private final SerialInputOutputManager.Listener mListener =
            new SerialInputOutputManager.Listener() {

                @Override
                public void onRunError(Exception e) {
                    Log.d(TAG, "Runner stopped.");
                }

                @Override
                public void onNewData(final byte[] data) {
                    StringBuilder result = new StringBuilder();
                    for (int i = 0; i < data.length - 2; i++) {
                        if (data[i] > ' ' && data[i] < '~') {
                            result.append(new String(new byte[]{data[i]}));
                        } else {
                            result.append(".");
                        }
                    }

                    String datas = new String(data, StandardCharsets.UTF_8);

                    Log.i(TAG, "reading GPS: " + datas );

                }
            };

上記の"reading GPS:" のところで実際のString型で取得している情報がlog出力されます。

まとめ

今回はu-bloxのUSBで試してみましたが、他のUSBシリアル通信も基本的には
ベンダIDとプロダクトIDを変えるだけでできると思いますので、試してみてください。

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