20200705のAndroidに関する記事は9件です。

【Unity】2019.4 Android実機でTextureが透過してしまう

Unityバージョン

2019.4.2f1

症状

image.png

解決方法

Render Over Native UI のチェックを外す
image.png

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

Firebase Crashlyticsを効果的に使うお話

最初に

今回はユーザーさんからのクラッシュの問い合わせをCrashlyticsを使い
どうしたら素早くやバグを解決できるか考えてみました。
デバッグって大変ですよね。

導入自体については、公式のドキュメントをご確認ください。
https://firebase.google.com/docs/crashlytics?hl=ja

今回は、Android javaでのコードを記載しました。

ユーザーID検索

ユーザーがログインした時などにユーザーの情報をセットしていくと、文字列をいれて送ることができます。

Crashlytics.setUserIdentifier("userid-12345678910");
Crashlytics.setUserEmail("userid-email");
Crashlytics.setUserName("userid-name");

そうするとこれはクラッシュ詳細の「データ」のタブに表示されます。
この画像の右下あたりのユーザーの部分です。
スクリーンショット 2020-07-05 16.04.35.png

実はこの値TOPの右下の「ユーザー検索」から検索ができて検索してみると
Email,Name,Identiferどれも横断で部分一致で検索してくれます。
スクリーンショット 2020-07-05 16.06.28.png

どのユーザーがどのクラッシュをしたかが特定できます。

ログとキー

これを使うことにより、クラッシュする前にどの画面を経由したか?が分かります。

ログ

// 各ActivityやFragmentでそれぞれ文字列を決めて、logにセットしていきます。
Crashlytics.log("******");

セットした値は、最終的にクラッシュしたクラッシュ詳細の「ログ」のタブに上から時系列順に並びます。
例えば以下は、1-aはAのActivity、2-bはBのActivityでCrashlytics.logを呼んでBでクラッシュさせたものです。
スクリーンショット 2020-07-05 16.15.50.png

そのため、どこの画面から遷移してクラッシュしたか?
クラッシュする前にどの画面を通ったか?などを確認することができます。

キー

Crashlytics.setInt("1-a", 1);
Crashlytics.setBool("1-a", true);
Crashlytics.setDouble("1-a", 1);
Crashlytics.setFloat("1-a", 1);
Crashlytics.setLong("1-a", 1);
Crashlytics.setString("1-a", "1-a");

key/valueでデータをセットして送る事もできます。
クラッシュ詳細の「鍵」のタブに集計されます。(翻訳ミスってる説)
スクリーンショット 2020-07-05 16.24.22.png

こちらの上から順に並んでいきます。
そして便利なのが「キー」をフィルタしますの部分に文字入力すると、そのキーのみの一覧表示ができます。
このログとキーについては、よく考えて設計すると、デバッグにかなり効果を発揮しそうです。

非重大

想定されるExceptionが起こった時、それをそのまま渡して集計ができます。
「クラッシュ」ではなく「非重大」の方にまとまります。

Crashlytics.logException(Exception);
// 自分でExceptionのインスタンスを作って渡す事もできます。
Crashlytics.logException(
  new IllegalArgumentException("IllegalArgumentExceptionのテストです")
);

こっちは、左上の「フィルタ」をクリックして非重大に切り替えて確認しましょう。
こちら上に記載したユーザー情報を入れる、ログやキーを入れる
をしておくと非重大の方の詳細にも同じように反映されます。

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

ActivityResultContractsの仕組みを調べてみる

activity:1.2.0、fragment:1.3.0 からActivityやFragmentの startActivityForResult / onActivityResultrequestPermissions / onRequestPermissionsResult あたりがdeprecatedになるそうですね。ActivityResultContractsを使えと言うことだそうです。
ちょっと使ってみたところ、良さそうなんだけど、どういう仕組みになっているのか気になったので調べてみました。

おさらい

startActivityForResult いままで

今まで、他のActivityから結果を受けとるには以下のようにしていました。

リクエスト

startActivityForResult(intent, REQUEST_CODE)

結果の受け取り

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            Toast.makeText(this, "result: $data", Toast.LENGTH_LONG).show()
        }
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}

ちなみに、呼び出し先ではsetResultでresultCodeと結果を詰めたIntentを設定してActivityを終了させることで呼び出し元に結果を返します。

setResult(Activity.RESULT_OK, intent)
finish()

startActivityForResult これから

