20220322のAndroidに関する記事は4件です。

FragmentContainerViewを使用した際の、ActivityでのNavigationComponentの書き方

はじめに 今まで、フラグメントを使う際にActivityで、fragmentを使用していたのですが、FragmentContainerViewの方がFragmentに適したContainerの仕組みが提供されているらしくパフォーマンスが高いらしいと言うことを知ったので、FragmentContainerViewを使い始めたのですが、その時に、ActivityでのNavigationの記述の仕方が少し変わっていて、そこで少しハマったのでその解決方法を残しておきます。 両者の違い fragmentを使用した場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = Navigation.findNavController(this, R.id.activity_main_nav_host_fragment) val bottomNavigationView = findViewById<BottomNavigationView>(R.id.activity_main_bottom_navigation_view) NavigationUI.setupWithNavController(bottomNavigationView, navController) } fragmentを使用した場合は、findNavControllerを用いて、Activityのfragmentのidを取得しています。 FragmentContainerViewを使用した場合 @AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = supportFragmentManager.findFragmentById(R.id.activity_main_nav_host) val bottomNavigationView = findViewById<BottomNavigationView>(R.id.activity_main_bottom_navigation_view) NavigationUI.setupWithNavController(bottomNavigationView, navController!!.findNavController()) } } FragmentContainerViewを使用した場合は、findFragmentByIdを用いて、Activityにあるidを取得しています。 微妙な違いなんですが、これができてないとエラーが起きてビルドできないので注意してください、、、、、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVVM + Repository + Hilt + Retrofitで、Repositoryレイヤーからキャッシュを用いてAPIデータを取得する

はじめに 先日Retrofitを、使ってデータを取得する処理を書いている時に、毎回毎回リモートサーバーからデータを取得するのは、通信量での負担がかかってしまいパフォーマンスが落ちてしまうという、レビューを受けてそれを解消するために、Repositoryレイヤーをうまく使って通信量の負担を減らす処理を教えていただいたので記録がてら書いておきます。 やり方 @Singleton class MainRepository @Inject constructor(private val service: MainService) { private var cache: List<Demo>? = null suspend fun fetchDemo() = service.fetchDemo().body().also { cache = it } suspend fun getDemo() = cache ?: fetchDemo() } イメージとしては、getDemo()を呼び出して、その時にcacheがnullならばfetchDemo()を呼び出して、cacheが値を持っているならばそのまま返すという感じです。これにより、呼び出す時にはcacheからなのかfetchDemo()からなのかこちらは意識せずに、データを取得することができます。 もしリモートサーバーから最新の情報を取得する必要がある場合だけfetchDemo()を呼び出してあげればOKですね! 同時呼び出しへの対策 上記の方法を使えば、毎回毎回リモートから取得する必要がなくなるので、だいぶ負担が減ると思います。 おまけで、2回以上同時に関数を呼ばれた際の対処法も載せておきます。 @Singleton class MainRepository @Inject constructor(private val service: MainService) { private var cache: List<Demo>? = null private val mutex = Mutex() suspend fun fetchDemo() = service.fetchDemo().body().also { cache = it } suspend fun getDemo() { mutex.withLock { cache ?: fetchDemo() } } } Mutexを使うことで、ブロック内を同時に一件のみ実行するようにすることができ、しっかり対策できました! 終わりに 少しづつでも、こういった細かいところまでしっかりコードが書けるようになりたいと思いました? まだまだ自分のコードは改善できるところがたくさんあるはずなので、ちゃんと勉強していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Timber×Crashlytics でログをクラッシュレポート送信(Crashlytics編)

