20201113のAndroidに関する記事は13件です。

使用したクーポンコードを指定したエンドポイントにポストバックする

広告の成果計測等のために、使用されたクーポンコード等の情報を含めて計測システムのエンドポイントへポストバックする必要がある際の成果通知の仕方です。

今回はHttpURLConnectionを利用しました。

security configの設定とmanifestの設定

設定するポストバック先のURLがhttps://〜であればこの設定は不要です。
Android 9(Pie)では暗号化されていない通信はできない、つまりhttp://ではじまるURLへの通信はデフォルトでは不可能です。
※設定するURLがhttps://で始まればこの手順は必要ありません。

security config設定

app/xmlディレクトリに以下のファイルを作成します。

network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">XXXX.net</domain>
    </domain-config>
</network-security-config>

指定したドメインに暗号化されていない通信(http://)を許可(cleartextTrafficPremittedをtrue)しています。

manifestの設定

その後AndroidManifestのapplicationタグの属性部分に

android:networkSecurityConfig="@xml/network_security_config

を追加します。
(例)

AndroidManifest.xml
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:networkSecurityConfig="@xml/network_security_config">

ポストバック

ポストバックを発生させたいイベント等が発生したときに以下のメソッドを実行します。
クーポンコード等、イベント毎に変わる可能性があるユーザ入力値は引数として受け取る設定をしています。

Activity.java
public void postBack(final String cp){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    URL url = new URL("https:/XXXX.net?coupon="+cp);
                    HttpURLConnection con = (HttpURLConnection)url.openConnection();
                    String str = InputStreamToString(con.getInputStream());
                    Log.d("HTTP",str);
                }catch(Exception e){
                    System.out.println(e);
                }
            }
        }).start();
    }

    static String InputStreamToString(InputStream is) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();
        String line;
        while( (line=br.readLine()) != null ){
            sb.append(line);
        }
        br.close();
        return sb.toString();
    }

参考

HttpURLConnection (Java Platform SE 8)

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

湯婆婆Androidアプリを作ってみました

メリークリスマス。この記事は、湯婆婆 Advent Calendar 2020の4日目です。

賛辞と謝辞

まずは@Nemesisさんに賛辞と謝辞を送ります。
Javaで湯婆婆を実装してみるという記事で、このようなムーヴメントを興こせるだなんて、素晴らしいです。
呼んでいる胸のどこか奥でいつも心踊る夢を見たい気分にさせていただきました。

私にこのような記事を投稿させてもらえる機会を与えていただき、ありがとうございます。

先日、Kotlinで湯婆婆を実装という記事を投稿させていただきました。
でも私の中では心のどこかでAndroidアプリにしたい、という想いが残っていて。
ということで作ったみた次第です。しかし今回はKotlinではなく、Javaで1

やりたいこと

まずは、こちらの名シーンをご覧ください。

GIFMAGAZINE:紙に書いた名前がふわーっと浮いてどこかへ行ってしまうGIF画像 2

荻野千尋おぎのちひろが書いた字が、ふわーっと浮いてどこかへ行ってしまうシーンです。これを実現したいです。

完成したアプリ

OLD_sentochihiro.gif

名前を入力してボタンをクリックすると、1字を残してふわーっと浮いてどこかへ行ってしまうアプリです。

なお、このアニメーションGIFを作るにあたって、うっかりおぎ野千尋を、誤って「はぎ」にしてしまっていました。これはオギノさんハギノさんあるあるなのかもしれませんが、実はこれが後々に恐ろしいことにつながっていきます...詳しくは後述をご覧ください(勿体もったいぶってすみません)。

コード

とりあえずまずは、ずらずらずら~っと掲載しておきます。
可能な限りコメントを付けましたので、日本語と照らし合わしながら解読していただければ幸いです。
ポイント解説は後ほど。

画面を実現するActivityクラスです。というか、Javaのプログラムはこれ1個だけのアプリです。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    // もはやこのRandomオブジェクト自体が湯婆婆なんじゃないのかと思ふ
    private Random random = new Random();

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

        // ここに入力した名前の画像(ImageViewそしてBitmap)を埋め込みます
        LinearLayout nameArea = findViewById(R.id.name_area);

        // 「契約する」ボタンクリック
        Button keiyakuBtn = findViewById(R.id.keiyaku_button);
        keiyakuBtn.setOnClickListener(v -> {
            EditText editText = findViewById(R.id.name);
            // EditTextクリックで出てきたソフトウェアキーボードを引っ込めさせる
            InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
            editText.clearFocus();
            // 2度目以降を考慮してこの名前表示領域から全子ビューを削除
            nameArea.removeAllViews();
            // 入力欄(EditText)から入力文字列(String)を取得
            String name = editText.getText().toString();
            // 配列としてバラバラにします("荻野千尋"なら要素数4の配列)
            String[] names = new String[name.length()];
            for (int i = 0; i < name.length(); i++) {
                names[i] = String.valueOf(name.charAt(i));
            }
            // 運命の分かれ道!さあ、どの字だけが残るのか?!
            int num = random.nextInt(names.length);
            String newName = names[num];
            // String配列の要素数と同数のTextView配列オブジェクトを生成
            TextView[] texts = new TextView[names.length];
            // LayoutInflaterは、XMLで定義した<TextView>を、JavaのTextViewオブジェクトにしてくれます
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            for (int i = 0; i < names.length; i++) {
                texts[i] = (TextView) layoutInflater.inflate(R.layout.name_entity, null);
                // 各TextView1個ずつに名前1字を設定
                texts[i].setText(names[i]);

                if (i != num) {
                    // 選択されなかった字以外は、ふわーっと浮いてどこかへ行ってしまうアニメーションをセット
                    huwa_xtutouiteAnimation(texts[i]);
                }
                // 名前エリアに埋め込みます
                nameArea.addView(texts[i]);
            }
            // 「フン~」セリフ出現
            TextView hun = findViewById(R.id.hun);
            hun.setText(String.format(getResources().getString(R.string.hun), name));
            hun.setVisibility(View.VISIBLE);

            // 「いいかい~」セリフ出現
            TextView iikai = findViewById(R.id.iikai);
            iikai.setText(String.format(getResources().getString(R.string.iikai), newName, newName, newName));
            iikai.setVisibility(View.VISIBLE);
        });
    }

    /**
     * ふわーっと浮いてどこかへ行ってしまうアニメーション
     *
     * @param view ふわーっと浮いてどこかへ行かせてしまいたいView
     */
    private void huwa_xtutouiteAnimation(final View view) {
        // 様々な挙動をするアニメーションを複合化するには、ObjectAnimatorが便利のようだ
        ObjectAnimator transY, transX, scaleX, alpha;
        List<Animator> animatorList = new ArrayList<>();

        // アニメーション時間もランダムに5~10秒に演出
        int durationTY = (random.nextInt(6) + 5) * 1000;
        int durationTX = (random.nextInt(6) + 5) * 1000;
        int durationSX = (random.nextInt(6) + 5) * 1000;

        // 上昇"translationY"
        transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y.getName(), 0.0f, -1000.0f);
        transY.setDuration(durationTY);
        animatorList.add(transY);

        // 左から右へ移動"translationX"
        transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X.getName(), view.getX(), view.getX() - 100.0f, view.getX() + 100.0f);
        transX.setDuration(durationTX);
        animatorList.add(transX);

        // 幅の伸び縮み"scaleX"
        scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X.getName(), 1.0f, 0.1f, 1.0f, 0.1f, 1.0f, 0.1f, 1.0f);
        scaleX.setDuration(durationSX);
        animatorList.add(scaleX);

        // 透明化"alpha"
        alpha = ObjectAnimator.ofFloat(view, View.ALPHA.getName(), 1.0f, 0.0f);
        alpha.setDuration(Math.max(Math.max(durationTY, durationTX), durationSX) / 2);
        animatorList.add(alpha);

        // アニメーションの複合化、再生
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animatorList);
        set.start();
    }
}

このMainActivityのためのレイアウトリソースファイルです。

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

    <!-- 契約書だよ。そこに名前を書きな。 -->
    <TextView
        android:id="@+id/keiyakushodayo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/keiyakushodayo"
        android:textColor="@android:color/black"
        android:textSize="30sp" />

    <!-- 名前入力欄 -->
    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keiyakushodayo"
        android:fontFamily="@font/sanarifont"
        android:textSize="40sp" />

    <!-- 書き終わったらクリックしてください -->
    <Button
        android:id="@+id/keiyaku_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/name"
        android:fontFamily="@font/sanarifont"
        android:text="@string/keiyakusuru"
        android:textSize="30sp" />

    <!-- 「フン~」セリフ -->
    <TextView
        android:id="@+id/hun"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keiyaku_button"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/hun"
        android:textColor="@android:color/black"
        android:textSize="30sp"
        android:visibility="invisible" />

    <!-- ここに入力した名前のTextViewを埋め込みます -->
    <LinearLayout
        android:id="@+id/name_area"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_centerVertical="true"
        android:gravity="bottom|center_horizontal"
        android:orientation="horizontal" />

    <!-- 「いいかい~」セリフ -->
    <TextView
        android:id="@+id/iikai"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/name_area"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/iikai"
        android:textColor="@android:color/black"
        android:textSize="30sp"
        android:visibility="invisible" />

</RelativeLayout>

変数名やid名が英語やローマ字が混在していますが、そこらへんの命名規約の破綻はご容赦ください。

レイアウトリソースファイルはもう1枚あります。
MainActivity内でLayoutInflaterTextViewオブジェクトにしてもらうためのタネです。
このTextViewは、プログラムの方で1字ずつ分解した「野」や「千」を表示するためのものです。
プログラムの方でふわーっと浮いてどこかへ行くアニメーションを設定します。

name_entity.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 名前1字分 -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:fontFamily="@font/sanarifont"
    android:foregroundGravity="bottom"
    android:textColor="@android:color/black"
    android:textSize="80sp" />

最後に文字列リソースファイルです。

strings.xml
<resources>
    <string name="app_name">湯婆婆</string>
    <string name="keiyakushodayo">契約書だよ。\nそこに名前を書きな。</string>
    <string name="keiyakusuru">契約する</string>
    <string name="hun">フン。\n%sというのかい。\n贅沢な名だねぇ。</string>
    <string name="iikai">今からお前の名前は%sだ。\nいいかい、%sだよ。\n分かったら返事をするんだ、%s!!</string>
</resources>

%sの箇所が、新しい名前を埋め込む箇所です。

ポイント

そもそも元ネタ「Javaで湯婆婆を実装してみる」がJavaで実装し、私のこのAndroidアプリもJavaで実装しているわけですから、入出力のUIは違えども、アルゴリズム的には同じすぎて、この私の記事の存在意義が疑われるところではあります。

ですのでこの記事はむしろAndroidアプリ開発におけるTipsという位置づけでお読みいただければと存じます。
そこで、

  • カスタムフォント
  • ObjectAnimatorAnimatorSetを使ったアニメーション
  • ソフトウェアキーボードの引っ込ませ方
  • AndroidのJavaはJavaではない不思議な世界

について以下説明します。

フォントにこだわってみました

今一度、前掲の紙に書いた名前がふわーっと浮いてどこかへ行ってしまうシーンを鑑賞していただきたいのですが、なんとも味のある字体です。
そこで似たようなフリーフォントを探しました。
それが細鳴りフォント(Sanarifont)です。ありがとうございます。

APIレベル26から、フォントリソースが導入されました。これが導入される前は難儀を強いられましたが、導入後はラクになったものです。