registerForActivityResultを使ってコールバックを登録し、ActivityResultLauncherのインスタンスを取得しておきます。

private val launcher = registerForActivityResult(StartActivityForResult()) {
    Toast.makeText(this,"result: $it", Toast.LENGTH_LONG).show()
}

リクエストを投げるときはlaunchメソッドを呼び出します。

launcher.launch(intent)

返ってくると registerForActivityResult に渡したコールバックがコールされます。
コールバックの引数は ActivityResultonActivityResult の 第二引数の resultCode: Int と 第三引数 data: Intent を格納したクラスです。

requestPermission これまで

requestPermissionはstartActivityForResultに似た使い方をしますが、リクエストメソッドも、結果を受けとるメソッドも異なります。

リクエスト

ActivityCompat.requestPermissions(this, arrayOf(WRITE_EXTERNAL_STORAGE), REQUEST_CODE)

結果の受け取り

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    if (requestCode == REQUEST_CODE) {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "result: granted", Toast.LENGTH_LONG).show()
        }
        return
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

requestPermission これから

registerForActivityResult の第一引数に RequestPermission を指定してコールバックを登録

private val launcher = registerForActivityResult(RequestPermission()) {
    if (it) {
        Toast.makeText(this, "result: granted", Toast.LENGTH_LONG).show()
    }
}

lauchメソッドでリクエスト、今度はStringが引数になっていて、リクエストするPermissionを渡します。

launcher.launch(WRITE_EXTERNAL_STORAGE)

コールバックの引数はbooleanになっていて、成功した場合にtrueが返るようになっています。

また、複数のパーミッションを同時にリクエストする場合は RequestMultiplePermissions を使います。
こちらの場合はlaunchの引数が Array<String> になり、コールバックの引数は Map<String, Boolean> になります。

ActivityResultContractsの仕組み

registerForActivityResult の仕組み

ActivityResultRegistry を引数に追加しています。

ComponentActivity.java
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultCallback<O> callback) {
    return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

ActivityResultRegistry のregisterをコールしていますね。第一引数は key だそうですが、mNextLocalRequestCode は AtomicIntegerでコールされる度にインクリメントされるようです。

ComponentActivity.java
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull final ActivityResultContract<I, O> contract,
        @NonNull final ActivityResultRegistry registry,
        @NonNull final ActivityResultCallback<O> callback) {
    return registry.register(
            "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}

registerはちょっと長いメソッドです。

ActivityResultRegistry.java
@NonNull
public final <I, O> ActivityResultLauncher<I> register(
        @NonNull final String key,
        @NonNull final LifecycleOwner lifecycleOwner,
        @NonNull final ActivityResultContract<I, O> contract,
        @NonNull final ActivityResultCallback<O> callback) {

    final int requestCode = registerKey(key);
    mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));

    Lifecycle lifecycle = lifecycleOwner.getLifecycle();

    final ActivityResult pendingResult = mPendingResults.getParcelable(key);
    if (pendingResult != null) {
       // 略
    }

    lifecycle.addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
                @NonNull Lifecycle.Event event) {
            if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                unregister(key);
            }
        }
    });

    return new ActivityResultLauncher<I>() {
        @Override
        public void launch(I input, @Nullable ActivityOptionsCompat options) {
            onLaunch(requestCode, contract, input, options);
        }

        @Override
        public void unregister() {
            ActivityResultRegistry.this.unregister(key);
        }

        @NonNull
        @Override
        public ActivityResultContract<I, ?> getContract() {
            return contract;
        }
    };
}

最初にkeyからrequestCodeを作っています。
registerKeyではすでに登録済みならそのrequestCodeを、登録されていなければ 0x0000ffff を初期値として順次インクリメントした値が割り振られるようです。

ActivityResultRegistry.java
private int registerKey(String key) {
    Integer existing = mKeyToRc.get(key);
    if (existing != null) {
        return existing;
    }
    int rc = mNextRc.getAndIncrement();
    bindRcKey(rc, key);
    return rc;
}

その後の returnまでの処理は、Activityが再生成されて、コールバックが再登録されるまでの間に受け取った結果が合った場合に、コールバックを呼び出す処理ですね。
それと、onDestroyで自動的にunregisterしているので、unregisterを呼び出す必要も無いし、Fragmentから登録した場合も、メモリリークなどが起こらないようになっています。
結局戻り値は ActivityResultLauncher になっていて、launch がコールされたら onLaunch が呼び出されます。