はじめに 日が空いてしまいましたが、後編です。 前編は「Timber×Crashlytics でログをクラッシュレポート送信(Timber編)」です。 前編では Timber を用いて、デバッグビルドとリリースビルドで、ログ出力を切り替える基盤を作成しました。 後編では、前編で作成した ReleaseTree クラスを用います。 そして、リリースビルド時は、「コンソールへログ出力しない」かつ「クラッシュ時にレポートを送信する」機能を組み込んでいきます。 Firebase Crashlytics Firebase Crashlytics は、モバイルアプリで発生したクラッシュを検知して、クラッシュが発生するまでのログを送信するクラッシュレポートツールです。 クラッシュレポートはFirebaseコンソールに集約され、クラッシュの分析や調査に役立ちます。 導入方法 前提 Googleアカウントの作成 Firebase Crashlytics のセットアップ セットアップに関しては、公式ドキュメントがかなり丁寧でわかりやすいので、本稿では省略します。 ※ Firebaseの導入と、Firebase Crashlyticsの導入に分かれている点に注意してください。 Firebase 公式 - Android プロジェクトに Firebase を追加する Firebase Crashlytics 公式 - Firebase Crashlytics を使ってみる プログラム変更 上記の公式手順をもとに、前編のTimber編 からの変更点を記載します。 build.gradle(:project) ※ プロジェクト直下 build.gradle buildscript { .... + dependencies { + classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + } } .... build.gradle(:app) ※ app直下 build.gradle .... apply plugin: 'com.android.application' + apply plugin: 'com.google.gms.google-services' .... dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' + implementation platform('com.google.firebase:firebase-bom:29.2.0') + implementation 'com.google.firebase:firebase-analytics' + implementation 'com.google.firebase:firebase-crashlytics' } MainActivity.java MainActivity.java public class MainActivity extends AppCompatActivity { - private final String TAG = MainActivity.class.getName() + "_TAG"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);      // 後編では不要なので消しておきます - Log.v(TAG, "Log VERBOSE"); - Log.d(TAG, "Log DEBUG"); - Log.i(TAG, "Log INFO"); - Log.w(TAG, "Log WARN"); - Log.e(TAG, "Log ERROR"); - Log.println(Log.ASSERT, TAG, "Log ASSERT"); - Timber.v("Timber VERBOSE"); - Timber.d("Timber DEBUG"); - Timber.i("Timber INFO"); - Timber.w("Timber WARN"); - Timber.e("Timber ERROR"); - Timber.wtf("Timber ASSERT"); + Button crashButton = new Button(this); + crashButton.setText("Test Crash"); + crashButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + throw new RuntimeException("Test Crash"); // Force a crash + } + }); + addContentView(crashButton, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); } } クラッシュレポート まずは公式ドキュメントに従って、クラッシュを強制的に起こし、クラッシュレポートを確認します。 ※ Test Crash ボタンをタップすると、アプリが落ち、クラッシュレポートが送信されます。 Timber × Firebase Crashlytics 導入が完了したところで、前編のTimber編 で作成した ReleaseTree へ、Firebase Crashlytics を組み込んでいきます! プログラム変更 MyApplication.java MyApplication.java public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { Timber.plant(new Timber.DebugTree()); + // デバッグビルドの場合はクラッシュレポートを送信しないように設定 + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false); } else { Timber.plant(new ReleaseTree()); } } } ポイント ビルド方法によって、Timberの設定を切り替えます FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) の一文を書いておくことで、クラッシュレポートを送信しないように設定できます (この設定をしないと、開発中に実行時例外が出るたびにクラッシュレポートが送信されてしまいます) ReleaseTree.java ReleaseTree.java public class ReleaseTree extends Timber.Tree { @Override protected void log(int priority, @Nullable String tag, @NotNull String message, @Nullable Throwable t) { switch (priority) { - case Log.INFO: - case Log.WARN: - case Log.ERROR: - case Log.ASSERT: - // Crashlytics ログ出力処理を記述(Crashlytics編で記載) - break; + case Log.INFO : recordLog("I", message); break; + case Log.WARN : recordLog("W", message); break; + case Log.ERROR : recordLog("E", message); break; + case Log.ASSERT: recordLog("A", message); break; default: /* 下記の優先度は出力なし ・Log.VERBOSE ・Log.DEBUG */ break; } } + private void recordLog(String prefix, String message) { + FirebaseCrashlytics.getInstance().log(String.format("%s: %s", prefix, message)); + } } ポイント どの優先度かを識別したいので、プレフィックスをつける関数を呼び出します recordLog 関数内の FirebaseCrashlytics.getInstance().log() を呼び出すことで、クラッシュレポートのログとして記録されます MainActivity.java MainActivity.java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Timber.i("メイン画面初期処理開始"); + Timber.d("デバッグログはコンソール出力のみ!"); Button crashButton = new Button(this); crashButton.setText("Test Crash"); crashButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { + Timber.e("Test Crash ボタンが押されました!"); throw new RuntimeException("Test Crash"); // Force a crash } }); addContentView(crashButton, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + Timber.i("メイン画面初期処理終了"); } } ポイント 初期処理の開始~終了のタイミングでINFOログ、 DEBUGログを出力します ボタンを押されたときにERRORログを出力します 動作確認 それぞれのビルド方法でクラッシュを発生させます。 デバッグビルド コンソール出力 AndroidStudio のコンソールには下記のように出力されます。 I: メイン画面初期処理開始 D: デバッグログはコンソール出力のみ! I: メイン画面初期処理終了 E: Test Crash ボタンが押されました! クラッシュレポート クラッシュレポートは送信されません。 リリースビルド コンソール出力 実装したログはコンソールに出力されません。 クラッシュレポート クラッシュレポートが送信されます。 また、「ログ」タブには実装したログが出力されています! そして ReleaseTree で実装した通り、デバッグログはクラッシュレポートには出力されていません? ※ #2 はデフォルトで出力 おわりに いかがだったでしょうか。 これで、開発中のコンソール上だけで確認したい内容は DEBUG 、開発中とクラッシュレポートどちらでも確認したい内容は INFO 以上という使い分けができますね! また、Firebase Crashlytics には、より分析しやすくするために下記のような機能もあります。 ユーザーIDを設定 任意のKeyValueをログの項目として保持 任意のタイミングでクラッシュレポートを送信 これらを扱うような Util クラスを作成してもよさそうですね。 以上、より良いログ基盤の構築の一助になりましたら幸いです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JetpackComposeのLazyRowでタップしたアイテムを中央にスクロールする