フォントファイル(.ttf、.ttc、.otf、.xml)を/res/fontフォルダに格納します。ファイル名はリソースIDとして使用されます。

使用箇所は、

プログラム上なら
Typeface typeface = getResources().getFont(R.font.sanarifont);

や、

レイアウトXML上なら
<TextView
    android:fontFamily="@font/sanarifont" />

といったところです。

字がふわーっと浮いてどこかへ行ってしまうアニメーション

「荻」や「千」の字を埋め込んだTextViewを移動させ透明化させます。

というわけで、参考にしたのがPieceX:AndroidStudioでアニメーション アニメーションで紙吹雪を降らせる方法を考えてみましたです。ありがとうございます。
MainActivityに定義したprivateメソッドのコードは、このサイトを参考にさせていただきました。

ポイントは、様々な挙動をするアニメーションを複合化して同時に再生することです。今回は、

  • 上昇
  • 左右にゆらゆら
  • 透明化

といった挙動をまとめてアニメーションしたかったわけです。

そこでandroid.animation.ObjectAnimatorを利用するのが適しているようです。
それぞれのObjectAnimatorオブジェクトに、プロパティ("translationY""scaleX"など)と値(1.0f0.1fなど)を設定し、Listにまとめてandroid.animation.AnimatorSetに俺とプレイトゥギャザーしてスタートしようぜ!(ルー大柴の声でお楽しみください)で済みます。

普段あまりアニメーションなんかやったことがない私は、ここにいちばん時間をかけてしまいました。でも楽しかったです。

このアニメーション、もっともっと凝ることもできるかと思いますし、そういうライブラリーも世に有るかとは思いますが、私は一つ自分へのかせとして標準APIのみで実装することに挑戦してみたかった3ので、この程度のアニメーションで「もういいか」とさせてください。

EditTextをクリックしたら出てきちゃうソフトウェアキーボードを引っ込めさせる

「契約する」ボタンをクリックしたら、見せ所はその下方のアニメーションと湯婆婆のキメ台詞なのに、依然としてソフトウェアキーボードがのさばって見えねーじゃねーか!ということで、

もう、引っ込んでくれて構わないのだよ。お疲れ。
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
editText.clearFocus();

InputMethodManagerさんにhideしてくれるように頼みました。これがホントの神隠し。なんちって。

Stringを1字ずつの配列にしたくて、String#splitしてみたら...

なにげにこんなことでつまずきました。

当初はこんなコードで済ませていたのです。

String#splitでいいや
String[] names = name.split("");

ところがこれだと配列の0番目に空文字(長さ0の文字列)が入ってしまっていました。4
調べてみると、StringsplitメソッドはJDK1.7まではそのような挙動をし、JDK1.8以降ならば本来の私のやりたかった挙動をしてくれるようです。

え~~~。build.gradleにちゃんと、

build.gradle
compileOptions {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
}

とJDK1.8でお願いしますと設定してある5のにー?!...こういったあたりで「AndroidのJavaは、Javaに非ず」というダークな面が垣間見えますね。

ということで、なんかスマートじゃないなぁと後ろめたさもありながらも、String#charAtをグルグル回してやりました。

Try KotlinでString#splitしてみたら...

ちなみに、Kotlinに書き換えてみても、同じでした(配列の0番目が空文字だった)。
「AndroidのKotlinだからかな?」と思って、Try Kotlinにて以下のKotlinプログラムを実行してみました。

KotlinでString#splitしてみた
fun main(args : Array<String>) {
    println("Hello, Kobaba!")
    val name = "荻野千尋"
    val names = name.split("")
    for (aStr in names) {
        println(aStr)
    }
    println(names.size)
}
実行結果
Hello, Kobaba!

荻
野
千
尋

6

え。うそ。6個?!
このTry Kotlinで試せるすべてのバージョンで実行しても、同じ結果に。

EclipseでString#splitしてみたら...

急いでEclipse(Version: 2020-06)を起動して、以下のJavaを書きました。

EclipseでString#splitしてみたくて
public static void main(String[] args) {
    System.out.println("Hello, jababa!");
    String name = "荻野千尋";
    String[] names = name.split("");
    for (String aStr : names) {
        System.out.println(aStr);
    }
    System.out.println(names.length);
}
実行結果
Hello, jababa!
荻
野
千
尋
4

まあ、私のEclipseがJDK11だからなんでしょうけど、もう何が何やら。不思議な世界に迷い込んでしまったような気分、もう私は空を見上げてこう口遊くちずさむしかないです。

繰り返すあやまちの そのたび ひとは ただ青い空の 青さを知る

クラッシュ湯婆婆

そういえば、無記入の場合はちゃんとクラッシュ湯婆婆すべきなのがこのネタの重要な仕様でしたね!

安心してください、ちゃんとクラッシュ湯婆婆します。

crashyubaba.png

セリフ「java.lang.IllegalArgumentException: bound must be positive」まで一緒です。そうこなくっちゃ。

実現できなくて心残りなこと

  • フォントサイズが決め打ちのままです。5字以上の名前(「ダルビッシュ有」さんとか)を考慮していないままです。端末によってはとにかくはみ出ます。ちゃんと丁寧に<dimen>で定義すべきなのでしょう。
  • ふわーっと浮いてどこかへ行ってしまう字のも再現したかったです。
  • 紙から離脱当初は浮遊されるのをあらがって粘って踏みとどまろうとするのだが、いざ離脱してしまったら抵抗を諦めたという加速感。(もっといっぱいObjectAnimatorを作って組み合わせればいいのかな。あと、android.view.animation.AccelerateInterpolatorとか使えばいいのかな)
  • 映画のシーンをなぞって縦書き。(Androidで縦書きを実現することがどんだけ難儀なことか!)
  • サロゲートペア対策

サロゲートペア対策ねぇ...ちょ、ちょっと待って。

サロゲートペア対策「?田さん」よりも「获野千尋」!

まずこの湯婆婆ネタで賑わっているサロゲートペア漢字「?」の対策を講じようかと思い、でも今回使用させていただいたフォントでは「?」が含まれていないのではないかと危惧したので、文字情報技術促進協議会が提供しているIPAmj明朝フォントで試してみようと...したのですが。

そんなことよりも!

しつこいのですが、前掲の紙に書いた名前がふわーっと浮いてどこかへ行ってしまうシーンを鑑賞してください。または、ビデオ持っている方はそのシーンを眼ん玉ひん剥いて視聴してみてください。

おぎ」の字が「」となっているのです!くさかんむり「艹」+けものへん「犭」+「」ではなく、「」という構成になっているのです!「はぎ」と間違えている場合じゃない!

ナニその漢字?!製作ミス?!監督の遊び心?!諸説紛紛!!

しかも調べてみたら、「获」という漢字は実在していて、なんと「獲」の簡体字(中国で使われている簡略化した字体の漢字)だそうです!UnicodeはU+83B7。

千尋ちゃんは「オギノ」さんじゃなく「カクノ」さんなの?!
んでもって門田さんが面倒臭がって「门」とか書いちゃうノリ?!

サロゲートペア漢字もサポートしつつ、中国の簡体字もサポートしたフォント...そんなのあるのかしら?と探そうと思いましたが、とっくにありました。
私の持っているAndroidスマホの中に。標準フォントとして。

最後に

なんやかんやありましたが、結果一番お気に入りなのは、ランチャーアイコンです。

ic_launcher.png

スタジオジブリは作品の場面写真を常識の範囲で提供してくださっています。これもありがたいことですね。

以上です。良いお年を。


  1. この記事投稿時点での最新バージョンのAndroid Studioには、Javaで実装したプロジェクトを一気にKotlinに変換してくれる機能があります。でもその逆はありません。ですので、Javaで書いたものはいつでもKotlinに変換できますし。 

  2. このアニメーションGIFはダウンロード可能のようなのですが、他サイト(つまりこのQiita)に貼り付けていいのかわからなかったのでリンクにしました。 

  3. そう言うわりには、JetpackのAppCompatActivityを使っているというのはどういう了見なんだい?...湯婆婆様、そこだけは見逃しておくんなまし。 

  4. そのおかげで何度か試行してたら「荻野千尋」の4字すべて消えてしまう事態が発生。Randomがチョイスしたのが0だったんでしょう。「今からお前の名前はだ。いいかい、だよ。分かったら返事をするんだ、!!」って言われちゃいました。 

  5. おかげでラムダ式は書けました。 

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

[Android][iOS]Crashlytics導入メモ

1. Androidメモ

build.gradle(app)への設定の注意

Execution failed for task
‘:app:uploadCrashlyticsMappingFileRelease’.
> Expected file collection to contain exactly one file, however, it contains no files.

とかいうエラーが出た時の原因と対処

前提
build.gradleで「minify true」を設定している場合、ビルド時に難読化が適用され、mapping.txtが作成される。
「minify false」の場合、ビルド時に難読化されず、mapping.txtは作成されない。

原因と対処
「minify false」かつ「mappingFileUploadEnabled true」を設定すると、ビルド時にmapping.txtが作成されていなくても、FirebaseSDKはmapping.txtをFirebaseにアップロードしようとして、上記のエラーが発生する。

したがって、「minify false」を設定する場合は、「mappingFileUploadEnabled false」(またはmappingFileUploadEnabled自体を書かないデフォルトfalseだから)を設定する必要があり、
「minify true」を設定する場合は、「mappingFileUploadEnabled trueまたはfalse」どちらでもよい(ただし、mappingFileUploadEnabled falseの場合、Firebaseのクラッシュログは難読化されているので、クラッシュログ見てもよくわかんなくなりそう)。

参考

Crashテスト方法

  1. 以下のコードを任意のActivityのonCreate()に仕込む
throw new RuntimeException("Test Crash"); // Force a crash
  1. ビルドしてアプリを起動して上記Activityの画面まで遷移し、クラッシュさせる。
  2. アプリをキルして再起動して、Firebaseにクラッシュレポートとしてあがってくるのをまつ(最長5分かかるらしい)

2. iOSメモ

Crashテスト方法

  1. 以下のコードをトップの画面のソースに付け加える。
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view, typically from a nib.

        let button = UIButton(type: .roundedRect)
        button.frame = CGRect(x: 20, y: 50, width: 100, height: 30)
        button.setTitle("Crash", for: [])
        button.addTarget(self, action: #selector(self.crashButtonTapped(_:)), for: .touchUpInside)
        view.addSubview(button)
    }

    @IBAction func crashButtonTapped(_ sender: AnyObject) {
        fatalError()
    }
  1. ビルドしてアプリを起動してCrashボタンをタップしてクラッシュさせる
  2. アプリをキルして再起動して、Firebaseにクラッシュレポートとしてあがってくるのをまつ(最長5分かかるらしい)

※Xcodeと端末を接続してデバッグ状態だとうまく動かないので注意。

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

個人的にEpoxyを利用していく中で学んだ基本的な知識と使い方、差分更新とハマったことのまとめ

はじめに

今回は、複雑な画面の構築を楽にしてくれるairbnbのライブラリEpoxyをプロダクト案件で利用してきたので、サンプルを元に知見をまとめて紹介させていただこうと思います。
利用するサンプルはこちらです。
sample.gif

こんな感じのリストが簡単に作れるようになります。
最後の方で、サンプルのリンク先について記載していますので、コードだけ見たい方はさいごにまで飛ばしていただけると幸いです。

記事概要

この記事で説明することは、次のとおりです。

  • 利用にあたって必要な知識
  • 基本的な使い方
  • EpoxyModelの各メソッドについて
  • 差分更新について
  • StickyHeaderを持つUIの実装方法について
  • Epoxyでのカルーセルの基本的な使い方とSnapHelperについて
  • ケース別での困った時の対処法