onLaunchはabstructメソッドで、実装自体はこちらになります。さすがに長いしのでリンクにさせてもらいます。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:activity/activity/src/main/java/androidx/activity/ComponentActivity.java;l=134

かなり泥臭いことやってますね。requestPermissionとstartActivityForResult って別の処理なのにどうやって共通化したのかと思いきやこういう仕組みでした。
getSynchronousResult のところでは、requestPermissionですべてのパーミッションがとれている場合のように、リクエストする必要が無い場合は、即座にコールバックだけコールする仕組みも用意されていますね。

結果の受け取りのところは以下のようになっていて、onActivityResultonRequestPermissionsResultdispatchResultをコールしています。

ComponentActivity.java
@CallSuper
@Override
@Deprecated
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (!mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

@CallSuper
@Override
@Deprecated
public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    if (!mActivityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, new Intent()
            .putExtra(EXTRA_PERMISSIONS, permissions)
            .putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))) {
        if (Build.VERSION.SDK_INT >= 23) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

最終的に以下のようにコールバックされています。

ActivityResultRegistry.java
@MainThread
public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
    String key = mRcToKey.get(requestCode);
    if (key == null) {
        return false;
    }
    doDispatch(key, resultCode, data, mKeyToCallback.get(key));
    return true;
}

private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
        @Nullable CallbackAndContract<O> callbackAndContract) {
    if (callbackAndContract != null && callbackAndContract.mCallback != null) {
        ActivityResultCallback<O> callback = callbackAndContract.mCallback;
        ActivityResultContract<?, O> contract = callbackAndContract.mContract;
        callback.onActivityResult(contract.parseResult(resultCode, data));
    } else {
        mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
    }
}

やっていることはそんな複雑なことではないですね。

調べた結果

分かったこと

ちょっと使ってみて最初に疑問だったのが肝心のActivityResultContracts の扱い
registerForActivityResult(StartActivityForResult()) { } って、何に使われてるんだ?って疑問がありましたが、中を読めば分かりました。
launchの引数と、コールバックの引数はジェネリックスになっていて、これに対する型情報を提供するとともに、launchの引数からIntentを、また結果のresultCodeとdataからコールバックの引数を作る役割を持っています。
dataから読み出した結果を返すようにした、独自のActivityResultContractsを作ることもできますね。

よく分からなかったこと

気になる点があります、以下の記事で書かれているとおり、requestCodeがregisterの順序に依存してしまっています。
https://medium.com/@star_zero/a5a408c15f50
Activityが復元された場合にも結果を受けとるには、リクエスト時と同じ順序でregisterForActivityResultを登録しておかないといけません。
Activityで完結している場合はコンストラクタやonCreateでの登録をすれば問題無いのですが、Fragmentの場合、ActivityのActivityResultRegistoryに、FragmentのonCreate以降に登録されることになります。
特に動的に配置するFragmentの場合、順序の制御ができないのでどうすればいいのかがよく分かりませんでした。

ちょととモヤモヤしてること

Activity/Fragmentのメソッドで結果を受け取っていて、リクエストと結果の処理が分離してしまったりであまり綺麗に書けなかった処理が、個別のコールバックで書けるようになるのでいい感じです。
しかし、実際のリクエストはlaunchで指定する必要があるので、リクエストとコールバックをまとめるには少し足りていない感があります。
独自の ActivityResultContracts を作成するなどで解決できる問題ではありますが、できればそこも含めてライブラリ側で提供して欲しいなぁという気がします。

と、良さそうなんだけど、もうちょっとなところがある気がしました。
なんだかんだ言っても、現在はAlphaなので、これからどうなるか見守りたいです。

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

通信ライブラリ Cronet

Cronetとは

Google製の通信ライブラリ
https://developer.android.com/guide/topics/connectivity/cronet?hl=ja
Sample
https://github.com/GoogleChromeLabs/cronet-sample

AndroidDeveloperによると、YoutubeなどGoogleアプリで使われているとのことでした。

Retrofit + Okhttp一強の時代(だと思っている)ですが
他のネットワーク系のライブラリに触れる機会がなかったので勉強がてらに触ってみました。

使い方

ともあれ早速使ってみましょう!

サンプルのリポジトリには、ソースをcloneして使ってくれとあるのですが、mavenリポジトリに登録がありました。
https://mvnrepository.com/artifact/com.google.android.gms/play-services-cronet

dependencies {
    api "com.google.android.gms:play-services-cronet:18.0.0"
}

