20210730のAndroidに関する記事は3件です。

Accompanist のユースケース

2021/8/5 v0.16.0 で追加されたライブラリを追加しました Jetpack Compose を使うプロジェクトでは使うことになるであろう Accompanist とは何なのか、Accompanist は何ができるのかを書いていこうと思います。(v0.16.0 時点) Accompanist とは? Accompanist は、Jetpack Compose で一部の API を使いやすくしたり、Android View にある機能を Compose でも使えるようにしたライブラリのグループです。 Accompanist は複数あるライブラリのグループで、v0.16.0 時点で以下のライブラリがあります。 Library package Inset com.google.accompanist:accompanist-insets:<version>com.google.accompanist:accompanist-insets-ui:<version> System UI Controller com.google.accompanist:accompanist-systemuicontroller:<version> AppCompat Theme Adapter com.google.accompanist:accompanist-appcompat-theme:<version> Pager com.google.accompanist:accompanist-pager:<version>com.google.accompanist:accompanist-pager-indicators:<version> Permissions com.google.accompanist:accompanist-permissions:<version> Placeholder com.google.accompanist:accompanist-placeholder:<version>com.google.accompanist:accompanist-placeholder-material:<version> Flow Layouts com.google.accompanist:accompanist-flowlayout:<version> Navigation Animation com.google.accompanist:accompanist-navigation-animation:<version> Navigation Material com.google.accompanist:accompanist-navigation-material:<version> Drawable Painter com.google.accompanist:accompanist-drawablepainter:<version> Swipe to Refresh com.google.accompanist:accompanist-swiperefresh:<version> これらの中から必要なライブラリだけ選択してプロジェクトで使用することができます。 Accompanist のライブラリのユースケース Inset WindowInsets を Jetpack Compose で扱いやすくしたライブラリで Edge to Edge 対応で使用することになる また、Experimental ではあるものの IME Animation も対応できる Scaffold( topBar = { // We use TopAppBar from accompanist-insets-ui which allows us to provide // content padding matching the system bars insets. TopAppBar( title = { Text(stringResource(R.string.insets_title_list)) }, backgroundColor = MaterialTheme.colors.surface.copy(alpha = 0.9f), contentPadding = rememberInsetsPaddingValues( LocalWindowInsets.current.statusBars, applyBottom = false, ), ) }, bottomBar = { // We add a spacer as a bottom bar, which is the same height as // the navigation bar Spacer(Modifier.navigationBarsHeight().fillMaxWidth()) }, ) { contentPadding -> // We apply the contentPadding passed to us from the Scaffold Box(Modifier.padding(contentPadding)) { // content } } System UI Controller StatusBar や NavigationBar を制御しやすくしたライブラリ StatusBar や NavigationBar の色や表示・非表示を変更できる Android 8.1 を境目に StatusBar の色が自動で適用される val systemUiController = rememberSystemUiController() val useDarkIcons = MaterialTheme.colors.isLight SideEffect { // Update all of the system bar colors to be transparent, and use // dark icons if we're in light theme systemUiController.setSystemBarsColor( color = Color.Transparent, darkIcons = useDarkIcons ) // setStatusBarsColor() and setNavigationBarsColor() also exist } AppCompat Theme Adapter AppCompat の XML Theme を Jetpack Compose の Theme に変換するライブラリ Material Component の XML Theme を変換するライブラリは MDC-Android Compose Theme Adapter が提供されている AppCompat の Theme に定義されている色が以下のように MaterialTheme color に割り当てられる MaterialTheme color AppCompat attribute primary colorPrimary primaryVariant colorPrimaryDark onPrimary Calculated black/white secondary colorAccent secondaryVariant colorAccent onSecondary Calculated black/white surface Default onSurface android:textColorPrimary, else calculated black/white background android:colorBackground onBackground android:textColorPrimary, else calculated black/white error colorError onError Calculated black/white Pager Android View の ViewPager のようなレイアウトができるライブラリ 横向きだけでなく縦向きの表示や無限にスワイプもできる Tab 連携やインジケータのサポートまでされている val pagerState = rememberPagerState(pageCount = 10) HorizontalPager(state = pagerState) { page -> // Our page content Text( text = "Page: $page", modifier = Modifier.fillMaxWidth() ) } Permissions Permission を Jetpack Compose で扱いやすくしたライブラリ @Composable fun FeatureThatRequiresCameraPermission( navigateToSettingsScreen: () -> Unit ) { // Track if the user doesn't want to see the rationale any more. var doNotShowRationale by rememberSaveable { mutableStateOf(false) } val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) PermissionRequired( permissionState = cameraPermissionState, permissionNotGrantedContent = { if (doNotShowRationale) { Text("Feature not available") } else { Column { Text("The camera is important for this app. Please grant the permission.") Spacer(modifier = Modifier.height(8.dp)) Row { Button(onClick = { cameraPermissionState.launchPermissionRequest() }) { Text("Ok!") } Spacer(Modifier.width(8.dp)) Button(onClick = { doNotShowRationale = true }) { Text("Nope") } } } } }, permissionNotAvailableContent = { PermissionDenied(navigateToSettingsScreen) } ) { Text("Camera permission Granted") } } Placeholder コンテンツが読み込み中の Placeholder を表示できるライブラリ Text( text = "Content to display after content has loaded", modifier = Modifier .padding(16.dp) .placeholder(visible = true) ) Flow Layouts Jetpack Compose で Flexbox のようなレイアウトができるライブラリ FlowRow { // row contents } FlowColumn { // column contents } Navigation Animation Navigation Compose での遷移アニメーションを実装しやすくしたライブラリ val navController = rememberAnimatedNavController() AnimatedNavHost(navController, startDestination = "Blue") { composable( "Blue", enterTransition = { initial, _ -> when (initial.destination.route) { "Red" -> slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = tween(700)) else -> null } }, exitTransition = { _, target -> when (target.destination.route) { "Red" -> slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = tween(700)) else -> null } }, popEnterTransition = { initial, _ -> when (initial.destination.route) { "Red" -> slideInHorizontally(initialOffsetX = { -1000 }, animationSpec = tween(700)) else -> null } }, popExitTransition = { _, target -> when (target.destination.route) { "Red" -> slideOutHorizontally(targetOffsetX = { 1000 }, animationSpec = tween(700)) else -> null } } ) { BlueScreen(navController) } composable( "Red", ... ) } Navigation Material Navigation Compose で BottomSheet への遷移を実装できるライブラリ val navController = rememberNavController() val bottomSheetNavigator = rememberBottomSheetNavigator() navController.navigatorProvider += bottomSheetNavigator ModalBottomSheetLayout(bottomSheetNavigator) { NavHost(navController, Destinations.Home) { composable(route = "home") { ... } bottomSheet(route = "sheet") { Text("This is a cool bottom sheet!") } } } Drawable Painter Android の Drawable リソースを Painter として利用できるライブラリ val drawable = AppCompatResources.getDrawable(LocalContext.current, R.drawable.rectangle) Image( painter = rememberDrawablePainter(drawable = drawable), contentDescription = "content description", ) Swipe to Refresh Android View の SwipeRefreshLayout のような Pull to Refresh ができるライブラリ val viewModel: MyViewModel = viewModel() val isRefreshing by viewModel.isRefreshing.collectAsState() SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = { viewModel.refresh() }, ) { LazyColumn { items(30) { index -> // TODO: list items } } } ImageLoader coil を使用した画像読み込みも元々は Accompanist にあったが coil のライブラリで提供されるようになった https://google.github.io/accompanist/coil/ Image( painter = rememberImagePainter("https://www.example.com/image.jpg"), contentDescription = null, modifier = Modifier.size(128.dp) ) リンク https://github.com/google/accompanist https://google.github.io/accompanist/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

View.onSaveInstanceState()を簡単に実装する方法

概要 Android の onSaveInstanceState() 及び onRestoreInstanceState(Parcelable) を簡単に実装する方法のメモです。 例えば、foo, bar, hoge, fuga という4つのプロパティを保存したいなら以下のような感じになります。 override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(target) override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, target) private val target = listOf(::foo, ::bar, ::hoge, ::fuga) 方式 onSaveInstanceState() 及び onRestoreInstanceState(Parcelable) の処理を行う汎用的なコードに処理を移譲する方式になります。 実装方法概要 ◆ super を移譲する版(API26以上のみ可) // valueOrValueRetrievers は、保存したい情報の vararg もしくは List @SuppressLint("MissingSuperCall") override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(valueOrValueRetrievers) // restoreValuesは、保存したい情報の vararg もしくは List @SuppressLint("MissingSuperCall") override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, restoreValues) ◆ super を移譲しない版 override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(super.onSaveInstanceState(), valueOrValueRetrievers) override fun onRestoreInstanceState(state: Parcelable) = super.onRestoreInstanceState(restoreThisStateAndReturnSuperState(state, restoreValues)) 保存・復元対象の設定方法 ◆ 順序 保存・復元対象の設定は、valueOrValueRetrievers と restoreValues の部分に、保存と復元の情報を同じ順番で記述します。 ◆ 保存・復元対象のスコープ super を移譲する版の場合は public にする必要があります。 ※ inline 関数から呼ばれるため ◆ 保存・復元される情報の型 保存・復元対象の情報は、Parcelable に詰められるものに限定されます。 ◆ 保存・復元方法の指定 ☆ 保存方法の指定 保存情報の指定は、以下のいづれかで行います。 値 Function 型の値 KCallable 型の値 KProperty 型の値 ☆ 復元情報の指定 Function 型の値 KCallable 型の値 KMutableProperty 型の値 例 ◆ 3つのプロパティを扱う場合 例えば、以下の3つの情報の保存と復元を行う場合は、 var myString: String? = null var myInt: Int? = null var myChar: Char? = null シンプルなのは以下のような形です。 @SuppressLint("MissingSuperCall") override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(target) @SuppressLint("MissingSuperCall") override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, target) private val target get() = listOf(::myString, ::myInt, ::myChar) 以下のようにすることもできます。(する意味が分かりませんがw) @SuppressLint("MissingSuperCall") override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(myString, ::myInt, { myChar }) @SuppressLint("MissingSuperCall") override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, { it: String? -> myString = it }, ::myInt.setter, ::myChar) ◆ オーバーロード対応 TextView の text を指定するような場合、名前だけでは型が確定できないので、型を明確に指定する必要があります。 クリックすると時刻が表示されるTextViewの抜粋無しのソース package com.objectfanatics.chrono0022 import android.annotation.SuppressLint import android.content.Context import android.os.Parcelable import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import com.objectfanatics.commons.android.view.instancestate.onRestoreInstanceState import com.objectfanatics.commons.android.view.instancestate.onSaveInstanceState import java.text.SimpleDateFormat import java.util.* class ClickAndShowCurrentTimeView : AppCompatTextView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) init { setOnClickListener { text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.JAPAN).format(Date()) } } @SuppressLint("MissingSuperCall") override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(::getText as () -> CharSequence?) @SuppressLint("MissingSuperCall") override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, { it: CharSequence? -> text = it }) } 考察 結構限界までラクチンさを追求したつくりになっていると思います。 基本的には property を作ると思うので、::prop という形式だけで事足りると思います。 ◆ ちゃぶ台返し しかしながら、メッチャちゃぶ台返しなのですが、View 単位で状態を保存・復元する方式は基本的にバッドプラクティスだと思っています。理由は以下: id の付け忘れとかでバグが起きやすい。(idをつけないと保存・復元対象にならない) id をつけても、id が重複すると後勝ちになってしまう。また、そうなることを事前に防ぐ方法が無い。1 ViewModel を用いた保存・復元コードと、各 View 単位での保存・復元コードをすべて把握しながら保守するのは現実的ではない。 ViewModel での復元忘れがあっても View 単位で復元してしまうケースがかなりヤバい。ViewModel 側で復元を忘れているので ViewModel 内部で情報が失われているのだが View には一見正しい情報が表示されているという発見しにくいバグが発生する。当然ながら ViewModel 側の情報を取得すると null が返るのだが、『ViewModel の内容が View に bind されてるのになんだこの現象は!ViewModel のバグだ!!』という謎理論が展開され、ワークアラウンドが行われ、ViewModel のバグ、View 側の復元に気づかないという見落とし、さらにワークアラウンドでごまかすという負債の三重奏が鳴り響き、そして往々にしてコードを書いた人が転職した後に問題が発生し、そして保守者が死ぬ(合掌)。 kill からの復元は ViewModel を復元するだけにしたほうがシンプルで、実質これ以外の方法はまともに保守できる気がしない。2 ◆ ありがとう、そしてさようなら ということで、今後おいらはこの仕組みを使うことはおそらくないであろう。 ありがとう onSaveInstanceState() ... そしてさようなら (´・ω・`) 注意 今回の方法に関するコードについては、コンセプトの検証目的であり、あまり細かく検証していません。 必要が生じたらまともな品質に持っていこうかなと思います。3 付録1:ライブラリ的なコード @file:Suppress("unused") package com.objectfanatics.commons.android.view.instancestate import android.annotation.TargetApi import android.os.Parcelable import android.view.View import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType import kotlin.reflect.KCallable import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty // --------------------------------------------------------- // common for onSaveInstanceState and onRestoreInstanceState // --------------------------------------------------------- @Parcelize data class FrozenState( val superState: Parcelable?, val thisState: List<@RawValue Any?> ) : Parcelable // --------------------------------------------------------- // for onSaveInstanceState // --------------------------------------------------------- // using 'inline' since this function is caller sensitive. @TargetApi(26) inline fun <reified T> T.onSaveInstanceState(vararg valueOrValueRetrievers: Any?): Parcelable = onSaveInstanceState(valueOrValueRetrievers.toList()) // using 'inline' since this function is caller sensitive. @TargetApi(26) inline fun <reified T> T.onSaveInstanceState(valueOrValueRetrievers: List<Any?>): Parcelable = onSaveInstanceState(superOnSaveInstanceState(), valueOrValueRetrievers = valueOrValueRetrievers) fun onSaveInstanceState(superState: Parcelable?, vararg valueOrValueRetrievers: Any?): FrozenState = onSaveInstanceState(superState, valueOrValueRetrievers.toList()) fun onSaveInstanceState(superState: Parcelable?, valueOrValueRetrievers: List<Any?>): FrozenState = FrozenState(superState, valueOrValueRetrievers.map { getValue(it) }) // --------------------------------------------------------- // for onRestoreInstanceState // --------------------------------------------------------- // using 'inline' since this function is caller sensitive. @TargetApi(26) inline fun <reified T> T.onRestoreInstanceState(frozenState: Parcelable, vararg restoreValues: Any?) { onRestoreInstanceState(frozenState, restoreValues.toList()) } // using 'inline' since this function is caller sensitive. @TargetApi(26) inline fun <reified T> T.onRestoreInstanceState(frozenState: Parcelable, restoreValues: List<Any?>) = superOnRestoreInstanceState(restoreThisStateAndReturnSuperState(frozenState, restoreValues)) fun restoreThisStateAndReturnSuperState(frozenState: Parcelable, vararg restoreValues: Any?): Parcelable? { return restoreThisStateAndReturnSuperState(frozenState, restoreValues.toList()) } fun restoreThisStateAndReturnSuperState(frozenState: Parcelable, restoreValues: List<Any?>): Parcelable? { restoreValues.forEachIndexed { index, restoreValue -> setValue(restoreValue, (frozenState as FrozenState).thisState[index]) } return (frozenState as FrozenState).superState } // --------------------------------------------------------- // for calling super // --------------------------------------------------------- // Using 'inline' since 'MethodHandles.lookup()' is caller sensitive. @TargetApi(26) inline fun <reified T> T.superOnSaveInstanceState(): Parcelable { val methodName = "onSaveInstanceState" val methodType = MethodType.methodType(Parcelable::class.java) return superMethod<T>(methodName, methodType).invokeWithArguments(this) as Parcelable } // Using 'inline' since 'MethodHandles.lookup()' is caller sensitive. @TargetApi(26) inline fun <reified T> T.superOnRestoreInstanceState(state: Parcelable?) { val methodName = "onRestoreInstanceState" val methodType = MethodType.methodType(Void::class.javaPrimitiveType, Parcelable::class.java) superMethod<T>(methodName, methodType).invokeWithArguments(this, state) } @TargetApi(26) inline fun <reified T> superMethod(methodName: String, methodType: MethodType?): MethodHandle = MethodHandles.lookup().findSpecial( View::class.java, methodName, methodType, T::class.java ) // --------------------------------------------------------- // misc // --------------------------------------------------------- private fun getValue(valueOrValueRetriever: Any?): Any? = when (valueOrValueRetriever) { is KProperty<*> -> valueOrValueRetriever.getter.call() is KCallable<*> -> valueOrValueRetriever.call() is Function<*> -> valueOrValueRetriever::class.java.getMethod("invoke").invoke(valueOrValueRetriever) else -> valueOrValueRetriever } private fun setValue(restoreValue: Any?, value: Any?) = when (restoreValue) { is KMutableProperty<*> -> restoreValue.setter.call(value) is KCallable<*> -> restoreValue.call(value) is Function1<*, *> -> restoreValue::class.java.getMethod("invoke", Any::class.java).invoke(restoreValue, value) else -> throw IllegalArgumentException("restoreValue.javaClass = ${restoreValue?.javaClass?.name}") } 付録2:github git clone git@github.com:beyondseeker/chrono0022.git -b qiita_ver_0001; cd chrono0022 将来的にどのように使われるかなんてわからない。 ↩ ViewModel と SavedStateHandle を組み合わせたうえで、content view の isSaveFromParentEnabled を false にしちゃうのが最強な気がする。 ↩ しかしながら、ちゃぶ台返しで語ったように、今後 View 単位でのデータ復元をプロダクションに入れることはなさそうな気がする (´・ω・`) ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BoxFitでcoverやfitWidthを指定しているのに両サイドにスペースができる場合の対処法

結論 widthを設定しましょう Column(children: [ Expanded( child: SizedBox( child: Image.network( 'path', fit: BoxFit.cover, ), ), ), Expanded( child: SizedBox( width: MediaQuery.of(context).size.width, // この一行を追加 child: Image.network( 'path', fit: BoxFit.cover, ), ), ), ]); おめでとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む