個人的に、振り返った時に必要だった知識や使い方、ケース別での困った時の対処法など紹介します。

利用にあたって必要な知識

EpoxyConfig

DataBinding利用時に、EpoxyDataBindingPatternを使ってレイアウトファイルからBindingModel_クラスを自動生成するためのファイルです。

layoutPrefixに定義されている名前のxmlを作成することで、自動生成されます。

私の場合は、layoutPrefixにepoxy_を指定しています。

個別にxmlを指定していくやり方もありますが、この手法で行うとこのファイルに追加の記載が必要なくなるので、こちらを推奨します。

EpoxyConfigはインターフェースでもクラスでもよく、どのパッケージに置いても問題ありません。

EpoxyConfig.kt
@EpoxyDataBindingPattern(rClass = R::class, layoutPrefix = "epoxy_")
interface EpoxyConfig

EpoxyModelについて

後述するDataBindingModelを継承したクラスを指します。

役割として、Viewに特定の値をbindしたい時に利用します。

特に値など指定する必要がない場合は、自動生成されたBindingModel_クラスを利用してください。

以降、EpoxyModelのことをモデルと表現します。

例として、下記のようなクラスのことを指しています。コード中の...は省略を意味し以降同様です。

ListRowModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel : DataBindingModel<EpoxyListRowBinding>() {
  ...
}

DataBindingModelについて

Epoxyライブラリによるものではないですが、EpoxyModel構築の際に実装をしやすいように用意しています。

EpoxyModelWithHolderを継承したEpoxyModelを作成する際にDataBindingを使用するための抽象クラスです。

これを継承することで、後述する必須の3つのメソッドについてを忘れずに実装できます。

DataBindingModel.kt
abstract class DataBindingModel<in T : ViewDataBinding> :
    EpoxyModelWithHolder<DataBindingEpoxyHolder>() {

    abstract fun bind(binding: T, context: Context)

    abstract fun bind(binding: T, context: Context, previouslyBoundModel: EpoxyModel<*>)

    abstract fun unbind(binding: T)

    @Suppress("UNCHECKED_CAST")
    override fun bind(holder: DataBindingEpoxyHolder) {
        val binding = holder.binding as? T ?: return
        val context = binding.root.context
        bind(binding, context)
    }

    @Suppress("UNCHECKED_CAST")
    override fun bind(holder: DataBindingEpoxyHolder, previouslyBoundModel: EpoxyModel<*>) {
        val binding = holder.binding as? T ?: return
        val context = binding.root.context
        bind(binding, context, previouslyBoundModel)
    }

    @Suppress("UNCHECKED_CAST")
    override fun unbind(holder: DataBindingEpoxyHolder) {
        val binding = holder.binding as? T ?: return
        unbind(binding)
    }
}

EpoxyAttributeについて

注釈付きフィールドのゲッター、セッター、等号、およびハッシュコードを使用してモデルのサブクラスを生成するために、EpoxyModelクラスのフィールドに注釈を付けるためのアノテーションです。

EpoxyAttributeには、いくつかのOptionがありますが、 よく利用するケースとしてリスナーに関してはDoNotHashを利用します。

DoNotHashは、wikiにある通り、Epoxyは各モデルのequalsとhashCodeを呼び出して差分を検知して更新を行っています。

DoNotHashを指定することで、生成されたモデルのequalsとhashCodeの条件分岐がnullかどうかになり、一度入るとunbindでリソースの解放がされるまで差分更新の影響を受けなくなります。

バインド呼び出しごとに再作成される匿名のリスナーでは更新する必要がないため、このオプションを指定して定義するようにします。

CardElementModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {

    @EpoxyAttribute
    lateinit var uiData: UiData

    @EpoxyAttribute(DoNotHash)
    var onRootClicked: View.OnClickListener? = null
    ...
}

EpoxyRecyclerView

Epoxyの実装を楽にしてくれるRecyclerViewになります。

1番のメリットとして使用する場合は、EpoxyModelのビルドの際にEpoxyControllerを自身で用意する必要がなくなります。

他にもいくつかの利点があります。
LinearLayoutManagerがデフォルトで設定されているため、特にカスタムする必要がない単一リストの場合はLayoutManagerを用意する必要がありません。
Shared View Poolに関しては、同じアクティビティ内のすべてのインスタンスで同じビュープールを共有すると記載されています。

StickyHeaderLinearLayoutManagerについて

次のサンプルのようなStickyHeaderをもつUIを実装したい場合は、こちらのLayoutManagerを利用することで実現することが可能です。

Sticky.gif

詳しい使い方についてはStickyHeaderを持つUIの実装の仕方の章にて説明します。

基本的な使い方

Epoxy利用のレイアウトファイルを用意する

今回は、前述したEpoxyConfigのlayoutPrefixにてepoxy_からはじまる名前のレイアウトファイルを定義すると定めています。

例えば、次のようにepoxy_list_row.xmlという名前のレイアウトファイルを定義することで、ListRowBindingModel_クラスが自動生成されます。

ListRowBindingModel_.kt
public class ListRowBindingModel_ extends DataBindingEpoxyModel implements GeneratedModel<DataBindingEpoxyModel.DataBindingHolder>, ListRowBindingModelBuilder {
  ...
}

特に値などカスタムしない場合は、このまま自動生成されたクラスを利用することができます。

動作や値をカスタムしたい場合、EpoxyModelを用意する

Viewに表示する情報やクリックリスナーなど用意する必要がある場合は、DataBindingModelを継承したEpoxyModelクラスを作成します。

クラス名に特に決まりはないのですが、XXXModel(XXXは任意の文字列)として定義しています。

例えば、ListRowModelを例にとると、次のような実装になります。

ListRowModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {
    ...
    override fun bind(binding: EpoxyListRowBinding, context: Context) {
        ...
    }

    override fun bind(
        binding: EpoxyListRowBinding,
        context: Context,
        previouslyBoundModel: EpoxyModel<*>?
    ) {
        ...
    }

    override fun unbind(binding: EpoxyListRowBinding) {
        ...
    }
}

クラスに対して@EpoxyModelClassのアノテーションをつけ、layoutIdでレイアウトのxmlを指定します。
DataBindingModelを継承しているため、後述する必須の3つのメソッドをoverrideし、必要な処理を記載します。

Activity / Fragment側にてEpoxyRecyclerViewを用意する

ActivityかFragment側のレイアウトファイルにて、EpoxyRecyclerViewを用意してください。
例えば、 fragment_list.xml の実装は次のようになっています。

fragment_list.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
    >

    ...

            <com.airbnb.epoxy.EpoxyRecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                />
    ...              
</layout>

Activity / Fragment側にて生成されたEpoxyModelに値や動作を記述する

ActivityかFragment側にてモデルの構築を行います。サンプルの場合は、updateViews内のIDEALに記載されています。

EpoxyRecyclerView#withModelsにて、次にあげる例のソースコードのように、自動生成されたモデルに対して必要な情報や処理の記載を行います。

例えば、 ListFragment.kt の実装は次のようになっています。

ListFragment.kt
binding.recyclerView.withModels {
  ...
  val list = data ?: throw IllegalArgumentException("Illegal results are being returned. Please review the communication.")
  ...
  list.forEach {
    listRow {
      id(LIST_ROW_ID, "${it.id}")  // ユニークな値を指定する
      listData(it)  // EpoxyAttributeで定めたデータをセットします
      spanSizeOverride { _, _, _ -> COLUMN1 }  // 列数についての指定を行います
      onRootClicked { _ ->
        Toast.makeText(requireContext(), "Press ${it.name}!", Toast.LENGTH_SHORT).show()
      }
    }
  }
}

EpoxyModelの各メソッドについて

EpoxyModel作成の際には共通で実装すべきメソッドが3つあります。ここでは、それらについて説明します。※XXXには任意のモデルの名前が入ります

ここでは、具体例として、 ListRowModel.kt を元に説明します。全て表示すると差分が大きいため、一部ソースコードを省略しています。

bind(binding: EpoxyXXXBinding, context: Context)

指定のViewにデータやリスナーをセットします。

差分更新以外の通常の時に基本的な処理についてはこちらに記載をします。

例えば、ListRowModelの場合は次のようになります。

ListRowModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {

    @EpoxyAttribute
    lateinit var listData: ListData
    ...
    @EpoxyAttribute(DoNotHash)
    var onRootClicked: View.OnClickListener? = null

    override fun bind(binding: EpoxyListRowBinding, context: Context) {
        binding.apply {
            label.visibility = View.GONE
            // EpoxyAttributeにて定義したデータを元に処理を記載します
            name.text = listData.name
            description.text = listData.description ?: "No Description."
            Glide.with(context)
                .load(listData.userUrl)
                .centerCrop()
                .into(userIcon)
            // EpoxyAttributeにて指定したクリックリスナーをセットします
            root.setOnClickListener(onRootClicked)
        }
    }
  ...
}

bind(binding: EpoxyXXXBinding, context: Context, previouslyBoundModel: EpoxyModel<*>)

差分更新時に呼ばれる為、差分更新の挙動について記載を行います。

previouslyBoundModelは、以前にバインドされたものと同じIDを持つモデルです。

このモデルと現在のモデルを比較して、何が変更されたかによって更新を行うようにします。

例えば、ListRowModelの場合は次のようになります。

ListRowModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {
    ...
    @JvmField
    @EpoxyAttribute
    var isChanged: Boolean = false
    ...
    override fun bind(
        binding: EpoxyListRowBinding,
        context: Context,
        previouslyBoundModel: EpoxyModel<*>?
    ) {
        // 念の為同じIDを持つ前のモデルが違う場合は早期リターンを行います。
        if (previouslyBoundModel !is ListRowModel) return
        // 更新時に同じIDを持つ前のモデルと後のモデルで値の比較を行います。同じ場合は通常のbindメソッドを念の為呼ぶようにしています。
        if (previouslyBoundModel.isChanged == isChanged) {
            bind(binding, context)
            return
        }
        // 差分更新時に行いたい処理を記載しています。サンプルではisChangedの値が異なる場合に変わったことを示すラベルを表示するようにしています
        binding.apply {
            label.visibility = View.VISIBLE
        }
    }
    ...
}

この状態で、差分更新を行った時の挙動が次のとおりです。Fabをクリックした時にAPIを呼び出し、データの更新を行い、差分が検知されてChangedのラベルが表示されていることが確認できます。

update.gif

unbind

モデルにバインドされたビューがリサイクルされるときに呼び出されます。ビューがリサイクルされたときにリソースを解放する必要がある場合の処理を記載します。

利用パターンとしては、画像のリソース解放とリスナーの解放を行うようにしています。初期化として利用はしないことに注意してください。

例えば、ListRowModelの場合は次のようになります。

ListRowMode.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {
    ...
    override fun unbind(binding: EpoxyListRowBinding) {
        binding.root.setOnClickListener(null)
        binding.userIcon.setImageDrawable(null)
    }
}

差分更新について

差分更新の仕方は、更新したいModelの新しいデータを用意してbindにて差分更新の条件を明記します。

サンプルでは、更新したい際の動作に合わせて、API呼び出し後UiStateと一緒に新しいデータをセットして更新を行うようにしています。

APIを呼ばずに更新したい場合は、requestModelBuildを行う必要があります。

次の画面を例にとって説明します。

スクリーンショット 2020-11-13 18.24.27.png

差分更新の条件については、前述のbindについてを参照してください。