今回APIは、Qiita API v2を利用しました。
Sampleにあったものは画像をそのままロードするようなものだったのですが、これは文字列を最終的に取り出してみました。

UrlRequest.Callbackを継承したCronetCallbackクラスでネットワークの通信状態を受け取ります。
onSucceededメソッドで最終的にJSONの文字列を受け取ります。

public class MainActivity extends AppCompatActivity {

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

        // Cronet準備
        CronetEngine.Builder engineBuilder = new CronetEngine.Builder(getApplicationContext());
        //  HTTP/2とQiucでの通信を許可
        engineBuilder.enableHttp2(true).enableQuic(true);
        CronetEngine engine = engineBuilder.build();

     // 通信をするExecutorを指定できる
        // これは一つのスレッドですが、複数のスレッドを指定することができそうです
        Executor executor = Executors.newSingleThreadExecutor();
        CronetCallback callback = new CronetCallback();
        UrlRequest.Builder requestBuilder = engine.newUrlRequestBuilder(
                "https://qiita.com/api/v2/items", callback, executor);
        UrlRequest request = requestBuilder.build();

        // リクエスト開始
        request.start();
    }

    /**
     * onResponseStarted
     * onReadCompleted
     * onSucceeded の順で呼ばれる
     * 次のメソッドを通知したい場合、オーバーライドしたメソッドでrequest.read(byteBuffer)を呼ぶ
     */
    private class CronetCallback extends UrlRequest.Callback {

        private ByteArrayOutputStream bytesReceived = new ByteArrayOutputStream();
        private WritableByteChannel receiveChannel = Channels.newChannel(bytesReceived);

        @Override
        public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
            Log.d("CronetCallback", "onRedirectReceived");
            // リダレクトを許可する場合
            request.followRedirect();

            // リダイレクトを許可しない場合
            // request.cancel();
        }

        @Override
        public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
            Log.d("CronetCallback", "onResponseStarted");
            int statusCode = info.getHttpStatusCode();
            // ステータスコードが200系だったので中身をリードする
            if (statusCode <= 200 && statusCode < 300) {
                request.read(ByteBuffer.allocateDirect(32 * 1024));
            } else {
                // ステータスコードが200系以外
            }
        }

        @Override
        public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
            // リクエスト完了
            Log.d("CronetCallback", "onReadCompleted");
            byteBuffer.flip();
            try {
                receiveChannel.write(byteBuffer);
            } catch (IOException e) {
            }
            byteBuffer.clear();
            request.read(byteBuffer);
        }

        @Override
        public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
            // リクエスト成功
            Log.d("CronetCallback", "onSucceeded");
            byte[] byteArray = bytesReceived.toByteArray();
            String json = new String(byteArray);
            Log.d("CronetCallback", "json " + json);
        }

        @Override
        public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
            Log.d("CronetCallback", "onFailed : " + error.getMessage());
        }
    }
}

余談

Exoplayerにも通信部分をCronetで行うextensionsが追加されています。
実際にこのextensionsを使ってみたところ、Explayerのサンプル動画が再生できました。(okhttp版もあります)
https://github.com/google/ExoPlayer/tree/release-v2/extensions/cronet

所感

リダイレクトの指定があったら、レスポンスを受け取った時にステータスコードをみて次の処理に飛ばすか
などあり、その点便利かなと思いました。
そしてやっぱretrofitすごい

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

Google製 通信ライブラリ Cronet

Cronetとは

Google製の通信ライブラリ
https://developer.android.com/guide/topics/connectivity/cronet?hl=ja
Sample
https://github.com/GoogleChromeLabs/cronet-sample

AndroidDeveloperによると、YoutubeなどGoogleアプリで使われているとのことでした。

Retrofit + Okhttp一強の時代(だと思っている)ですが
他のネットワーク系のライブラリに触れる機会がなかったので勉強がてらに触ってみました。

使い方

ともあれ早速使ってみましょう!

サンプルのリポジトリには、ソースをcloneして使ってくれとあるのですが、mavenリポジトリに登録がありました。
https://mvnrepository.com/artifact/com.google.android.gms/play-services-cronet

dependencies {
    api "com.google.android.gms:play-services-cronet:18.0.0"
}

今回APIは、Qiita API v2を利用しました。
Sampleにあったものは画像をそのままロードするようなものだったのですが、これは文字列を最終的に取り出してみました。