animateScrollBy()を使用してLazyRowの中央にアイテムスクロールさせる こんな感じの動き Eをタップ → Gをタップ → Eをタップ コード全体 @Composable fun ScrollList( horizontalPadding: Dp = 20.dp, horizontalItemSpace: Dp = 10.dp, itemWidth: Dp = 60.dp, ) { BoxWithConstraints( modifier = Modifier .fillMaxSize() .background(Color.Black), ) { val horizontalPaddingPx = horizontalPadding.toPx() val itemWidthPx = itemWidth.toPx() val items = listOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K") val boxWidth = LocalDensity.current.run { constraints.maxWidth } val state = rememberLazyListState() val coroutineScope = rememberCoroutineScope() LazyRow( state = state, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .align(Alignment.Center), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(horizontalItemSpace), contentPadding = PaddingValues(horizontal = horizontalPadding), ) { itemsIndexed(items = items) { index, item -> Text( modifier = Modifier .width(itemWidth) .padding(vertical = 20.dp) .background(Color.White) .clickable { coroutineScope.launch { val itemInfo = state.layoutInfo.visibleItemsInfo.find { // タップしたアイテムのLazyListItemInfoをindexをkeyにして取得 it.index == index } itemInfo?.let { // Boxの横方向の中央とアイテムの中央との差分だけscrollさせる state.animateScrollBy((it.offset + horizontalPaddingPx + (itemWidthPx / 2) - (boxWidth / 2))) } } }, textAlign = TextAlign.Center, fontSize = 20.sp, text = item, ) } } } } ポイント なぜanimateScrollByを使用したか animateScrollToItemでは指定したindexのitemをscrollのtop(今回のケースでは一番左)へスクロールする前提でそこからscrollOffsetでスクロール位置を調整することになります。 今回はマイナス方向(今回のケースでは右方向)へ調整したいためマイナスのoffsetとなりますが、animateScrollToItemでscrollOffsetに負数を渡すと以下のようなExceptionが発生しクラッシュするため今回はanimateScrollByを使用しています。 IllegalArgumentException: scrollOffset should be non-negative 解説 animateScrollByはsuspend functionなのでcoroutineScopeを使用しています .clickable { coroutineScope.launch { // スクロール処理 } }, animateScrollByに渡すスクロール量(value)は画面中央の位置からアイテムの中央の位置までの距離なので以下のような計算になりました。 今回のようにLazyRowにcontentPaddingが設定されている場合はその分も計算が必要になるのに気が付かずに微妙に中央にならないため少しハマりました。。 state.animateScrollBy((it.offset + horizontalPaddingPx + (itemWidthPx / 2) - (boxWidth / 2))) 内容と直接関係ないですが、DpをPxに変換しているtoPx()については以下で詳細を書いています JetpackComposeでDpからPxを簡単に計算する拡張関数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む