サンプルの場合だと、ViewModelにてViewに必要な情報をUiDataというクラスで保持しているため、API呼び出し後に更新したい項目のみ新しいものに入れ替えてデータを更新します。

ListViewModelを例にとって、データ更新のロジックは次の通りです。

ListViewModel.kt
class ListViewModel : ViewModel() {

    private fun repositoryBuilder() = GitHubRepository()
    val uiLive = MutableLiveData<Pair<UiState, UiData?>>()
    private var uiData = UiData(
        list = mutableListOf(),
        isChanged = false
    )

    fun getGitHubRepositoryData(since: Int, shouldChange: Boolean = false) {
        viewModelScope.launch {
            uiLive.postValue(UiState.LOADING to null)
            repositoryBuilder().getRepositories(since).collect { response ->
                try {
                    // 更新するためのデータを作り直す
                    uiData = uiData.copy(
                        list = response,
                        isChanged = shouldChange
                    )
                    // 更新したデータをLiveDataに渡す。サンプルはUIの状態として、UiStateを定めているため、一緒に渡しています。
                    uiLive.postValue(UiState.IDEAL to uiData)
                } catch (e: HttpException) {
                    uiLive.postValue(UiState.ERROR to null)
                }
            }
        }
    }
}

その後にLiveDataにて、更新したデータを流し再度recyclerView#withModelsが呼び出されることでモデルの再ビルドが走り、Epoxy側が内部的に差分を検知して差分更新のbindメソッドを呼び出すようになります。

サンプルの場合は、LiveDataを元にして必ずrecyclerView#withModelsが通るため、requestModelBuild()を呼ぶことはないですが、requestModelBuild()でもモデルの再ビルドになるため、同様の結果を受けることができます。

StickyHeaderを持つUIの実装方法について

利用する手順としては、次のとおりです。
1. TypedEpoxyControllerを用意して、buildModelsにて構築したいEpoxyModelについて記載する
2. EpoxyRecyclerViewに1で用意したControllerのAdapterと、StickyHeaderLinearLayoutManagerをセットする
3. TypedEpoxyController#setDataで更新したい時に、更新したいデータをセットする
4. HeaderとするModelの記載を行う

StickyListFragment.ktを例にとって説明します。
まず、TypedEpoxyControllerを用意して、buildModelsにて構築したいEpoxyModelについてbuildModelsの中に記載を行います。

StickyListFragment.kt
class StickyListFragment: Fragment() {
    private lateinit var binding: FragmentStickyListBinding
    ...
    // モデルにセットしたいデータを持つTypedEpoxyControllerを用意
    private lateinit var stickyHeaderController: TypedEpoxyController<List<ListData>>
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setUp()
    }

    private fun setUp() {
        viewModel = StickyListViewModel()
        stickyHeaderController = object : TypedEpoxyController<List<ListData>>() {
            override fun buildModels(data: List<ListData>) {
                // 構築したいEpoxyModelについて記載する
                data.forEach {
                    stickyHeader {
                        id(STICKY_HEADER_ID, "${it.id}Header")
                        titleText(it.name)
                        onTitleClicked { _ ->
                            Toast.makeText(requireContext(), "Press ${it.name}!", Toast.LENGTH_SHORT).show()
                        }
                    }

                    stickyContents {
                        id(STICKY_CONTENTS_ID, "${it.id}")
                        listData(it)
                        onRootClickListener { _ ->
                            Toast.makeText(requireContext(), "Press ${it.description}!", Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
          ...
        }
        ...
    }
  ...
}

次に、EpoxyRecyclerViewに用意したTypedEpoxyControllerのAdapterとStickyHeaderLinearLayoutManagerのセットを行います。

StickyListFragment.kt
class StickyListFragment: Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setUp()
    }

    private fun setUp() {
        ...
        binding.apply {
            ...
            recyclerView.layoutManager = StickyHeaderLinearLayoutManager(requireContext())
            recyclerView.adapter = stickyHeaderController.adapter
        }
        ...
    }
  ...
}

次に、TypedEpoxyController#setDataで更新したい時に、更新したいデータをセットするようにします。サンプルの場合は、通信が成功した時に、UiState.IDEALに更新時のデータが流れるので、ここでsetDataを行っています。

StickyListFragment.kt
class StickyListFragment: Fragment() {
    ...
    private fun updateViews(uiState: UiState, data: UiData?) {
        when(uiState) {
            ...
            UiState.IDEAL -> {
                val uiData = data ?: throw IllegalArgumentException("Illegal results are being returned. Please review the communication.")
                ...
                stickyHeaderController.setData(uiData.list)
            }
        }
    }
    ...
}

最後に、HeaderとするEpoxyModelの指定を行います。
次のようにすることで、Headerとなるモデルの指定が可能です。

class StickyListFragment: Fragment() {
    ...
    private fun setUp() {
        ...
        stickyHeaderController = object : TypedEpoxyController<List<ListData>>() {
            ...
            // HeaderとするEpoxyModelの指定を行う
            override fun isStickyHeader(position: Int): Boolean {
                return adapter.getModelAtPosition(position)::class == StickyHeaderModel_::class
            }
        }
        ...
    }
    ...
}

Epoxyでのカルーセルの基本的な使い方とSnapHelperについて

基本的な使い方

Epoxyを利用すると、カルーセルの実装が非常に楽に行うことができます。
カルーセルというのは、下記のサンプル画像の赤枠部分のUIを指します。
スクリーンショット 2020-11-16 13.12.49.png

では、ソースコードを見ていきましょう。
まずは、カルーセルにセットする項目の一つ一つのEpoxyModelのリストを作成します。サンプルでいうと、丸いアイコンとテキストのUIを持つEpoxyModelを定義したリストを作成します。

ListFragment.kt
binding.recyclerView.withModels {
  // カルーセルのリストを作成する
  val carouselList = list.map {
    // カルーセル中の項目の一つ一つのModel
    ContributorsModel_()
      .apply {
        id(CONTRIBUTORS_ID, "${it.id}")
        uiData(it)
        onRootClickListener { _ ->
          Toast.makeText(requireContext(), "Press ${it.name}!", Toast.LENGTH_SHORT).show()
        }
      }
    }
  ...
}

次に、EpoxyにはCarouselのEpoxyModelが標準で用意されており、Kotlin Extenstionsもサポートされているため、次のようにして利用します。

ListFragment.kt
binding.recyclerView.withModels {
  ...
  carousel {
    val margin12 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12F, resources.displayMetrics).toInt()
    val margin16 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16F, resources.displayMetrics).toInt()
    id(CAROUSEL_ID)
    models(carouselList)  // カルーセルのリストをセットする
    spanSizeOverride { _, _, _ -> COLUMN1 }
    numViewsToShowOnScreen(4.5f)  // 表示したいカルーセルの項目数を設定する
    padding(Carousel.Padding(0, margin16, 0, margin12, 0))  // カルーセルのPaddingを設定する
  }
}

EpoxyRecyclerView#withModelsの中で、modelsに先ほど作成したEpoxyModelのリストをセットします。
表示に関しては、これだけで可能です。
さらに、表示項目数を選ぶことができるため、サンプルではnumViewsToShowOnScreenで、4.5fと指定しています。これは、4.5個分の要素を表示することを示しています。
これだけではなく、paddingにてカルーセルの枠からの上下左右のpaddingと項目間のスペースをも指定することができます。サンプルでは上下にpaddingを追加しています。

カルーセルのSnapHelperについて

EpoxyではデフォルトでSnapHelperが指定されているため、特にカスタムせずに使用すると次のような挙動になります。
carouselSnap.gif

サンプルでは右端までスクロールして、戻らない挙動を実現したいため、無効にする対応を入れています。
その方法について紹介します。

Customizationにあるとおり、まずはカルーセルの振る舞いを変えるべくカルーセルのサブクラスを作成します。

DisableSnapHelperCarousel.kt
@ModelView(saveViewState = true, autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class DisableSnapHelperCarousel(context: Context) : Carousel(context) {
    override fun getSnapHelperFactory(): SnapHelperFactory? {
        return null
    }
}

getSnapHelperFactoryにて、Snapの振る舞いを変更できます。今回は無効化にしたいため、nullを指定します。

次に、Kotlin Extensionsと同様にモデルの構築ができるように、Builderクラスを用意します。

DisableSnapHelperCarouselBuilder.kt
class DisableSnapHelperCarouselBuilder(
    val disableSnapHelperCarousel: DisableSnapHelperCarouselModel_ = DisableSnapHelperCarouselModel_()
) : ModelCollector, DisableSnapHelperCarouselModelBuilder by disableSnapHelperCarousel {
    private val models = mutableListOf<EpoxyModel<*>>()
    override fun add(model: EpoxyModel<*>) {
        models.add(model)
        disableSnapHelperCarousel.models(models)
    }
}

ここでは、カルーセルにセットするカルーセルの項目の一つ一つのEpoxyModelを追加するようにしています。
最後に、Fragment側で拡張関数を作成し、EpoxyRecyclerView#withModelsの中で通常のカルーセルと同様に記載します。

ListFragment.kt
binding.recyclerView.withModels {
  ...
  customCarousel {
    val margin12 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12F, resources.displayMetrics).toInt()
    val margin16 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16F, resources.displayMetrics).toInt()
    id(CAROUSEL_ID)
    models(carouselList)  // カルーセルのリストをセットする
    spanSizeOverride { _, _, _ -> COLUMN1 }
    numViewsToShowOnScreen(4.5f)  // 表示したいカルーセルの項目数を設定する
    padding(Carousel.Padding(0, margin16, 0, margin12, 0))  // カルーセルのPaddingを設定する
  }
}

// 公式Kotlin Extensionsの時と同じようにして、Modelのセットを行う
private fun ModelCollector.customCarousel(builder: DisableSnapHelperCarouselBuilder.() -> Unit) : DisableSnapHelperCarouselModel_ {
  val carouselBuilder = DisableSnapHelperCarouselBuilder().apply { builder() }
  add(carouselBuilder.disableSnapHelperCarousel)
  return carouselBuilder.disableSnapHelperCarousel
}

これでサンプルを動かしてみると、SnapHelperが無効になりカルーセルの振る舞いのカスタムができていることがわかります。
disableSnap.gif

ケース別での困ったときの対処法

更新時のアニメーションが意図せずに走る場合

デフォルト状態だと、更新時に差分を検知してRecyclerViewのアニメーションが走ってしまいます。

EpoxyRecyclerViewを持つActivityやFragmentで、 recyclerView.itemAnimator = null を指定して、無効化する必要があります。

コンテンツの内容が重複している場合

モデルはIDをセットする必要があり、このIDがユニークな値でないと重複しているとし、同じ要素のViewが表示されてしまうことがあります。

この場合、IDをユニークな値にして重複を避けましょう。

差分更新がされない場合

新しいデータを入れるようにして、再度Modelをビルドしても差分更新がされない場合、モデルのIDに問題があるということがありました。

モデルのIDに、可変な値を指定してしまっているとモデルを別のものと認識して差分更新のbindが呼ばれません。差分更新がうまくいかない場合は、モデルのIDの見直しを行ってみてください。