UrlRequest.Callbackを継承したCronetCallbackクラスでネットワークの通信状態を受け取ります。
onSucceededメソッドで最終的にJSONの文字列を受け取ります。

public class MainActivity extends AppCompatActivity {

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

        // Cronet準備
        CronetEngine.Builder engineBuilder = new CronetEngine.Builder(getApplicationContext());
        //  HTTP/2とQiucでの通信を許可
        engineBuilder.enableHttp2(true).enableQuic(true);
        CronetEngine engine = engineBuilder.build();

     // 通信をするExecutorを指定できる
        // これは一つのスレッドですが、複数のスレッドを指定することができそうです
        Executor executor = Executors.newSingleThreadExecutor();
        CronetCallback callback = new CronetCallback();
        UrlRequest.Builder requestBuilder = engine.newUrlRequestBuilder(
                "https://qiita.com/api/v2/items", callback, executor);
        UrlRequest request = requestBuilder.build();

        // リクエスト開始
        request.start();
    }

    /**
     * onResponseStarted
     * onReadCompleted
     * onSucceeded の順で呼ばれる
     * 次のメソッドを通知したい場合、オーバーライドしたメソッドでrequest.read(byteBuffer)を呼ぶ
     */
    private class CronetCallback extends UrlRequest.Callback {

        private ByteArrayOutputStream bytesReceived = new ByteArrayOutputStream();
        private WritableByteChannel receiveChannel = Channels.newChannel(bytesReceived);

        @Override
        public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
            Log.d("CronetCallback", "onRedirectReceived");
            // リダレクトを許可する場合
            request.followRedirect();

            // リダイレクトを許可しない場合
            // request.cancel();
        }

        @Override
        public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
            Log.d("CronetCallback", "onResponseStarted");
            int statusCode = info.getHttpStatusCode();
            // ステータスコードが200系だったので中身をリードする
            if (statusCode <= 200 && statusCode < 300) {
                request.read(ByteBuffer.allocateDirect(32 * 1024));
            } else {
                // ステータスコードが200系以外
            }
        }

        @Override
        public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
            // リクエスト完了
            Log.d("CronetCallback", "onReadCompleted");
            byteBuffer.flip();
            try {
                receiveChannel.write(byteBuffer);
            } catch (IOException e) {
            }
            byteBuffer.clear();
            request.read(byteBuffer);
        }

        @Override
        public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
            // リクエスト成功
            Log.d("CronetCallback", "onSucceeded");
            byte[] byteArray = bytesReceived.toByteArray();
            String json = new String(byteArray);
            Log.d("CronetCallback", "json " + json);
        }

        @Override
        public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
            Log.d("CronetCallback", "onFailed : " + error.getMessage());
        }
    }
}

余談

Exoplayerにも通信部分をCronetで行うextensionsが追加されています。
実際にこのextensionsを使ってみたところ、Explayerのサンプル動画が再生できました。(okhttp版もあります)
https://github.com/google/ExoPlayer/tree/release-v2/extensions/cronet

所感

リダイレクトの指定があったら、レスポンスを受け取った時にステータスコードをみて次の処理に飛ばすか
などあり、その点便利かなと思いました。
そしてやっぱretrofitすごい

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

折り返し表示もこれで怖くない! Google製ライブラリ【FlexboxLayout】

FlexboxLayout

https://github.com/google/flexbox-layout
https://developers-jp.googleblog.com/2017/03/build-flexible-layouts-with.html

こういう表示を折り返し部分をやってくれる
スクリーンショット 2020-07-05 12.35.22.png

タグ表示などで使えそうです。

1、一つのViewの中にViewを配置すると自動で折り返しくれるFlexboxLayout
2、RecyclerView内のViewを紐づけるFlexboxLayoutManager(LayoutManager)
があります。

スクロールするような画面では、RecyclerViewの仕組みでViewが再利用されるので
FlexboxLayoutManagerを使うのが良さそうです。

使い方

今回は2のFlexboxLayoutManagerを使ってみます。
既存のRecyclerViewにLayoutMnagerをセットします。

// RecyclerViewを用意
RecyclerView recyclerView = findViewById(R.id.flexbox_layout);

// ライブラリに定義されているFlexboxLayoutManagerを準備
FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(flexboxLayoutManager);

