- 投稿日:2020-07-05T21:59:55+09:00
【Unity】2019.4 Android実機でTextureが透過してしまう
- 投稿日:2020-07-05T16:48:13+09:00
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");そうするとこれはクラッシュ詳細の「データ」のタブに表示されます。
この画像の右下あたりのユーザーの部分です。
実はこの値TOPの右下の「ユーザー検索」から検索ができて検索してみると
Email,Name,Identiferどれも横断で部分一致で検索してくれます。
どのユーザーがどのクラッシュをしたかが特定できます。
ログとキー
これを使うことにより、クラッシュする前にどの画面を経由したか?が分かります。
ログ
// 各ActivityやFragmentでそれぞれ文字列を決めて、logにセットしていきます。 Crashlytics.log("******");セットした値は、最終的にクラッシュしたクラッシュ詳細の「ログ」のタブに上から時系列順に並びます。
例えば以下は、1-aはAのActivity、2-bはBのActivityでCrashlytics.logを呼んでBでクラッシュさせたものです。
そのため、どこの画面から遷移してクラッシュしたか?
クラッシュする前にどの画面を通ったか?などを確認することができます。キー
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でデータをセットして送る事もできます。
クラッシュ詳細の「鍵」のタブに集計されます。(翻訳ミスってる説)
こちらの上から順に並んでいきます。
そして便利なのが「キー」をフィルタしますの部分に文字入力すると、そのキーのみの一覧表示ができます。
このログとキーについては、よく考えて設計すると、デバッグにかなり効果を発揮しそうです。非重大
想定されるExceptionが起こった時、それをそのまま渡して集計ができます。
「クラッシュ」ではなく「非重大」の方にまとまります。Crashlytics.logException(Exception); // 自分でExceptionのインスタンスを作って渡す事もできます。 Crashlytics.logException( new IllegalArgumentException("IllegalArgumentExceptionのテストです") );こっちは、左上の「フィルタ」をクリックして非重大に切り替えて確認しましょう。
こちら上に記載したユーザー情報を入れる、ログやキーを入れる
をしておくと非重大の方の詳細にも同じように反映されます。
- 投稿日:2020-07-05T16:27:55+09:00
ActivityResultContractsの仕組みを調べてみる
activity:1.2.0、fragment:1.3.0 からActivityやFragmentの
startActivityForResult
/onActivityResult
、requestPermissions
/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
に渡したコールバックがコールされます。
コールバックの引数はActivityResult
でonActivityResult
の 第二引数の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.javaprivate 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メソッドで、実装自体はこちらになります。さすがに長いしのでリンクにさせてもらいます。
かなり泥臭いことやってますね。requestPermissionとstartActivityForResult って別の処理なのにどうやって共通化したのかと思いきやこういう仕組みでした。
getSynchronousResult のところでは、requestPermissionですべてのパーミッションがとれている場合のように、リクエストする必要が無い場合は、即座にコールバックだけコールする仕組みも用意されていますね。結果の受け取りのところは以下のようになっていて、
onActivityResult
もonRequestPermissionsResult
もdispatchResult
をコールしています。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なので、これからどうなるか見守りたいです。
- 投稿日:2020-07-05T14:50:07+09:00
通信ライブラリ Cronet
Cronetとは
Google製の通信ライブラリ
https://developer.android.com/guide/topics/connectivity/cronet?hl=ja
Sample
https://github.com/GoogleChromeLabs/cronet-sampleAndroidDeveloperによると、YoutubeなどGoogleアプリで使われているとのことでした。
Retrofit + Okhttp一強の時代(だと思っている)ですが
他のネットワーク系のライブラリに触れる機会がなかったので勉強がてらに触ってみました。使い方
ともあれ早速使ってみましょう!
サンプルのリポジトリには、ソースをcloneして使ってくれとあるのですが、mavenリポジトリに登録がありました。
https://mvnrepository.com/artifact/com.google.android.gms/play-services-cronetdependencies { 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すごい
- 投稿日:2020-07-05T14:50:07+09:00
Google製 通信ライブラリ Cronet
Cronetとは
Google製の通信ライブラリ
https://developer.android.com/guide/topics/connectivity/cronet?hl=ja
Sample
https://github.com/GoogleChromeLabs/cronet-sampleAndroidDeveloperによると、YoutubeなどGoogleアプリで使われているとのことでした。
Retrofit + Okhttp一強の時代(だと思っている)ですが
他のネットワーク系のライブラリに触れる機会がなかったので勉強がてらに触ってみました。使い方
ともあれ早速使ってみましょう!
サンプルのリポジトリには、ソースをcloneして使ってくれとあるのですが、mavenリポジトリに登録がありました。
https://mvnrepository.com/artifact/com.google.android.gms/play-services-cronetdependencies { 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すごい
- 投稿日:2020-07-05T12:56:53+09:00
折り返し表示もこれで怖くない! Google製ライブラリ【FlexboxLayout】
FlexboxLayout
https://github.com/google/flexbox-layout
https://developers-jp.googleblog.com/2017/03/build-flexible-layouts-with.htmlタグ表示などで使えそうです。
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を指定することにより
縦柚はアイテムが揃っており、横にスクロールすることができるようにします。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); } } }
- 投稿日:2020-07-05T11:33:49+09:00
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で流れてきた値が受け取れるか確認しておきます。
SnackBarが表示されたので、きちんとLiveDataの値を受け取れていることが確認できますね。
LiveDataの落とし穴その1
では次に、以下のようにViewModelのMutableLiveDataにデフォルト値を入れてみます。
private val mutableLiveData = MutableLiveData<String>)("Not yet tapped button!!")これで再度アプリを起動してみます。
少し分かりづらいですがLIVEボタンをタップしていないにも関わらずスナックバーが表示されてしまっています。
これは仕様とは異なる動作です。。LiveDataの落とし穴その2
では次に、Liveボタンを押したあとにFirstFragmentからSecondFragmentに遷移し、SecondFragmentからFirstFragmentに戻ってみます。
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
- 投稿日:2020-07-05T11:05:47+09:00
(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/9479fb64a016032c09381.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 PropertiesとGrobal 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を参考にしてください。
- 投稿日:2020-07-05T03:03:36+09:00
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のダークテーマに応じて白か黒が入っているので、
これを入れてあげると端末のダークテーマの設定に合わせてスプラッシュの背景色も変わります。