ListFragment.kt
class ListFragment: Fragment() {
    ...  
    private fun updateViews(uiState: UiState, data: UiData?) {
        when(uiState) {
            ...
            UiState.IDEAL -> {
                 ...
                    list.forEach {
                        listRow {
                            id(LIST_ROW_ID, "${it.id}")  // IDはユニークな値にし、可変な値は指定しない
                            listData(it)
                            isChanged(data.isChanged)
                            spanSizeOverride { _, _, _ -> COLUMN1 }
                            onRootClicked { _ ->
                                Toast.makeText(requireContext(), "Press ${it.name}!", Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                }
            }
        }
    }
  ...
}

BooleanをEpoxyAttributeで利用したい場合

Epoxyのissueによると、Epoxyの生成されたモデルは属性ごとにgetterを作成してくれますが、

kotlinがgetterをfinalにして、ブール値の場合のみうまく生成されないようです。

対応自体が入っていない為、現状の回避策として、@JvmFieldを属性に付与して、フィールドに直接アクセスするようにします。

ListRowModel.kt
@EpoxyModelClass(layout = R.layout.epoxy_list_row)
abstract class ListRowModel: DataBindingModel<EpoxyListRowBinding>() {
    ...
    @JvmField
    @EpoxyAttribute
    var isChanged: Boolean = false
    ...
}

プルトゥがうまく動作しない場合

プルトゥリフレッシュを行いたい場合において、注意点があります。

モデルのビルドを行う際に、一番先頭にくるモデルが非表示になる可能性がある場合、つまり、タイミングによって先頭のモデルが非表示になってしまうとプルトゥが効かなくなる現象が判明してます。

EpoxyRecyclerView#withModelsにて最初に追加を行うEpoxyModelは必ず表示される想定のモデルを入れるようにしましょう。

回避策として、先頭に目に見えないほどの高さのダミーのViewのモデルを入れるという手法があります。

モデルのデータのセットやリスナーのセットがうまくいかない場合

カスタムしたEpoxyModelを利用している場合で、ActivityやFragmentにてモデルにデータをセットしたりリスナーをセットする際にうまくいかないことがあります。

その際に、現在使用しているモデルがカスタムしたEpoxyModelによって自動生成されたモデルを使用しているかの確認を行ってください。

カスタムしたEpoxyModelを使用している場合は、BindingModelとModelがそれぞれ自動生成されます。BindingModelを参照している可能性が高いため、一度見直しを行うようにしてください。

// BindingModel
hoge(modelInitializer: HogeBindingModelBuilder.() -> Unit)
// Model
hoge(modelInitializer: HogeModelBuilder.() -> Unit)

さいごに

Epoxyを利用することで、簡単にリストの構築ができるのでぜひ利用してみてください。
今回紹介したサンプルに関しては、下記リンクより詳細な実装をみることができます。

AndroidUITips | GitHub

少しでも何かの参考になれば幸いです。

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

今更Build Testable Apps for Androidを見て驚いたところメモ

概要

去年のIOで少し古い動画なのですが、すごくいい動画なので、見てみてください。
特に驚いたポイントを太字にしています。
かなり端折っているのでおかしいなって思ったら動画の方を確認してください。解釈が違いそうっていう所があればコメントか修正リクエストをください :bow:
https://www.youtube.com/watch?v=VJi2vmaQe6w

テストにおいて、キーとなるアトリビュートはスコープ

スコープは一つのメソッドでテストがアプリケーションのどのぐらいカバーしているのか、または複数の機能、複数の画面にまたがることはできる。
スコープはテストの他の2つのアトリビュートに影響を与える。
→ スピード
ミリ秒単位で終わるものもあれば何分もかかるものもある。
Fidelity(忠実さ)
テストケースのシミュレートが、どのぐらい本当の世界のシナリオに近いか。スコープを広げれば広げるほど、テストのFidelityは上がる。しかし、逆にスピードを遅くする。一つのテストではすべてにおいて一番いいものはできない。

テストのバランスを見るにはテストピラミッドを見ると良い。

テストピラミッドは
上に行くほどFidelity(忠実さ)が高く、スコープが広がる、しかし、スピードやフォーカスやスケーラビリティがない。
https://developer.android.com/training/testing/fundamentals?hl=ja より
image.png

テストピラミッドとアプリの設計

何も設計しない場合

何も考えずにコードを書いていくと密結合になっていって、テストすることが本当に難しくなり、End to end testで全てテストしないといけなくなる。そうすると以下のようなピラミッドになる。 これでテストしていくとすごく時間がかかる。
https://youtu.be/VJi2vmaQe6w?t=463 より
image.png

レイヤーでモジュールを分けた場合

DIを導入し、依存を切ることができる。そして、ユニットテストをすることができる。
しかし、複雑にアプリケーションが成長した場合、レイヤーよりも機能単位で成長していることに気づく。また、この方法でモジュール化したにもかかわらず、モジュール下のレイヤーの変更はアプリすべての再ビルドを引き起こす。そして、レイヤーがそれ自体がモノリスになっているため、巨大なend-to-endが作られてしまう。ユニットテストが書けることは良いが、end-to-endテストとunit testが膨らんだテストになってしまう。これの問題はユニットテストのFidelityの低さを、低速で重いend-to-endのテストで補おうとしていることだ。 大きなend-to-endのテストへ過剰に頼ると長い間テストが必要になってしまう。そしてフォーカスできないためバグを見つけるのを難しくする。
効果的(effective)なモジュール化なしではすべての変更で、全てのコードが再ビルドされ、全てのテストが走ってしまう。
これらのキーとなるポイントはチームのベロシティを下げてしまう。しかしコードの構造をちゃんとすれば、大きなテスタビリティへの影響と開発スピードを持つことができる。

https://youtu.be/VJi2vmaQe6w?t=546 より
image.png

プロジェクトを分解(Decompose)しよう

サンプルのTODOアプリではタスクを管理する部分と
プログレスモジュールがあり、これはタスクに依存している。
そして、タスクはたくさんの機能からなる事がわかる。追加、詳細、リスト表示など。
そしてこのアプローチはスケールし、グロースさせられる。例えば新しい機能も追加でき、また機能が複雑になったときに細かくブレイクダウンできる。
コンポーネントを独立させられるため、もっとフォーカスしたテストを書くことができる。

https://youtu.be/VJi2vmaQe6w?t=641
image.png

End-to-end test

Critical User Journeyというものがある。
プロジェクトでCritical User Journey(重要なユーザーの工程)を定義してから初める。

Critical User Journey

  • Critical User Journeyはステップバイステップでユーザーがアプリケーションですることが定義されている。
  • 決められた最終的なゴールに向かいます。
  • そのJourney(工程)はおそらく複数の画面で、ゴールに向けて決定するポイントがいくつもある。
  • そしてそれらはときどきモックの一連としてSketchされる。

Critical User Journeyの例はこういう形

User journey1: Creating a new task. (タスクを作る)

まずホームのタスクのリストにたどり着き、それは空になっていて、
追加ボタンを押し、TODOの詳細を入れ、
そしてセーブボタンを押し、
ホーム画面に戻り、新しいタスクが見れている状態になる。

End to end testでこれをカバーしよう。

  • Androidの環境で動くかを確かめる
  • できるだけリリースのアプリの依存関係を使う
  • ユーザーが使うのと同じようにテストする。つまりブラックボックステストになる。

網羅的である必要はない。それは他のレイヤーのテストの仕事。

Integration test

協調して動作していることを確かめる。End-to-endに頼らなくてすむようにするために大切。ここではすべて本物のコンポーネントを使うことはそこまで大切ではない。

実際のオブジェクトを使うと、遅かったりする、ビルドに時間がかかったりする、不特定のネットワークに依存してテストが失敗しやすくなったり、コントロールできなかったりするので、test doubleを検討する

  • Dummy: 依存関係を満たすためだけに値を返すもの
  • Stub: テストに必要な特定の動きを設定できる。これのためにMockitoが使える。
  • Fake: 正確でリアルな動きをする軽量なクラス
  • Real object: テストが読みやすく、堅牢になる。例えばValue Objectとかは常に本物を使ったほうが良い。

タスクを追加するFragmentのテストの例

  • Activtyはnavigationについて関心があるが、このレベルではここのテストはせずFragmentで良い。
  • NavigationControllerをtest doubleに置き換えて呼び出しを確かめる
  • リモートのDataSourceは遅く失敗しやすいので、Test doubleで置き換える
  • EspressoのAPIでテキストを入力させたり、クリックさせる
  • 2つのことを確認する
    • タスクがちゃんと保存されたかを確認する
    • 正しい画面に遷移したか?

https://youtu.be/VJi2vmaQe6w?t=1453 より
image.png

次にRepositoryのテストの例

  • Interfaceに対してテストを書く
  • FakeRepositoryに対してもプロダクションコードと同じテストを回す

    • このFakeを使うとビルドの時間を短くできたりもっと軽量のテストを書ける https://youtu.be/VJi2vmaQe6w?t=1658 より image.png
    • Fakeが信頼が置けるものになる

 * このRepositoryに依存するFragmentのIntegration TestなどでこのFakeのリポジトリが使い回せる

https://youtu.be/VJi2vmaQe6w?t=1708 より
image.png

Integration Testで全ての入力バリデーションなどができるか?それはユニットテストでやる。

Unit test

  • 小さい単位のコードを保証する。
  • ローカルで走る。
  • 網羅的にテストされる。
  • とても速い。そしてたくさん作られる。

プロダクションのdependenciesを交換して良いが、テストはブラックボックスであるべき。実装ではなく、挙動をテストしたい。
テストの境界はぼやけることがある。

  • 通常LocalDataSourceをテストしようとして、DaoをMockitoでモックすると実装の詳細を知りすぎることになる。そのため、振る舞いが同じなのに、実装を変えるときにテストも変えないといけなくなる
  • これはchange detector testとして知られている。これはすぐにメンテナンスを難しくする。 効果的なユニットテストは実装の代わりに振る舞いにフォーカスする。どのようにそれをするか?

image.png

Repositoryと同じようにTaskDaoを使ったFakeを作るか?おそらくTaskDaoはモジュールのpublicなAPIにならないので、このFakeを共有できず、それ以外の利益が無さそうなので作らない。
そのため、実際のRoomのDAOを使おう。Roomを使うとメモリ内にDBが作れる。

そしてこれはまだユニットテストか?結合テストになったか?多くの人が反対し、その境界がぼやけるかもしれない。
しかし、重要なポイントはテストでは実際の依存関係を使用することを恐れないこと。それによって読みやすく軽量で堅牢なテストが書ける。

まとめ

End-to-end testではCritical User Journeyをカバーしよう。
機能を分解し、integration testをしよう。テストするときにはUIからデータレイヤーに縦にスライスして (画面でスライス)してテストしよう。
次にモデル(Repository)に対してテストを書こう。これに他のモジュールが基づいているのでキーとなる。そしてUIやdatastoreなどに対する小さい単位のintegrationとunit testになる。
明確にモジュール間の契約を定義してコードベースを小さくすることで、ビルドを合理化し、ビルド時間を短縮し、交換できる検証されたFakeをExportできる。これによりテストを重たい本番環境への依存から切り離す。
アプリに自信を持たせるためにEnd-to-endテストを行うことはできるが、大部分をここにもってくるべきではない。モジュール化し、小さくすることでより焦点を絞ったテストにできる。そしてモジュールは切り離されている状態にする。
テストする場所を決める必要がある。どこをテストするのかドキュメント化して他のメンバーと共有する事が大事。



動画を見た雑なメモ書き


建造物でも、のこぎりがでてきたり、ツールやパターンや方法が進化してきた。ソフトウェアでも一緒。
Android Development ToolでもEclipse、Android Studiooになったり、Jetpackになったり。。
ただ、こうした進化は常に簡単ではなく、どのように設計するか、どのようにコードベースを管理するのか、どのようなライブラリを選ぶのか、どのようなツールを選ぶのか。この選択は開発の早い段階に行われる。これはアプリのテスタビリティに長い影響を与える。そして、それにより、開発速度、継続的に新しい機能の追加に影響を与える。
実際の例によって、どのように長期のテストの戦略を作るかを説明します。

テストにおいて、キーとなるアトリビュートはスコープです。
スコープは一つのメソッドでテストがアプリケーションのどのぐらいカバーしているのか、または複数の機能、複数の画面にまたがることはできる。スコープはテストの他の2つのアトリビュートに影響を与える。スピード、ミリ秒単位で終わるものもあれば何分もかかるものもある。Fidelity(忠実さ)、テストケースのシミュレートが、どのぐらい本当の世界の(シナリオに近いか。スコープを広げれば広げるほど、あなたのテストのFidelityは上がる。しかし、逆にスピードを遅くする。一つのテストではすべてにおいて一番いいものはできない。

いつ十分いいという状態になるの?いつパーフェクトになるの?いいバランスにするにはどうすればいいの?テストピラミッドはバランスを作るガイドとして利用される。ピラミッドの上の方ではFidelity(忠実さ)が高く、スコープが広がる、しかし、スピードやフォーカスやスケーラビリティがないというピラミッドです。

ユニットテストは速く、軽く、フォーカスされていて、スケールしやすい。一つのメソッドについてテストするだけなので、簡単に定義できる。これは失敗した場所にフォーカスできる。

インテグレーションテストではいくつかのユニットを一緒にするものです。それらのコラボレーションに関心がある。一緒にして、全体で期待した動きになっているかどうかを見る。

End-to-endテストはアプリケーションのキーパスをカバーする。複数の画面や複数の機能。アプリをどうテストすればよいのか知っているので、簡単に定義できる。

新しいTODOアプリケーションを作りました。Google Officialではないが、本当のアプリになっている。Android testing codelabの一部になっている。
https://github.com/android/testing-samples

これまで対面した議論などを紹介していきます。
アプリケーションを作るときにいくつかのキーとなるCritical User Journey(重要なユーザーの工程)を定義してから初めます。
Critical User Journeyはステップバイステップでユーザーがアプリケーションですることです。
また、決められた最終的なゴールに向かいます。
そのJourney(工程)はおそらく複数の画面で、ゴールに向けて決定するポイントがいくつもあります。
そしてそれらはときどきモックの一連としてSketchされる。

Critical User Journeyから実装を始めよう。
そしてTODOアプリでUXデザイナが送ってきたものの最初が以下になる。

User journey1: Creating a new task. (タスクを作る)

まずホームのタスクのリストにたどり着き、それは空になっていて、
追加ボタンを押し、T
ODOの詳細を入れ、
そしてセーブボタンを押し、
ホーム画面に戻り、新しいタスクが見れている状態になる。

User journey2: Checking your progress (タスクの進捗を見る)
ユーザーは存在するタスクを選択でき、
そしてタスクの完了をマークでき、
統計ページに行き
そしてその進捗を見ることができる。

すべてのプロジェクトは小さく始まる。もしデザインや設計、構造に意識を払わない場合、開発は早い段階で制御不能になる。何も考えずに作るとでかいモノリス、スパゲッティのようなボールになり、一貫しない依存関係について考えるのも難しいが、テストも困難にする。
もし個々の単位が高凝集で低結合などのキーとなる原則に従わない場合、独立によってテストすることが本当に難しくなる。それだけでなく、このようなモノリスなコードベースでは一つの変更を加えるとすべてを再ビルドする必要がある。そしてこの事実は大きいend to endを作らないといけなくなる。テストピラミッドが逆になってしまい、テストが不均一になってしまっている。
もし構造を作ることを考えて、レイヤーアーキテクチャーを考えたとしましょう。 開発の最初の段階ではこれが分けられる最初の部分で、各レイヤーにマッピングできるものがあるので、良さそうです。
このようにコードを構成した場合、高凝集で低結合といった原則を取り入れ、そしてDIを導入し、依存を切ることができる。そして、ユニットテストをすることができる。しかし、複雑にアプリケーションが成長した場合、レイヤーよりも機能単位で成長していることに気づく。また、この方法でモジュール化したにもかかわらず、モジュール下のレイヤーの変更はアプリすべての再ビルドを引き起こす。そして、レイヤーがそれ自体がモノリスになっているため、巨大なend-to-endが作られてしまう。ユニットテストが書けることは良いが、end-to-endテストとunit testが膨らんだテストになってしまう。これの問題はユニットテストのFidelityの低さを低速で重いend-to-endのテストで補おうとしていることだ。
バランスが取れたピラミッドを作るためのガイドがない。

そのため、あまり構造化、設計されていないコードベースは開発ワークフローでボトルネックを引き起こしやすい。大きなend-to-endのテストへ過剰に頼ると長い間テストが必要になってしまう。そしてフォーカスできないためバグを見つけるのを難しくする。
効果的(effective)なモジュール化なしではすべての変更で、全てのコードが再ビルドされ、全てのテストが走ってしまう。
これのキーとなるポイントはチームのベロシティを下げてしまう。しかしコードの構造をちゃんとすれば、大きなテスタビリティへの影響と開発スピードを持つことができる。

プロジェクトを解体することを考えてみよう。
TODOアプリではタスクを管理する部分と
プログレスモジュールがあり、これはタスクに依存している。
そして、タスクはたくさんの機能からなる事がわかる。追加、詳細、リスト表示など。

そしてこのアプローチはスケールし、グロースさせられる。新しい機能も追加でき、また機能が複雑になったときに細かくブレイクダウンできる。
このアプローチはただActivityであるというだけでまとめられるというアプローチよりも、同じドメインのコンポーネントのほうが機能的に関連しているため。
これはGradleのモジュールやBazelのライブラリで実現できる。

ドメイン指向のモジュールをアプリに追加でき、それらのAPIの境界でインタラクションを取り決められる。
そして、コンポーネントを独立させられるため、もっとフォーカスしたテストを書くことができる。
最後にintegration testの設計を見ましょう
もちろん全てのモジュールが解体されているので、ユニットテストも可能になっている。そしてユニットテストも可能になっている。そして大きいEend-to-end testも書くことができる。さらに、この構成によって新しい機能がスケールでき、それとともにテストもスケール可能になる。

このガイドをスタートポイントとして使うことができる。そして、もちろん、違った形でアプリを解体することができる。

アプリケーションを作るときにdata bindingやview model、live data, navigation, roomといったJetpackからアーキテクチャコンポーネントを利用した。そしてアプリの設計をMVVMに則った。これはとても良く関心事を分けられる。そして、Jetpackライブラリはこれをうまく行うことができる。
Single Activityから初め、navigation componentを使って、ユーザーのフローに対応付け、Fragmentを管理する。
それぞれのFragmentはそれぞれのxmlを持っており、それぞれのViewModelを持っている。。。。(アーキテクチャの紹介) モデルレイヤーはRepositoryに抽象化されている。

TDDのアプローチであれば、最初はEnd-to-endテストから始める。

End to endテスト

  • Androidの環境で動くかを確かめる
  • できるだけリリースのアプリの依存関係を使う
  • ユーザーが使うのと同じようにテストする。つまりブラックボックステストになる。

網羅的である必要はない他のレイヤーのテストの仕事になる。

Integrationテスト

強調して動作していることを確かめる。End-to-endに頼らなくてすむようにするために大切。ここではすべて本物のコンポーネントを使うことはそこまで大切ではない。
スコープを狭める候補何があるか?
フラグメント単位。
test doubleを使う。(遅かったりする、ビルドに時間がかかったりする、不特定のネットワークに依存してテストが失敗しやすくなったり、コントロールできなかったり)

  • Dummy: 依存関係を満たすためだけに値を返すもの
  • Stub: テストに必要な特定の動きを設定できる。これのためにMockitoが使える。
  • Fake: もっと正確でリアルな動きをする。
  • Real object: テストが読みやすく、堅牢になる。例えばValue Objectとかは常に本物を使ったほうが良い。

AddEditTasksFragmentTest

  • Activtyはnavigationについて関心があるが、このレベルではここのテストはせずFragmentで良い。
  • NavigationControllerをtest doubleに置き換えて呼び出しを確かめる
  • リモートのDataSourceは遅く失敗しやすいので、Test doubleで置き換える
  • EspressoのAPIでテキストを入力させたり、クリックさせる
  • 2つのことを確認する
    • タスクがちゃんと保存されたかを確認する
    • 正しい画面に遷移したか?

TaskRepositoryTest

  • Interfaceに対してテストを書く
  • FakeRepositoryに対してもプロダクションコードと同じテストを回す
    • このFakeを使うとビルドの時間を短くできたりもっと軽量のテストを書ける
    • Fakeが信頼が置けるものになる → * このRepositoryに依存するすべてのテストAddEditTasksFragmentTestなどでこのFakeのリポジトリが使える

ユニットテスト

全ての入力のバリデーションのケースで、これが保証できるか?
ユニットテストはここで使われる。

小さい単位のコードを保証する。
ローカルで走る。
網羅的にテストされる。
とても速い。そしてたくさん作られる。
プロダクションのdependenciesを交換して良いが、テストはブラックボックスであるべき。実装ではなく、挙動をテストしたい。
ここでのテストの境界はぼやけることがある。
- 通常LocalDataSourceをテストしようとして、DaoをMockitoでモックすると実装の詳細を知りすぎることになる。そのため、振る舞いが同じなのに、実装を変えるときにテストも変えないといけなくなる

これはchange detector testとして知られている。これはすぐにメンテナンスを難しくする。
効果的なユニットテストは実装の代わりに振る舞いにフォーカスする。どのようにそれをするか?

まずは実装方法として非同期になるので、確実に終わるようにrunBlocking{}などを使おう。
呼び出しをmockする方法だとすぐに汚くなってしまう。そのためRepositoryと同じようにTaskDaoを使ったFakeを作るか?おそらくTaskDaoはモジュールのpublicなAPIにならないので、このFakeを共有できず、それ以外の利益が無さそうなので作らない。そのため、実際のRoomのDAOを使おう。Roomを使うとメモリ内にDBが作れる。
そしてこれはまだユニットテストか?結合テストになったか?多くの人が反対し、その境界がぼやけるかもしれない。しかし、重要なポイントはテストでは実際の依存関係を使用することを恐れないこと。それによって読みやすく軽量で堅牢なテストが書ける。

まとめ

end-to-end testではcritical key user journeyをカバーしよう。
機能を分解し、integration testをしよう。テストするときにはUIからデータレイヤーに縦にスライスして (画面でスライス)してテストしよう。
次にモデル(Repository)に対してテストを書こう。これに他のモジュールが基づいているのでキーとなる。そしてUIやdatastoreなどに対する小さい単位のintegrationとunit testになる。
明確にモジュール間の契約を定義してコードベースを小さくすることで、ビルドを合理化し、ビルド時間を短縮し、交換できる検証されたFakeをExportできる。これによりテストを重たい本番環境への依存から切り離す。
アプリに自信を持たせるためにEnd-to-endテストを行うことはできるが、大部分をここにもってくるべきではない。モジュール化し、小さくすることでより焦点を絞ったテストにできる。そしてモジュールは切り離されている状態にする。
この方法で作ったアプリでは自然にいくつかの切断できるポイントができる。独自のテストする場所を決める必要がある。あるプロジェクトで動作しても他のプロジェクトでは動作しない場合があるので、どこをテストするのかドキュメント化して他のメンバーと共有する事が大事。

今後のAndroidではユニットテストでも端末のテストでも両方で使えるAPIを作っていて、それがどのようになるかやっていっている。


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

YoutubePlayerSupportFragmentがandroid11でinitializeに失敗するエラー(Service Messing)の対処法

背景

android11の端末でのみYoutubePlayerSupportFragmentを利用して動画を再生したがinitializeの時にエラー(Service Missing)を返却して再生できなくなった。
コード上でerror表示はあったがビルドは通るしandroid10までの端末では問題なく再生できたので調査を行った。

対処法

以前投稿したandroid11でTextToSpeechが動作しない時の対処法とほぼ同じ方法で動作するようになった。
マニフェストのqueriesに以下のように追加すると動作するようになります。

AndroidManifest.xml
<manifest>
  ・・・
    <queries>
   ・・・
        <intent>
            <action android:name="com.google.android.youtube.api.service.START" />
        </intent>
    </queries>
</manifest>

これを追加することで無事動画は再生されます。

別のエラー

上記の方法でビルドは通り動画は再生できるようになりますが、transaction.addの部分にErrorの表示が出ます。

YoutubeFragment.java
// YouTubeフラグメントインスタンスを取得
        YouTubePlayerSupportFragment youTubePlayerFragment = YouTubePlayerSupportFragment.newInstance();

        // レイアウトにYouTubeフラグメントを追加
        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
        transaction.add(R.id.youtube_layout, youTubePlayerFragment).commit();
//                      ↑ここにエラーが表示される

原因はandroidX対応している場合、androidx.fragment.app.FragmentTransactionをimportすることになるが、YoutubeSupportFragmentがandroid.support.v4.app.Fragmentをextendしている為,
Cannot resolve method addというエラーが表示されます。

対処法

ビルド可能なので対応としてはandroidx対応したYoutubeSuppportFragmentが提供されるのを待つでも良いですが、自分でandroidXに対応した YouTubePlayerSupportFragment(YouTubePlayerSupportFragmentX)を作成するという方法があります。

Projectのjava配下にcom.google.android.youtube.playerというディレクトリを作成し、androidX対応したYoutubePlayerSupportFragmentXを作ればエラーは消えます。
スクリーンショット 2020-11-13 16.46.44.png

YoutubePlayerSupportFragmentXの中身はYoutubePlayerSupportFragmentからコピペしてYoutubePlayerSupportFragmentXにリネームしてimportを置換えます。

import android.support.v4.app.Fragment;
→ import androidx.fragment.app.Fragment;

import android.support.v4.app.FragmentActivity;
→ import androidx.fragment.app.FragmentActivity;

YoutubePlayerSupportFragmentX.java
package com.google.android.youtube.player;

import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;

import com.google.android.youtube.player.internal.ab;

public class YouTubePlayerSupportFragmentX extends Fragment implements YouTubePlayer.Provider {
    private final YouTubePlayerSupportFragmentX.a a = new YouTubePlayerSupportFragmentX.a();
    private Bundle b;
    private YouTubePlayerView c;
    private String d;
    private YouTubePlayer.OnInitializedListener e;
    private boolean f;

    public static YouTubePlayerSupportFragmentX newInstance() {
        return new YouTubePlayerSupportFragmentX();
    }

    public YouTubePlayerSupportFragmentX() {
    }

    public void initialize(String var1, YouTubePlayer.OnInitializedListener var2) {
        this.d = ab.a(var1, "Developer key cannot be null or empty");
        this.e = var2;
        this.a();
    }

    private void a() {
        if (this.c != null && this.e != null) {
            this.c.a(this.f);
            this.c.a(this.getActivity(), this, this.d, this.e, this.b);
            this.b = null;
            this.e = null;
        }

    }

    public void onCreate(Bundle var1) {
        super.onCreate(var1);
        this.b = var1 != null ? var1.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE") : null;
    }

    public View onCreateView(LayoutInflater var1, ViewGroup var2, Bundle var3) {
        this.c = new YouTubePlayerView(this.getActivity(), (AttributeSet)null, 0, this.a);
        this.a();
        return this.c;
    }

    public void onStart() {
        super.onStart();
        this.c.a();
    }

    public void onResume() {
        super.onResume();
        this.c.b();
    }

    public void onPause() {
        this.c.c();
        super.onPause();
    }

    public void onSaveInstanceState(Bundle var1) {
        super.onSaveInstanceState(var1);
        Bundle var2 = this.c != null ? this.c.e() : this.b;
        var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2);
    }

    public void onStop() {
        this.c.d();
        super.onStop();
    }

    public void onDestroyView() {
        this.c.c(this.getActivity().isFinishing());
        this.c = null;
        super.onDestroyView();
    }

    public void onDestroy() {
        if (this.c != null) {
            FragmentActivity var1 = this.getActivity();
            this.c.b(var1 == null || var1.isFinishing());
        }

        super.onDestroy();
    }

    private final class a implements YouTubePlayerView.b {
        private a() {
        }

        public final void a(YouTubePlayerView var1, String var2, YouTubePlayer.OnInitializedListener var3) {
            YouTubePlayerSupportFragmentX.this.initialize(var2, YouTubePlayerSupportFragmentX.this.e);
        }

        public final void a(YouTubePlayerView var1) {
        }
    }
}

参考

https://stackoverflow.com/questions/52577000/youtube-player-support-fragment-no-longer-working-on-android-studio-3-2-android
https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492
https://stackoverflow.com/questions/63909770/why-youtube-player-api-does-not-work-on-android-11

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

YoutubePlayerSupportFragmentがandroid11でinitializeに失敗するエラー(Service Missing)の対処法

背景

android11の端末でのみYoutubePlayerSupportFragmentを利用して動画を再生したがinitializeの時にエラー(Service Missing)を返却して再生できなくなった。
コード上でerror表示はあったがビルドは通るしandroid10までの端末では問題なく再生できたので調査を行った。

対処法

以前投稿したandroid11でTextToSpeechが動作しない時の対処法とほぼ同じ方法で動作するようになった。
マニフェストのqueriesに以下のように追加すると動作するようになります。

AndroidManifest.xml
<manifest>
  ・・・
    <queries>
   ・・・
        <intent>
            <action android:name="com.google.android.youtube.api.service.START" />
        </intent>
    </queries>
</manifest>

これを追加することで無事動画は再生されます。

別のエラー

上記の方法でビルドは通り動画は再生できるようになりますが、transaction.addの部分にErrorの表示が出ます。

YoutubeFragment.java
// YouTubeフラグメントインスタンスを取得
        YouTubePlayerSupportFragment youTubePlayerFragment = YouTubePlayerSupportFragment.newInstance();

        // レイアウトにYouTubeフラグメントを追加
        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
        transaction.add(R.id.youtube_layout, youTubePlayerFragment).commit();
//                      ↑ここにエラーが表示される

原因はandroidX対応している場合、androidx.fragment.app.FragmentTransactionをimportすることになるが、YoutubeSupportFragmentがandroid.support.v4.app.Fragmentをextendしている為,
Cannot resolve method addというエラーが表示されます。

対処法

ビルド可能なので対応としてはandroidx対応したYoutubeSuppportFragmentが提供されるのを待つでも良いですが、自分でandroidXに対応した YouTubePlayerSupportFragment(YouTubePlayerSupportFragmentX)を作成するという方法があります。

Projectのjava配下にcom.google.android.youtube.playerというディレクトリを作成し、androidX対応したYoutubePlayerSupportFragmentXを作ればエラーは消えます。
スクリーンショット 2020-11-13 16.46.44.png

YoutubePlayerSupportFragmentXの中身はYoutubePlayerSupportFragmentからコピペしてYoutubePlayerSupportFragmentXにリネームしてimportを置換えます。

import android.support.v4.app.Fragment;
→ import androidx.fragment.app.Fragment;

import android.support.v4.app.FragmentActivity;
→ import androidx.fragment.app.FragmentActivity;

YoutubePlayerSupportFragmentX.java
package com.google.android.youtube.player;

import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;

import com.google.android.youtube.player.internal.ab;

public class YouTubePlayerSupportFragmentX extends Fragment implements YouTubePlayer.Provider {
    private final YouTubePlayerSupportFragmentX.a a = new YouTubePlayerSupportFragmentX.a();
    private Bundle b;
    private YouTubePlayerView c;
    private String d;
    private YouTubePlayer.OnInitializedListener e;
    private boolean f;

    public static YouTubePlayerSupportFragmentX newInstance() {
        return new YouTubePlayerSupportFragmentX();
    }

    public YouTubePlayerSupportFragmentX() {
    }

    public void initialize(String var1, YouTubePlayer.OnInitializedListener var2) {
        this.d = ab.a(var1, "Developer key cannot be null or empty");
        this.e = var2;
        this.a();
    }

    private void a() {
        if (this.c != null && this.e != null) {
            this.c.a(this.f);
            this.c.a(this.getActivity(), this, this.d, this.e, this.b);
            this.b = null;
            this.e = null;
        }

    }

    public void onCreate(Bundle var1) {
        super.onCreate(var1);
        this.b = var1 != null ? var1.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE") : null;
    }

    public View onCreateView(LayoutInflater var1, ViewGroup var2, Bundle var3) {
        this.c = new YouTubePlayerView(this.getActivity(), (AttributeSet)null, 0, this.a);
        this.a();
        return this.c;
    }

    public void onStart() {
        super.onStart();
        this.c.a();
    }

    public void onResume() {
        super.onResume();
        this.c.b();
    }

    public void onPause() {
        this.c.c();
        super.onPause();
    }

    public void onSaveInstanceState(Bundle var1) {
        super.onSaveInstanceState(var1);
        Bundle var2 = this.c != null ? this.c.e() : this.b;
        var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2);
    }

    public void onStop() {
        this.c.d();
        super.onStop();
    }

    public void onDestroyView() {
        this.c.c(this.getActivity().isFinishing());
        this.c = null;
        super.onDestroyView();
    }

    public void onDestroy() {
        if (this.c != null) {
            FragmentActivity var1 = this.getActivity();
            this.c.b(var1 == null || var1.isFinishing());
        }

        super.onDestroy();
    }

    private final class a implements YouTubePlayerView.b {
        private a() {
        }

        public final void a(YouTubePlayerView var1, String var2, YouTubePlayer.OnInitializedListener var3) {
            YouTubePlayerSupportFragmentX.this.initialize(var2, YouTubePlayerSupportFragmentX.this.e);
        }

        public final void a(YouTubePlayerView var1) {
        }
    }
}

参考

https://stackoverflow.com/questions/52577000/youtube-player-support-fragment-no-longer-working-on-android-studio-3-2-android
https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492
https://stackoverflow.com/questions/63909770/why-youtube-player-api-does-not-work-on-android-11

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

【Flutter】Firebase環境構築(iOS/Android)

参考文献

iOS編

1. Firebaseプロジェクト作成

Image from Gyazo

2. iOSアプリの追加

Image from Gyazo

  • iOSバンドルID

Image from Gyazo

3. GoogleService-Info.plist

Image from Gyazo

  • Xcodeプロジェクトに追加

Image from Gyazo

4.FlutterFire プラグインを追加する

  • firebase_core導入

Image from Gyazo

  • cloud_firestore導入

Image from Gyazo

Android編

1.Androidアプリの追加

Image from Gyazo

  • Androidパッケージ名
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="sample">

2.google-services.jsonの追加

Image from Gyazo

android/app
├── build.gradle
├── google-services.json  //追加
└── src

3.Firebase SDK の追加

android/build.gradle
dependencies {
    classpath 'com.google.gms:google-services:4.3.4'  //追加
  }
android/app/build.gradle
apply plugin: 'com.google.gms.google-services'  //追加
android/app/build.gradle
android {
    defaultConfig {
        ...
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true
    }
    ...
}

dependencies {
  implementation 'com.android.support:multidex:1.0.3'
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ADBでよく使うコマンド一覧

はじめに

ADBでよく使うコマンドを紹介します。

「ADB」とは?

「Android Debug Bridge」の略で、Android端末とやりとりするための多用途なコマンドです。
アプリのインストールやアンインストール、クラッシュログの出力など、さまざまな操作が行えます。

環境

  • Android Studio:4.0
  • adb:1.0.41

ADBのパスを通す

ADBを使うには、Android Studioをインストールし、パスを通す必要があります。

以下の記事をご参考に行ってください。
Android SDK Platform-Toolsのパスを通す方法 - Qiita

ADBでよく使うコマンド

グローバルオプション

オプション 説明
-s {シリアル} 対象シリアルの端末を使う

-s オプションを使うと、対象にしたい端末を確実に指定できます。
後述する adb devices で対象端末のシリアルを調べて指定するのがオススメです。

adb version

コマンド 説明
adb version ADBのバージョンを出力する

adb help

コマンド 説明
adb help ヘルプを出力する

adb devices

コマンド 説明
adb devices 接続されている端末の一覧を出力する
adb devices -l 接続されている端末の一覧を長い形式で出力する

adb install

コマンド 説明
adb install {パッケージ} 対象パッケージ(APKファイルなど)をインストールする
adb install -r {パッケージ} インストール済の対象パッケージを置き換えてインストールする
adb install -d {パッケージ} 対象パッケージのダウングレードを許可してインストールする(デバッグできるパッケージのみ)

adb uninstall

コマンド 説明
adb uninstall {パッケージ} 対象パッケージをアンインストールする

adb push

コマンド 説明
adb push {ローカルのファイルパス} {端末のファイルパス} ローカルの対象ファイル(またはフォルダ)を端末にコピーする

adb pull

コマンド 説明
adb pull {端末のファイルパス} {ローカルのファイルパス} 端末の対象ファイル(またはフォルダ)をローカルにコピーする

adb logcat

コマンド 説明
adb logcat デバイスログを出力する
adb logcat --help adb logcat のヘルプを出力する
adb logcat -b {バッファ} 別のリングバッファを要求してデバイスログを出力する
adb logcat -b crash クラッシュログを出力する

^C を入力しないと出力が終わらないので注意です。

adb shell

コマンド 説明
adb shell シェルを起動する
adb shell screencap -p {端末の画像パス} 対象パスにスクリーンショットを保存する
adb shell screenrecord {端末の動画パス} 対象パスにスクリーンレコードを保存する( ^C で撮影終了する)

ADBでよく使うテクニック

アプリの外部ストレージのデータをデスクトップにコピーする

以下を実行すると、端末内の files フォルダがデスクトップにコピーされます。

$ adb -s {シリアル} pull /storage/emulated/0/Android/data/{パッケージ}/files ~/Desktop/

上記のパスは getExternalFilesDir() メソッドの戻り値です。
環境によって異なるため、適宜置き換えてください。

アプリの内部ストレージにアクセスする

adb shell でシェルを起動し、 run-as {パッケージ} でアプリの内部ストレージにアクセスできます。

$ adb -s {シリアル} shell
sargo:/ $ run-as {パッケージ}
sargo:/data/user/0/{パッケージ} $

あとは lscd コマンドなどでどのようなファイルが存在するか確認できます。

スクリーンショットを撮ってデスクトップにコピーする

adb shell screencap でスクリーンショットを撮って端末内に保存し、 adb pull でデスクトップにコピーします。

$ adb -s {シリアル} shell screencap -p /sdcard/screenshot.png
$ adb -s {シリアル} pull /sdcard/screenshot.png ~/Desktop/
/sdcard/screenshot.png: 1 file pulled, 0 skipped. 23.0 MB/s (3150517 bytes in 0.130s)

スクリーンレコードを撮ってデスクトップにコピーする

スクリーンショットとほぼ同様です。
adb shell screenrecord でスクリーンレコードを撮って端末内に保存し、 adb pull でデスクトップにコピーします。

$ adb -s {シリアル} shell screenrecord /sdcard/screen.mp4
$ adb -s {シリアル} pull /sdcard/screen.mp4 ~/Desktop/
/sdcard/screen.mp4: 1 file pulled, 0 skipped. 26.7 MB/s (22255200 bytes in 0.795s)

クラッシュログをテキストファイルに出力する

adb logcat は標準出力されるため、テキストファイルに出力して Vim などのテキストエディタで開くと見やすいです。
「Caused by:」 にクラッシュの原因が書かれています。

$ adb -s {シリアル} logcat -b crash > ~/Desktop/logcat_crash.log

おわりに

これでADBのコマンドを忘れたときは、本記事を見れば解決できるかもしれません!

他にもよく使うコマンドがありましたら、コメントなどで教えていただけると嬉しいです :relaxed:

参考リンク

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

[Unity + Android + Windows] Android SDK 29を入れようとしたらsdkmanagerに怒られた

2020年11月2日からAndroidアプリのアップデートをする際にtargetSDKバージョン29以上が必須になったらしい

sdkmanagerでAndroid SDK 29を入れてみる

この記事を参考にAndroid SDK 29を入れようとしてみた
https://kudougames.com/2020/08/30/unity%E3%81%A7-android-api-level-29-%E3%81%AE%E3%83%93%E3%83%AB%E3%83%89/

しかし、sdkmanagerさんに以下のエラーで怒られます

C:\Program Files\Unity\Hub\Editor\2019.4.10f1\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\tools\bin>sdkmanager.bat "platforms;android-29"
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
        at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
        at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
        at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
        at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73)
        at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 5 more

解決方法

環境変数「Path」と「JAVA_HOME」にJDK 8のPathを追加するだけでした

例:C:\Program Files (x86)\Java\jre1.8.0_271\bin

おしまい

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

【Android】NavigationViewを使わないで、スライドするメニューをつくる

スライドするメニューはDialogFragmentで作成しました。
Activityの上に、DialogFragmentが被さるイメージです。

アニメーションの作成

左から右へスライドするアニメーション(res/anim/slide_in.xml)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="250"
        android:fromXDelta="-100%"
        android:toXDelta="0%" />

    <alpha
        android:duration="250"
        android:fromAlpha="0.8"
        android:toAlpha="1.0" />
</set>

右から左へスライドするアニメーション(res/anim/slide_out.xml)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="250"
        android:fromXDelta="0%"
        android:toXDelta="-100%" />

    <alpha
        android:duration="250"
        android:fromAlpha="1.0"
        android:toAlpha="0.2" />
</set>

themes.xmlに、作成したアニメーションのstyleを定義

<resources xmlns:tools="http://schemas.android.com/tools">
    ....
    <style name="SlideInMenuAnimation">
        <item name="android:windowEnterAnimation">@anim/slide_in</item>
        <item name="android:windowExitAnimation">@anim/slide_out</item>
    </style>
    ....
</resources>

スライドするメニューの作成

レイアウト

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/slide_menu"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".SlideInMenuFragment">

    <LinearLayout
        android:id="@+id/menu_area"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- ActionBarと同じ高さのスペースの余白 -->
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/design_default_color_primary"
            android:minHeight="?android:attr/actionBarSize" />

        <!-- メニュー部分 -->
        <ListView
            android:id="@+id/menu_list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</FrameLayout>

DialogFragmentの実装

public class SlideInMenuFragment extends DialogFragment {

    public SlideInMenuFragment() {
        // Required empty public constructor
    }

    public static SlideInMenuFragment newInstance() {
        SlideInMenuFragment fragment = new SlideInMenuFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = new Dialog(getActivity());
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.setContentView(R.layout.fragment_slide_in_menu);

        // 全画面表示のDialogで、左寄せに配置する
        Window window = dialog.getWindow();
        window.getAttributes().windowAnimations = R.style.SlideInMenuAnimation;
        window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        window.setGravity(Gravity.TOP | Gravity.START);
        window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);

        // LayoutのルートViewを設定
        FrameLayout rootView = dialog.findViewById(R.id.slide_menu);
        onViewCreated(rootView, savedInstanceState);
        return dialog;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        String[] menuArray = {"メニュー", "メニュー", "メニュー"};
        // メニューの内容を設定する
        ListView menu = view.findViewById(R.id.menu_list);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, menuArray);
        menu.setAdapter(adapter);
    }
}