// Adapterを準備
FlexLayoutManagerAdapter flexLayoutManagerAdapter = new FlexLayoutManagerAdapter();
recyclerView.setAdapter(flexLayoutManagerAdapter);
flexLayoutManagerAdapter.setData(getTagList());
  // データ
  private ArrayList<String> getTagList() {
    ArrayList<String> tags = new ArrayList<>();
      for (int i = 0; i < 5; i++) {
        tags.add("あいうえお");
        tags.add("かきくけこ");
        tags.add("さし");
        tags.add("すせそ");
        tags.add("たちつてとなにぬねのは");
        tags.add("ひふえほまみ");
        tags.add("むめもやゆよ");
        tags.add("らりる");
        tags.add("れろわをん");
      }
    return tags;
  }

で一番載せたに書いた画面ができ、上下にスクロールもします。

FlexLayoutManagerをインスタンス化するときに

new FlexboxLayoutManager(getApplicationContext(), FlexDirection.COLUMN);

第二引数にFlexDirection.COLUMNを指定することにより
縦柚はアイテムが揃っており、横にスクロールすることができるようにします。

スクリーンショット 2020-07-05 12.49.31.png

Adapterも載せておきますが、こっちは通常のRecyclerViewを表示する時のAdapterで大丈夫です。

// Adapter
    private class FlexLayoutManagerAdapter extends RecyclerView.Adapter {
        private ArrayList<String> tags;

        public void setData(ArrayList<String> tags) {
            this.tags = tags;
            notifyDataSetChanged();
        }

        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            return new FlexLayoutViewHolder(inflater.inflate(R.layout.tag_item, parent, false));
        }

        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof FlexLayoutViewHolder) {
                ((FlexLayoutViewHolder) holder).bind(tags.get(position));
            }
        }

        @Override
        public int getItemCount() {
            if (tags == null) return 0;
            return tags.size();
        }

        // Holder
        public class FlexLayoutViewHolder extends RecyclerView.ViewHolder {
            private TextView tagTextView;

            public FlexLayoutViewHolder(@NonNull View itemView) {
                super(itemView);
                tagTextView = itemView.findViewById(R.id.tag_text_view);
            }

            public void bind(String tag) {
                tagTextView.setText(tag);
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LiveDataをObserveする際の注意点

概要

みんなLiveDataって使ってますよね!LiveData便利ですよね!
でも意識していないと意図しない動作になってしまうことがあります。
本稿ではそんな落とし穴になり得る書き方の一つを紹介します。

TLDR

LiveDataのobserveメソッドを呼び出した際、LiveDataにイベントが保持されていればイベントを処理してしまう

LiveDataの頻出パターン

まずはLiveDataを使う上での頻出パターンを紹介します。

以下のような仕様がある前提で本稿を進めていきます。

LIVEボタンをタップした時にだけスナックバーを表示する。

まずViewModelにLiveDataに値を流すための処理を書きます。

class FirstViewModel: ViewModel() {
    // 外から値を流せないようにするために、mutableLiveDataはアクセス修飾子をprivateにしている。
    private val mutableLiveData = MutableLiveData<String>)()
    val liveData: LiveData<String> = mutableLiveData

    fun tappedButton() {
      mutableLiveData.value = "Tapped Button!!"
    }
}

次にFragmentでViewModelから流れてきた値を受け取るための処理を書きます。

class FirstFragment : Fragment() {

    private val viewModel: FirstViewModel by lazy {
        ViewModelProvider.NewInstanceFactory().create(FirstViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.first_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 次の画面に遷移するボタン.
        val nextButton: Button = view.findViewById(R.id.nextButton)
        nextButton.setOnClickListener {
            findNavController().navigate(R.id.from_first_to_second)
        }

        // ViewModelのLiveDataから値を流してもらうボタン.
        val liveButton: Button = view.findViewById(R.id.liveButton)
        liveButton.setOnClickListener {
            viewModel.tappedButton()
        }

        // ここでViewModelから流れてきた値を受け取る.
        viewModel.liveData.observe(viewLifecycleOwner, Observer {
            Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
        })
    }
}

念の為LiveDataで流れてきた値が受け取れるか確認しておきます。

liveData_example1.gif

SnackBarが表示されたので、きちんとLiveDataの値を受け取れていることが確認できますね。

LiveDataの落とし穴その1

では次に、以下のようにViewModelのMutableLiveDataにデフォルト値を入れてみます。

private val mutableLiveData = MutableLiveData<String>)("Not yet tapped button!!")

これで再度アプリを起動してみます。

livedata_excample2.gif

少し分かりづらいですがLIVEボタンをタップしていないにも関わらずスナックバーが表示されてしまっています。
これは仕様とは異なる動作です。。

LiveDataの落とし穴その2

では次に、Liveボタンを押したあとにFirstFragmentからSecondFragmentに遷移し、SecondFragmentからFirstFragmentに戻ってみます。

livedata_excample3.gif

FirstFragmentに戻ってきた際、LIVEボタンをタップしていないにも関わらずまたスナックバーが表示されてしまっています。
これも仕様とは異なる動作です。。

解決方法

どうやらobserveした際、LiveDataにイベントが保持されていればそのイベントを即座に処理してしまうようです。
上記の様な挙動は想定仕様のようで、設計上の問題として対処しなければいけないようです。

以降は前述した問題を解決するためのいくつかの方法について見ていくことにします。

解決方法1~イベントを処理したか否かの状態を持っておく~

まず思いつくのは解決方法は、イベントを処理したか否かの状態をフラグで管理する、というものかなと思います。

今回はFragment側で状態を持つようにしておきます。

class FirstFragment: Fragment() {
    private var hasBeenHandled = fasle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ・
        ・
        ・
        viewModel.liveData.observe(viewLifecycleOwner, Observer {
            if (hasBeenHandled.not()) {
                hasBeenHandled = true
                Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
            }
        })
    }
}

もしくは、

class FirstFragment: Fragment() {
    private var hasBeenHandled = fasle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ・
        ・
        ・
        if (hasBeenHandled.not) {
            hasBeenHandled = true
            viewModel.liveData.observe(viewLifecycleOwner, Observer {  
                Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
            }
        }
    }
}

ただこのフラグで状態をもつ方法には、

  • フラグを管理するコストがかかってしまったり、うっかりフラグの状態を書きかえてしまう可能性がある。
  • 一度処理されたあとは永遠にtrueになったフラグを持ち続けなければならない

というようなデメリットがあると思います。

解決方法2~イベントを流したあと、nullを流しておく~

LiveDataの型をStringからString?のnullableに変更し、値を流した直後にnullを流す、という方法もあるかと思います。

class FirstViewModel: ViewModel() {
    // 外から値を流せないようにするために、mutableLiveDataはアクセス修飾子をprivateにしている。
    private val mutableLiveData = MutableLiveData<String?>)()
    val liveData: LiveData<String?> = mutableLiveData

    fun tappedButton() {
      mutableLiveData.value = "Tapped Button!!"
      mutableLiveData.value = null
    }
}
class FirstFragment: Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ・
        ・
        ・
        viewModel.liveData.observe(viewLifecycleOwner, Observer {
            if (it != null) {
                Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
            }
        })
    }
}

この方法では、

  • いちいちnullを流してリセットしなければならない
  • リセットするためだけにLiveDataのジェネリック型をString?にしなければならない。(nullableは値がオプションのときに使用するためだと考えているためです。)

というようなデメリットがあります。

解決方法3~SingleLiveEventを使用する~

以下のような。イベントを流したときに1度だけobserveするクラスを作ります。

class SingleLiveEvent<T>: MutableLiveData<T>() {

    private val tag = "SingleLiveEvent"

    private var isPending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {

        if (hasActiveObservers()) {
            Log.w(tag, "Multiple observers registered but only one will be notified of changes.");
        }

        super.observe(owner, Observer<T?> {
            if (isPending.compareAndSet(true, false)) {
                observer.onChanged(it)
            }
        })
    }

    override fun setValue(value: T?) {
        isPending.set(true)
        super.setValue(value)
    }

    fun call(value: T?) {
        this.value = value
    }
}
class FirstViewModel : ViewModel() {

    private val mutableLiveData = SingleLiveEvent<String>()
    val liveData: LiveData<String> = mutableLiveData

    fun tappedButton() {
        mutableLiveData.call("Tapped Button!!")
    }
}
viewModel.liveData.observe(viewLifecycleOwner, Observer {
    Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
})

このようにすると、イベントを流したときのみスナックバーが表示されるようになります。

ただこの方法には一つのストリームを複数observeできない、というデメリットがあります。

解決方法4~イベントのラッパークラスを使用する~

最後に以下のような流したいイベントをラップするクラスを使用することです。

class Event<out T>(private val content: T) {
    var hasBeenHandled = false
        private set

    val contentIfNotHandled: T?
        get() {
            return if (hasBeenHandled) {
                null
            } else {
                hasBeenHandled = true
                content
            }
        }