MainActivityで呼ぶ

public class MainActivity extends AppCompatActivity {

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

        // ActionBarの左端にボタンを設定する
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            // ↓アイコンは標準のソートアイコン使ってますが、≡ に変えたらいい感じになります。
            actionBar.setHomeAsUpIndicator(android.R.drawable.ic_menu_sort_by_size);
        }
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            // メニュー表示
            FragmentManager fragmentManager = getSupportFragmentManager();
            SlideInMenuFragment fragment = SlideInMenuFragment.newInstance();
            fragment.show(fragmentManager, fragment.getClass().getSimpleName());
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

結果

アクションバーの左にあるアイコンをクリックすると、
image.png
 ↓
image.png
メニューが開きます。背景の透明部分をタップすると、メニューが閉じます。

注意

この実装だとジェスチャー操作して、メニューがピロピロと出てこないです。
自前でメニューを色々する必要があったのでこの方法を実装しましたが、
やっぱりNavigationView 使った方がいいです。

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

MacでAndroidをビルドする際に参考にしたサイト

macでunityをビルドしてAndroid実機でテストする方法
https://note.com/pes88/n/n2141cffd2f8f

UnityでAndroidにビルドする(Mac)
https://sh-lu0.hatenablog.com/entry/2018/12/09/Unity%E3%81%A7Android%E3%81%AB%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8B%28Mac%29