    val peekContent: T = content
}
class FirstViewModel : ViewModel() {

    private val mutableLiveData = MutableLiveData<Event<String>>()
    val liveData: LiveData<Event<String>> = mutableLiveData

    fun tappedButton() {
        mutableLiveData.value = Event("Tapped Button!!")
    }
}
viewModel.liveData.observe(viewLifecycleOwner, Observer { event ->
    event.contentIfNotHandled?.let {
        Snackbar.make(view, it, Snackbar.LENGTH_LONG).show()
    }
})

内部で行っていることは解決方法1とほぼ同じことですが、Eventでラップするだけで状態を管理するコストが無くなるのでこの方法がベストなのかなと考えています。

まとめ

LiveDataは便利で簡単に扱えますが、気をつけていないと意図しない挙動が発生することがあるので少しだけ注意して使ったほうが良さそうです。

参考URL

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

(Android Studio)Gradleのプロキシ設定のやりかた

エラーの概要

学校や職場では、ネットワークにプロキシ(Proxy)サーバーが使われている。その環境下でAndroid開発を行うと、プロキシサーバーエラーになってしまう。
僕の場合は、学校のネットワークで開発を行っていました。Android studioのプロキシ設定は、[File]→[settings]→[Appearance&Behavior]→[System Settings]→[HTTP Proxy]から設定できます。

しかし、僕の場合はこれでは解決しませんでした。Gradleエラーになっていまうのです。

解決策

Android Studio 4.0で確認できました。
こちらを参考にしました。 https://qiita.com/hishida/items/9479fb64a016032c0938

1.gradlewかgradlew.batに以下を記述

set JAVA_OPTS=-DproxyHost=myproxy.co.jp //プロキシサーバーのアドレス
              -DproxyPort=8080          //ポート番号(ほぼ8080だからそのまま記述)
          //Http通信
              -Dhttp.proxyUser=****     //プロキシサーバーのユーザー名
              -Dhttp.proxyPassword=**** //プロキシサーバーのパスワード
        //Https通信
              -Dhttps.proxyUser=****   //プロキシサーバーのユーザー名(Httpと一緒でOK)
              -Dhttps.proxyPassword=**** //プロキシサーバーのユーザー名(Httpと一緒でOK)

2.gradle.propertiesに以下を記述

1を設定しただけでは、またエラーがでたので以下を記述。
※注意点は、gradle.propertiesは2つあって、Project PropertiesGrobal Propertiesの両方に記述すること

//Http通信
systemProp.http.proxyHost=myproxy.co.jp //プロキシサーバーのアドレス
systemProp.http.proxyPort=8080      //ポート番号(ほぼ8080だからそのまま記述)
systemProp.http.proxyUser=****      //プロキシサーバーのユーザー名
systemProp.http.proxyPassword=****    //プロキシサーバーのパスワード
//Https通信
systemProp.https.proxyHost=myproxy.co.jp //プロキシサーバーのアドレス(Httpと一緒でOK)
systemProp.https.proxyPort=8080      //ポート番号(ほぼ8080だからそのまま記述)
systemProp.https.proxyUser=****      //プロキシサーバーのユーザー名(Httpと一緒でOK)
systemProp.https.proxyPassword=****    //プロキシサーバーのユーザー名(Httpと一緒でOK)

3.その他

僕の場合は、壊れていなかったのでやっていませんがZipファイルが壊れている場合があるようなので、その場合は、https://qiita.com/hishida/items/9479fb64a016032c0938を参考にしてください。

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

Flutterのスプラッシュ画面でAndroidのダークテーマ対応

Flutterで開発中に、アプリ内ではダークテーマしたもののスプラッシュ画面が真っ白のままだったのが気になったので、
一番簡単にダークテーマ対応をする方法です。

スプラッシュ用のlaunch_background.xmlの記述を以下のようにするだけです。

launch_background.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="?android:attr/colorBackground"/> <!-- ここが変えた場所 -->
    <!-- You can insert your own image assets here -->
  <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/ic_launcher" />
    </item>
</layer-list>

ようするに、初期状態だと

    <item android:drawable="@android:color/white" />

このようになっているところを

    <item android:drawable="?android:attr/colorBackground" />

こう。
色々いじっていなければ、colorBackground属性にはOSのダークテーマに応じて白か黒が入っているので、
これを入れてあげると端末のダークテーマの設定に合わせてスプラッシュの背景色も変わります。

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