Unity2018インストールからAndroidビルドするまでの手順
https://qiita.com/taroyan/items/2ff7f6aadd640221bde5
※このサイトが一番参考になった

Android Studioは結局使ってない。どこかで使うシーンが出てくるんだろうが、テスト用にAndroidのスマホにビルドしたアプリを入れるだけなら必要なかったので、案外楽チンだった

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

docomo らくらくホン F-02J 物理ボタンのキーコード一覧

前書き

環境

データ
メーカー Fujitsu (docomo)
機種 F-02J
Androidバージョン 5.1.1
ビルド番号 V28R066A

調べ方

Android API のonKeyDownメソッド引数keyCodeを参照

実測値

ダイヤルパッド

表示キー キーコード
1 8
2 9
3 10
4 11
5 12
6 13
7 14
8 15
9 16
* 17
0 7
# 18

ワンタッチダイアル

表示キー キーコード 備考
96 外部アプリケーションのポインタ切り替えにも利用(無効化不可)
97
98

方向・サブメニューキー

表示キー キーコード 備考
19
20
21
22
決定 23
メニュー 131 Androidの機能に割り当てられている可能性あり(詳細不明)
電話帳 132

発信・戻る

表示キー キーコード 備考
発信 5
戻る 4 AndroidのBackボタンに割り当て
切断 - 不明(アプリケーションの終了になっているため)

本体下部ファンクションボタン

表示キー キーコード 備考
使い方 104
らくらくサイト 105
カメラ 103

サイドボタン

表示キー キーコード 備考
読み上げ 102
+ 24
- 25

ワンタッチブザー

表示キー キーコード 備考
ワンタッチブザー 83 (引き抜き時のみ。引き抜き状態では画面が隠される)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む