- val entryPoint = EntryPoints.get(activity, ActivityCreatorEntryPoint::class.java)
- val entryPoint = EntryPointAccessors.fromActivity<ActivityCreatorEntryPoint>(activity)
val entryPoint = EntryPoints.get(activity, ActivityCreatorEntryPoint::class.java)
EntryPointAccessors を使うと
val entryPoint = EntryPointAccessors.fromActivity<ActivityCreatorEntryPoint>(activity)
fromApplication(), fromActivity(), fromFragment(), fromView() が用意されている。
class Player {
@field:Json(name = "lucky number") val luckyNumber: Int
...
}
R8 / ProGuard をかけない場合は @Json でも動くのですが、R8 / ProGuard をかける場合は(ライブラリに含まれる keep 設定では) @field:Json にしないとこの指定が効かず、上記のコードだと実行時に luckyNumber にアクセスしたときに NullPointerException になります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
}
if (savedInstanceState == null) {
handleIntent(intent)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { handleIntent(it) }
}
...
}
class MainActivity : ComponentActivity() {
...
private fun handleIntent(intent: Intent) {
if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
return
}
// intent の内容に応じて NavHostController.navigate() する
}
}
Task が生きているときに Recent Apps からアプリを開いても FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY はつかない
@Preview
@Composable
fun SampleScreen() {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val bitmap = remember { mutableStateOf<Bitmap?>(null) }
Column {
Button(
onClick = {
bitmap.value = createBitmap(density, layoutDirection)
},
modifier = Modifier.padding(16.dp)
) {
Text(text = "click")
}
AndroidView(
factory = { context ->
ImageView(context)
},
update = { imageView ->
imageView.setImageBitmap(bitmap.value)
},
modifier = Modifier.padding(16.dp)
)
androidx.compose.foundation.Canvas(
modifier = Modifier.padding(16.dp)
.size(with(LocalDensity.current) { 512.toDp() })
) {
val bmp = bitmap.value
if (bmp != null) {
drawImage(bmp.asImageBitmap())
drawCircle(Color.White, radius = size.minDimension / 4f)
}
}
}
}
private fun createBitmap(
density: Density,
layoutDirection: LayoutDirection
): Bitmap {
val targetSize = 512
val imageBitmap = ImageBitmap(targetSize, targetSize)
val size = Size(targetSize.toFloat(), targetSize.toFloat())
CanvasDrawScope().draw(density, layoutDirection, Canvas(imageBitmap), size) {
drawCircle(Color.Red)
}
return imageBitmap.asAndroidBitmap()
}
ViewModel が Compose のライフサイクルと対応してないって言ってる正確な意味はわからないけど、SingleActivity + full Compose で、Compose で作った1画面を今までの Activity や Fragment のように使いたいなら navigation-compose 使うのがいいと思う。https://t.co/fYTMoZqTLm
— Yuki Anzai (@yanzm) August 26, 2021
class Screen1ViewModel : ViewModel() {
val values: List<Int> = (0 until 20).map { Random.nextInt() }
init {
println("Screen1ViewModel : created : $this")
}
override fun onCleared() {
println("Screen1ViewModel : cleared : $this")
}
}
class Screen2ViewModel : ViewModel() {
val value = Random.nextFloat()
init {
println("Screen2ViewModel : created : $this")
}
override fun onCleared() {
println("Screen2ViewModel : cleared : $this")
}
}
@Composable
fun Screen1(
viewModel: Screen1ViewModel,
onClickItem: (Int) -> Unit,
) {
DisposableEffect(Unit) {
println("Screen1 : composed : viewModel = $viewModel")
onDispose {
println("Screen1 : disposed")
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(viewModel.values) { value ->
Text(
text = value.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable {
onClickItem(value)
}
.padding(16.dp)
)
}
}
}
@Composable
fun Screen2(
viewModel: Screen2ViewModel,
value1: Int
) {
DisposableEffect(Unit) {
println("Screen2 : composed : viewModel = $viewModel")
onDispose {
println("Screen2 : disposed")
}
}
Text(
text = "value1 = $value1, value2 = ${viewModel.value}",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
MyApp()
}
}
}
}
}
@Composable
fun MyApp() {
val screenState = remember { mutableStateOf<Screen>(Screen.Screen1) }
when (val screen = screenState.value) {
Screen.Screen1 -> {
Screen1(viewModel()) {
screenState.value = Screen.Screen2(it)
}
}
is Screen.Screen2 -> {
BackHandler {
screenState.value = Screen.Screen1
}
Screen2(viewModel(), screen.value)
}
}
}
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@7826ac6
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@4d64aba
Screen1 : disposed
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
-- back キータップ
Screen2 : disposed
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : disposed
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
-- back キータップ
Screen2 : disposed
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@7826ac6
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@4d64aba
Screen1ViewModel、Screen2ViewModel いずれも1回しか生成されておらず、画面上の表示は Screen1 → Screen2 → Screen1 → Screen2 → Screen1 と遷移していますが、viewModel() では 同じ ViewModel インスタンスが返ってきていることがわかります。
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)
@Composable
public fun <VM : ViewModel> viewModel(
modelClass: Class<VM>,
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM = viewModelStoreOwner.get(modelClass, key, factory)
private fun <VM : ViewModel> ViewModelStoreOwner.get(
javaClass: Class<VM>,
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM {
val provider = if (factory != null) {
ViewModelProvider(this, factory)
} else {
ViewModelProvider(this)
}
return if (key != null) {
provider.get(key, javaClass)
} else {
provider.get(javaClass)
}
}
LocalViewModelStoreOwner.current から ViewModelStoreOwner を取得し、ViewModelProvider() にそれを渡して ViewModel のインスタンスを取得しています。
Screen.Screen1 -> {
println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen1(viewModel()) {
screenState.value = Screen.Screen2(it)
}
}
Screen1 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
...
Screen2 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
LocalViewModelStoreOwner には MainActivity が入っていることがわかりました。
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "Screen1") {
composable(
route = "Screen1"
) {
println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen1(viewModel()) {
navController.navigate("Screen2/$it")
}
}
composable(
route = "Screen2/{value}",
arguments = listOf(navArgument("value") { type = NavType.IntType })
) {
val value1 = requireNotNull(it.arguments).getInt("value")
println("Screen2 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen2(viewModel(), value1)
}
}
}
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : disposed
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
-- back キータップ
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : disposed
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : disposed
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
-- back キータップ
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : disposed
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
LocalViewModelStoreOwner には MainActivity ではなく NavBackStackEntry が入っていることがわかりました。
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
Screen1 : disposed
-- back キータップ = popBackstack
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : disposed
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen1 : disposed
-- back キータップ = popBackstack
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : disposed
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 → Screen2 → Screen1 のとき(Screen2 が backstack からいなくなった)に Screen2ViewModel が破棄(onCleared)され、再度 Screen2 に遷移したときに別の Screen2ViewModel インスタンスが作られていることがわかります。
@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
saveableStateHolder: SaveableStateHolder,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides this,
LocalLifecycleOwner provides this,
LocalSavedStateRegistryOwner provides this
) {
saveableStateHolder.SaveableStateProvider(content)
}
}
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
...
if (backStackEntry != null) {
...
Crossfade(backStackEntry.id, modifier) {
val lastEntry = transitionsInProgress.lastOrNull { entry ->
it == entry.id
} ?: backStack.lastOrNull { entry ->
it == entry.id
}
lastEntry?.LocalOwnersProvider(saveableStateHolder) {
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
}
...
}
}
...
}
backStackEntry には backstack の一番最後のものが入ります。
public class NavBackStackEntry private constructor(
...
private val viewModelStoreProvider: NavViewModelStoreProvider? = null,
...
) : LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner {
...
public override fun getViewModelStore(): ViewModelStore {
...
checkNotNull(viewModelStoreProvider) {
...
}
return viewModelStoreProvider.getViewModelStore(id)
}
}
NavBackStackEntry の ViewModelStoreOwner 実装では NavViewModelStoreProvider から ViewModelStore を取得しています。
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface NavViewModelStoreProvider {
public fun getViewModelStore(backStackEntryId: String): ViewModelStore
}
NavViewModelStoreProvider を実装しているのは NavControllerViewModel です。
internal class NavControllerViewModel : ViewModel(), NavViewModelStoreProvider {
private val viewModelStores = mutableMapOf<String, ViewModelStore>()
fun clear(backStackEntryId: String) {
// Clear and remove the NavGraph's ViewModelStore
val viewModelStore = viewModelStores.remove(backStackEntryId)
viewModelStore?.clear()
}
override fun onCleared() {
for (store in viewModelStores.values) {
store.clear()
}
viewModelStores.clear()
}
override fun getViewModelStore(backStackEntryId: String): ViewModelStore {
var viewModelStore = viewModelStores[backStackEntryId]
if (viewModelStore == null) {
viewModelStore = ViewModelStore()
viewModelStores[backStackEntryId] = viewModelStore
}
return viewModelStore
}
...
companion object {
private val FACTORY: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NavControllerViewModel() as T
}
}
@JvmStatic
fun getInstance(viewModelStore: ViewModelStore): NavControllerViewModel {
val viewModelProvider = ViewModelProvider(viewModelStore, FACTORY)
return viewModelProvider.get()
}
}
}
NavControllerViewModel は backStackEntryId: String と ViewModelStore の Map を持っています。
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
}
...
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
...
}
public open class NavHostController(context: Context) : NavController(context) {
...
public final override fun setViewModelStore(viewModelStore: ViewModelStore) {
super.setViewModelStore(viewModelStore)
}
}
public open class NavController(
...
) {
...
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open fun setViewModelStore(viewModelStore: ViewModelStore) {
if (viewModel == NavControllerViewModel.getInstance(viewModelStore)) {
return
}
check(backQueue.isEmpty()) { "ViewModelStore should be set before setGraph call" }
viewModel = NavControllerViewModel.getInstance(viewModelStore)
}
...
}
NavController にセットされた NavControllerViewModel は、NavBackStackEntry を生成するときに NavViewModelStoreProvider として渡されます。
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
MyApp()
}
}
}
}
}
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = "Screen1") {
composable("Screen1") {
Screen1(hiltViewModel()) {
navController.navigate("Screen2/$it")
}
}
composable(
route = "Screen2/{value}",
arguments = listOf(
navArgument("value") { type = NavType.IntType },
)
) {
val arguments = requireNotNull(it.arguments)
val value = arguments.getInt("value")
Screen2(value)
}
}
}
@Composable
fun Screen1(
viewModel: Screen1ViewModel,
onClickItem: (Int) -> Unit,
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(viewModel.values) { value ->
Text(
text = value.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable {
onClickItem(value)
}
.padding(16.dp)
)
}
}
}
@Composable
fun Screen2(value: Int) {
Text(
text = "value = $value",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
@HiltViewModel
class Screen1ViewModel @Inject constructor(
valueProvider: ValueProvider
) : ViewModel() {
val values: List<Int> = valueProvider.getValues()
}
interface ValueProvider {
fun getValues(): List<Int>
}
class DefaultValueProvider @Inject constructor() : ValueProvider {
override fun getValues(): List<Int> = (0 until 20).map { Random.nextInt() }
}
@Module
@InstallIn(SingletonComponent::class)
interface ValueProviderModule {
@Binds
@Singleton
fun bindValueProvider(provider: DefaultValueProvider): ValueProvider
}
dependencies {
...
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
}
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
build.gradle (app)
android {
...
defaultConfig {
...
testInstrumentationRunner "com.sample.myapplication.CustomTestRunner"
}
...
}
この設定をしないと
@AndroidEntryPoint
class HiltTestActivity : ComponentActivity()
app/src/debug/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sample.myapplication">
<application>
<activity
android:name=".HiltTestActivity"
android:exported="false" />
</application>
</manifest>
@UninstallModules(ValueProviderModule::class)
@HiltAndroidTest
class MyAppTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
@BindValue
val repository: ValueProvider = object : ValueProvider {
override fun getValues(): List<Int> {
return listOf(100)
}
}
@Test
fun test() {
composeTestRule.setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
MyApp()
}
}
}
// Screen1 に 100 が表示されている
composeTestRule.onNodeWithText("100").assertIsDisplayed()
// Screen1 の 100 が表示されている Node をクリック
composeTestRule.onNodeWithText("100").performClick()
// Screen2 に value = 100 が表示されている
composeTestRule.onNodeWithText("value = 100").assertIsDisplayed()
}
}
@Composable
inline fun <reified VM : ViewModel> assistedViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
crossinline viewModelProducer: (SavedStateHandle) -> VM
): VM {
val factory = if (viewModelStoreOwner is NavBackStackEntry) {
object : AbstractSavedStateViewModelFactory(viewModelStoreOwner, viewModelStoreOwner.arguments) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
return viewModelProducer(handle) as T
}
}
} else {
// Use the default factory provided by the ViewModelStoreOwner
// and assume it is an @AndroidEntryPoint annotated fragment or activity
null
}
return viewModel(viewModelStoreOwner, factory = factory)
}
fun Context.extractActivity(): Activity {
var ctx = this
while (ctx is ContextWrapper) {
if (ctx is Activity) {
return ctx
}
ctx = ctx.baseContext
}
throw IllegalStateException(
"Expected an activity context for creating a HiltViewModelFactory for a " +
"NavBackStackEntry but instead found: $ctx"
)
}
MainActivity.kt
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
MyApp()
}
}
}
}
}
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = "Screen1") {
composable("Screen1") {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(20) {
Text(
text = "Item : $it",
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("Screen2/$it")
}
.padding(16.dp)
)
}
}
}
composable(
route = "Screen2/{id}",
arguments = listOf(
navArgument("id") { type = NavType.IntType },
)
) {
val arguments = requireNotNull(it.arguments)
val id = arguments.getInt("id")
val viewModel = assistedViewModel { savedStateHandle ->
Screen2ViewModel.provideFactory(LocalContext.current)
.create(savedStateHandle, id)
}
Screen2(viewModel)
}
}
}
@Composable
fun Screen2(viewModel: Screen2ViewModel) {
Text(
text = viewModel.greet(),
modifier = Modifier.padding(24.dp)
)
}
class Screen2ViewModel @AssistedInject constructor(
private val nameProvider: NameProvider,
@Assisted private val savedStateHandle: SavedStateHandle,
@Assisted private val id: Int
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(savedStateHandle: SavedStateHandle, id: Int): Screen2ViewModel
}
@EntryPoint
@InstallIn(ActivityComponent::class)
interface ActivityCreatorEntryPoint {
fun getScreen2ViewModelFactory(): Factory
}
companion object {
fun provideFactory(context: Context): Factory {
val activity = context.extractActivity()
return EntryPoints.get(activity, ActivityCreatorEntryPoint::class.java)
.getScreen2ViewModelFactory()
}
}
fun greet(): String {
return "Hello ${nameProvider.name()} : id = $id"
}
}
@Singleton
class NameProvider @Inject constructor() {
fun name(): String {
return "Android"
}
}
Issue (https://github.com/google/dagger/issues/2287) は 1月からあるけど、進んでなさそう。
Canvas {
drawRoundRect(
color = color,
cornerRadius = radius,
style = Stroke(
width = strokeWidth,
pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(onInterval, offInterval),
phase = onInterval + offInterval,
)
)
)
}
@Stable
fun Modifier.weight(
weight: Float,
fill: Boolean = true
): Modifier
fill に true を指定すると、割り当てられた領域を占めるように配置されます。
@Preview
@Composable
fun Sample() {
Column(modifier = Modifier.height(300.dp)) {
Column(
modifier = Modifier.weight(1f, true)
) {
Text("タイトル")
Text("メッセージ")
}
Button(
modifier = Modifier.weight(1f),
onClick = { /*TODO*/ },
) {
Text("Button")
}
}
}
@Preview
@Composable
fun Sample2() {
Column(modifier = Modifier.height(300.dp)) {
Column(
modifier = Modifier.weight(1f, false)
) {
Text("タイトル")
Text("メッセージ")
}
Button(
modifier = Modifier.weight(1f),
onClick = { /*TODO*/ },
) {
Text("Button")
}
}
}
@Preview
@Composable
fun Sample3() {
Column(modifier = Modifier.height(300.dp)) {
Column(
modifier = Modifier
) {
Text("タイトル")
Text("メッセージ")
}
Button(
modifier = Modifier.weight(1f),
onClick = { /*TODO*/ },
) {
Text("Button")
}
}
}
Sample2 では fill に false を指定しています。これにより "タイトル" と "メッセージ" を含む Column は 150dp より小さくなります。Sample3 と違い、Button の大きさを計算するときに考慮されるため Button の大きさは 150dp になります。
implementation "com.google.accompanist:accompanist-insets:$accompanist_version"
2. Activity に android:windowSoftInputMode="adjustResize" をセットする
<activity
android:name=".RelocationRequesterSampleActivity"
android:windowSoftInputMode="adjustResize">
3. Activity で WindowCompat.setDecorFitsSystemWindows(window, false) する
class RelocationRequesterSampleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MaterialTheme {
ProvideWindowInsets {
RelocationRequesterSample()
}
}
}
}
}
4. RelocationRequester を使う
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RelocationRequesterSample() {
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
.padding(24.dp)
) {
Spacer(
modifier = Modifier
.fillMaxSize()
.height(600.dp)
.background(color = Color.LightGray)
)
Spacer(modifier = Modifier.height(24.dp))
val relocationRequester = remember { RelocationRequester() }
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && !ime.animationInProgress && isFocused) {
LaunchedEffect(Unit) {
relocationRequester.bringIntoView()
}
}
var value by remember { mutableStateOf("") }
OutlinedTextField(
value = value,
onValueChange = { value = it },
interactionSource = interactionSource,
modifier = Modifier.relocationRequester(relocationRequester)
)
}
}
ちなみに、 relocationRequester.bringIntoView() 部分をコメントアウトするとこうなる
val density = LocalDensity.current
Text(
text = "Hello",
fontSize = with(density) { 18.dp.toSp() },
)
enum class PermissionState {
Checking,
Granted,
Denied,
}
@Composable
private fun NeedPermissionScreen() {
var state by remember { mutableStateOf(PermissionState.Checking) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
state = if (it) PermissionState.Granted else PermissionState.Denied
}
val permission = Manifest.permission.CAMERA
val context = LocalContext.current
val lifecycleObserver = remember {
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
val result = context.checkSelfPermission(permission)
if (result != PackageManager.PERMISSION_GRANTED) {
state = PermissionState.Checking
launcher.launch(permission)
} else {
state = PermissionState.Granted
}
}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle, lifecycleObserver) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
when (state) {
PermissionState.Checking -> {
}
PermissionState.Granted -> {
// TODO パーミッションが必要な機能を使う画面
}
PermissionState.Denied -> {
// TODO 拒否された時の画面
}
}
}
SelectionContainer {
Text("This text is selectable")
}
選択ハンドルなどの色は MaterialTheme.colors.primary が使われます。
val original = MaterialTheme.colors
val textColor = original.primary
MaterialTheme(colors = original.copy(primary = original.secondary)) {
SelectionContainer {
Text("This text is selectable", color = textColor)
}
}
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.state.observe(this) {
when (it) {
is State.Data -> {
println(it.value)
}
State.Loading -> {
}
}
}
}
}
class MainViewModel : ViewModel() {
private val _state = MutableLiveData<State>()
val state: LiveData<State>
get() = _state
fun doSomething() {
val state = _state.value
if (state is State.Data) {
state.mutableValue = Random.nextInt()
}
}
}
sealed class State {
object Loading : State()
data class Data(val id: String) : State() {
// 変更できるのは MainViewModel からだけにしたいが、
// private にすると MainViewModel からも見えなくなる
var mutableValue: Int = -1
val value: Int
get() = mutableValue
}
}
↑ State.Data が持つ mutableValue は MainViewModel からのみ変更できるようにしたい。
private var mutableValue: Int = -1
これもダメ。
private sealed class State {
private data class Data(val id: String) : State() {
Data を top level にすると private をつけても MainViewModel から見えるけど、MainActivity から見えなくなるのでダメ。
sealed class State
object Loading : State()
// MainViewModel から mutableValue は見えるが
// Data が MainActivity からは見えなくなる
private data class Data(val id: String) : State() {
var mutableValue: Int = -1
val value: Int
get() = mutableValue
}
class MainViewModel : ViewModel() {
private val _state = MutableLiveData<State>()
val state: LiveData<State>
get() = _state
fun doSomething() {
val state = _state.value
if (state is DataImpl) {
state.mutableValue = Random.nextInt()
}
}
}
sealed interface State
object Loading : State
sealed interface Data : State {
val value: Int
}
private class DataImpl(val id: Int) : Data {
// private class なので変更できるのは同じファイルからだけ
var mutableValue: Int = id
override val value: Int
get() = mutableValue
}
@Composable
fun SampleScreen(
notificationEnabled: Boolean,
onCheckNotification: () -> Unit
) {
val lifecycleObserver = remember(onCheckNotification) {
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
onCheckNotification()
}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
if (!notificationEnabled) {
Text("通知設定がオフです")
}
}
state hoisting だとこんな感じだけど、引数 ViewModel にするならこんな感じ。
@Composable
fun SampleScreen(
viewModel: SampleViewModel = viewModel(),
) {
val lifecycleObserver = remember(viewModel) {
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
viewModel.updateNotificationEnabledState()
}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
if (!viewModel.notificationEnabled.value) {
Text("通知設定がオフです")
}
}
@Composable
fun Checkbox(
...
enabled: Boolean = true,
...
) {
...
}
Text とか Image には enabled 設定は用意されていないので自分でがんばる必要があります。
方法としては
val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
Column(
modifier = Modifier.padding(16.dp).alpha(alpha)
) {
Image(Icons.Default.Android, contentDescription = null)
Icon(Icons.Default.Home, contentDescription = null)
Text("Android")
}
val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides alpha) {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(Icons.Default.Android, contentDescription = null)
Icon(Icons.Default.Home, contentDescription = null)
Text("Android")
}
}
val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
Icons.Default.Android,
contentDescription = null,
alpha = alpha
)
Icon(
Icons.Default.Home,
contentDescription = null,
tint = MaterialTheme.colors.primary.copy(alpha = alpha)
)
Text(
"Android",
color = MaterialTheme.colors.primary.copy(alpha = alpha)
)
}
@Composable
fun AutoSizeableText(
text: String,
maxTextSize: Int = 16,
minTextSize: Int = 14,
modifier: Modifier
) {
var textSize by remember(text) { mutableStateOf(maxTextSize) }
Text(
text = text,
fontSize = textSize.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier,
onTextLayout = {
if (it.hasVisualOverflow && textSize > minTextSize) {
textSize -= 1
}
}
)
}
↑だと1文字増減したときに maxTextSize からやり直しになるので、現在の文字サイズを覚えておいてそこから +/- するようにしたのが ↓
@Composable
fun AutoSizeableText(
text: String,
maxTextSize: Int = 16,
minTextSize: Int = 14,
modifier: Modifier
) {
var textSize by remember { mutableStateOf(maxTextSize) }
val checked = remember(text) { mutableMapOf<Int, Boolean?>() }
var overflow by remember { mutableStateOf(TextOverflow.Clip) }
Text(
text = text,
fontSize = textSize.sp,
maxLines = 1,
overflow = overflow,
modifier = modifier,
onTextLayout = {
if (it.hasVisualOverflow) {
checked[textSize] = true
if (textSize > minTextSize) {
textSize -= 1
} else {
overflow = TextOverflow.Ellipsis
}
} else {
checked[textSize] = false
if (textSize < maxTextSize) {
if (checked[textSize + 1] == null) {
textSize += 1
}
}
}
}
)
}
それでも State が変わる回数が多い(maxTextSize と minTextSize の差が大きくて、一度に長い文字をペーストするとか)と文字サイズが変わるアニメーションみたいになってしまうのがつらいんですよね〜。
Text(
text = buildAnnotatedString {
append("By clicking blow you agree to our ")
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append("Terms of Use")
}
append(" and consent \nto our ")
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append("Privacy Policy")
}
append(".")
},
)
SpanStyle か ParagraphStyle を指定する。
class SpanStyle(
val color: Color = Color.Unspecified,
val fontSize: TextUnit = TextUnit.Unspecified,
val fontWeight: FontWeight? = null,
val fontStyle: FontStyle? = null,
val fontSynthesis: FontSynthesis? = null,
val fontFamily: FontFamily? = null,
val fontFeatureSettings: String? = null,
val letterSpacing: TextUnit = TextUnit.Unspecified,
val baselineShift: BaselineShift? = null,
val textGeometricTransform: TextGeometricTransform? = null,
val localeList: LocaleList? = null,
val background: Color = Color.Unspecified,
val textDecoration: TextDecoration? = null,
val shadow: Shadow? = null
) {
...
}
class ParagraphStyle constructor(
val textAlign: TextAlign? = null,
val textDirection: TextDirection? = null,
val lineHeight: TextUnit = TextUnit.Unspecified,
val textIndent: TextIndent? = null
) {
...
}
val focusManager = LocalFocusManager.current
Button(onClick = { focusManager.clearFocus() }) {
Text("Button")
}
@Serializable
inline class ItemId(val value: String)
@Serializable
data class Item(val id: ItemId, val name: String)
fun main() {
val item = Item(ItemId("1"), "Android")
val json = Json.encodeToString(item)
println(json)
println(Json.decodeFromString<Item>(json))
}
value class のときのコード
@Serializable
@JvmInline
value class ItemId(val value: String)
@Serializable
data class Item(val id: ItemId, val name: String)
fun main() {
val item = Item(ItemId("1"), "Android")
val json = Json.encodeToString(item)
println(json)
println(Json.decodeFromString<Item>(json))
}
@Composable
fun CircleProgress(
progress: Int,
modifier: Modifier,
colorProgress: Color = MaterialTheme.colors.primary,
colorBackground: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
.compositeOver(MaterialTheme.colors.surface),
strokeWidth: Dp = 8.dp,
) {
Canvas(modifier = modifier) {
val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
val diameter = min(size.width, size.height) - stroke.width
val topLeft = Offset((size.width - diameter) / 2, (size.height - diameter) / 2)
val circleSize = Size(diameter, diameter)
drawArc(
color = colorBackground,
startAngle = -90f,
sweepAngle = 360f,
useCenter = false,
style = stroke,
topLeft = topLeft,
size = circleSize,
)
drawArc(
color = colorProgress,
startAngle = -90f,
sweepAngle = 360f / 100 * progress,
useCenter = false,
style = stroke,
topLeft = topLeft,
size = circleSize,
)
}
}
@Preview
@Composable
fun CircleProgressPreview() {
var progress by remember { mutableStateOf(0) }
val animateProgress by animateIntAsState(targetValue = progress, animationSpec = tween())
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
Button(onClick = {
progress = Random.nextInt(0, 101)
}) {
Text("Change Progress")
}
Spacer(modifier = Modifier.height(16.dp))
CircleProgress(
progress = animateProgress,
modifier = Modifier.size(120.dp)
)
}
}
animate**AsState などでアニメーションも簡単にできます。
@Composable
fun IconToggleButton(
imageVector: ImageVector,
contentDescription: String?,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true
) {
CompositionLocalProvider(
LocalContentColor provides contentColor(enabled = enabled, checked = checked),
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.toggleable(
value = checked,
onValueChange = onCheckedChange,
role = Role.RadioButton,
)
.size(48.dp)
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(24.dp)
)
}
}
}
private fun Modifier.drawToggleButtonFrame(
...
): Modifier = this.drawWithContent {
...
// draw checked border
drawPath(
path = ...,
color = checkedBorderColor,
style = Stroke(strokeWidth),
)
drawContent()
}
var state by remember { mutableStateOf(ToggleableState.On) }
TriStateCheckbox(state = state, onClick = {
state = when (state) {
ToggleableState.On -> ToggleableState.Indeterminate
ToggleableState.Indeterminate -> ToggleableState.Off
ToggleableState.Off -> ToggleableState.On
}
})
var state by remember { mutableStateOf(ToggleableState.On) }
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.triStateToggleable(
state = state,
onClick = {
state = when (state) {
ToggleableState.On -> ToggleableState.Off
ToggleableState.Off -> ToggleableState.Indeterminate
ToggleableState.Indeterminate -> ToggleableState.On
}
},
role = Role.Checkbox,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = 24.dp
)
)
) {
Icon(
imageVector = when (state) {
ToggleableState.On -> Icons.Default.Favorite
ToggleableState.Off -> Icons.Default.FavoriteBorder
ToggleableState.Indeterminate -> Icons.Default.FavoriteBorder
},
contentDescription = when (state) {
ToggleableState.On -> "favorite on"
ToggleableState.Off -> "favorite on"
ToggleableState.Indeterminate -> "favorite indeterminate"
},
tint = when (state) {
ToggleableState.On -> MaterialTheme.colors.primary
ToggleableState.Off -> MaterialTheme.colors.primary
ToggleableState.Indeterminate -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
}
)
}
var checked by remember { mutableStateOf(false) }
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.toggleable(
value = checked,
onValueChange = { checked = it }
)
.size(48.dp)
) {
Icon(
imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = if (checked) "favorite on" else "favorite off",
)
}
Box + Modifier.toggleable() 部分は IconToggleButton として用意されているので、それを使うこともできます。
var checked by remember { mutableStateOf(false) }
IconToggleButton(
checked = checked,
onCheckedChange = { checked = it },
) {
Icon(
imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = if (checked) "favorite on" else "favorite off",
)
}
@Composable
fun DogList(list: List<Dog>) {
LazyColumn {
items(list.size + 1) {
if (it == 0) {
Header()
} else {
DogListItem(list[it - 1])
}
}
}
}
@Composable
fun DogList(list: List<Dog>) {
LazyColumn {
item { Header() }
items(list) { dog ->
DogListItem(dog)
}
}
}
また、itemsIndexed() を使うと index もとれるので、例えば dog.name の1文字目が変わったら区切りヘッダーを入れるというのもこんな感じで簡単に書けます(list は dog.name で sort されている前提)。
@Composable
fun DogList(list: List<Dog>) {
LazyColumn {
itemsIndexed(list) { index, dog ->
if (index == 0 || list[index - 1].name[0] != dog.name[0]) {
NameDivider(dog.name[0])
}
DogListItem(dog)
}
}
}
@Composable
fun Capsule() {
Text(
text = "Android",
modifier = Modifier
.padding(16.dp)
.background(
color = Color.LightGray,
shape = RoundedCornerShape(50)
)
.padding(vertical = 8.dp, horizontal = 16.dp)
)
}
BasicTextField(
...,
textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),
...
)
BasicTextField(
...,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
...
)
BasicTextField(
...,
textStyle = MaterialTheme.typography.body1.copy(textAlign = TextAlign.Center),
...
)
MDC の TextField と OutlinedTextField は innerTextField の位置が動かせないので、幅が innerTextField より大きい場合は左に寄った innerTextField の中で中央揃えになってしまいます。そのうち設定できるようになるのかもしれません。
@Composable
fun CenteringTextField() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
var text1 by remember { mutableStateOf("") }
TextField(
value = text1,
onValueChange = { text1 = it },
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
)
Spacer(modifier = Modifier.height(32.dp))
var text2 by remember { mutableStateOf("") }
OutlinedTextField(
value = text2,
onValueChange = { text2 = it },
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
)
Spacer(modifier = Modifier.height(32.dp))
var text3 by remember { mutableStateOf("") }
BasicTextField(
value = text3,
onValueChange = { text3 = it },
textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),
decorationBox = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(Color.LightGray)
.padding(16.dp)
) {
it()
}
}
)
Spacer(modifier = Modifier.height(32.dp))
var text4 by remember { mutableStateOf("") }
BasicTextField(
value = text4,
onValueChange = { text4 = it },
textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),
decorationBox = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(Color.LightGray)
.padding(16.dp)
) {
it()
}
},
modifier = Modifier.fillMaxWidth()
)
}
}
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.width(100.dp)
)
}
Spacer(Modifier.height(24.dp))
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.requiredWidth(100.dp)
)
}
Spacer(Modifier.height(24.dp))
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.wrapContentWidth()
)
}
Spacer(Modifier.height(24.dp))
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.wrapContentWidth(align = Alignment.Start, unbounded = true)
)
}
Spacer(Modifier.height(24.dp))
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.wrapContentWidth(align = Alignment.CenterHorizontally, unbounded = true)
)
}
Spacer(Modifier.height(24.dp))
Box(
Modifier
.size(50.dp)
.background(Color.LightGray)
) {
GrayText(
"Hello Android",
modifier = Modifier
.wrapContentWidth(align = Alignment.End, unbounded = true)
)
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context: Context = this
setContent {
MyAppTheme {
MyApp(context)
}
}
}
}
@Composable
fun MyApp(context:Context) {
TopScreen(context)
}
@Composable
fun TopScreen(context:Context) {
MyList(context)
}
@Composable
fun MyList(context:Context) {
// use context
}
この場合 Context を必要としているのは MyList ですが、MyList に Context を渡すために
private val MyLocalContext = staticCompositionLocalOf<Context> {
error("No current Context")
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context: Context = this
setContent {
MyAppTheme {
CompositionLocalProvider(MyLocalContext provides context) {
MyApp()
}
}
}
}
}
@Composable
fun MyApp() {
TopScreen()
}
@Composable
fun TopScreen() {
MyList()
}
@Composable
fun MyList() {
val context:Context = MyLocalContext.current
// use context
}
実は Context を受け渡す Composition Local は Jetpack Compose の方で用意されています。それが LocalContext です。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
MyApp()
}
}
}
}
@Composable
fun MyApp() {
TopScreen()
}
@Composable
fun TopScreen() {
MyList()
}
@Composable
fun MyList() {
val context:Context = LocalContext.current
// use context
}
private val MyLocalColor: ProvidableCompositionLocal<Color> = compositionLocalOf<Color> {
error("No current color")
}
staticCompositionLocalOf() および compositionLocalOf() に渡す lambda はデフォルト値のファクトリーです。値が与えられる前に取得しようとすると、このファクトリーが呼ばれます。
CompositionLocalProvider(MyLocalColor provides Color.Black) {
MyApp()
}
こうすると、CompositionLocalProvider の content lambda 内の Composable では ProvidableCompositionLocal.current で指定された値を取得することができます。
@Composable
fun MyApp() {
TopScreen()
}
@Composable
fun TopScreen() {
MyList()
}
@Composable
fun MyList() {
val color:Color = MyLocalColor.current // Color.Black
}
CompositionLocalProvider(MyLocalColor provides Color.Black) {
Column {
Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black
CompositionLocalProvider(MyLocalColor provides Color.Red) {
Text("Hello", color = MyLocalColor.current) // 文字色は Color.Red
}
}
}
providesDefault() ではすでに指定された値がある場合は上書きしません。
CompositionLocalProvider(MyLocalColor provides Color.Black) {
Column {
Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black
CompositionLocalProvider(MyLocalColor providesDefault Color.Red) {
Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black のまま
}
}
}
@Composable
@ReadOnlyComposable
private fun resources(): Resources {
LocalConfiguration.current
return LocalContext.current.resources
}
@Composable
fun Text(
text: AnnotatedString,
...
style: TextStyle = LocalTextStyle.current
) {
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
...
}
よって CompositionLocalProvider で LocalTextStyle を上書きすると、Text のデフォルトスタイルが変わります。
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h3) {
Text("Hello") // h3
}
@Composable
fun Surface(
...
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
...
content: @Composable () -> Unit
) {
...
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalAbsoluteElevation provides absoluteElevation
) {
Box(
...
) {
content()
}
}
}
Column(modifier = Modifier.padding(16.dp)) {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "favorite",
modifier = Modifier
.clickable { }
.padding(12.dp)
.size(24.dp)
)
Spacer(modifier = Modifier.height(48.dp))
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "favorite",
modifier = Modifier
.size(24.dp)
)
}
}
IconButton は ripple がいい感じです。まるいし。
fun Color.compositeOver(background: Color): Color
を使う。
val gray = Color(0xFF232323)
val white150 = Color.White.copy(alpha = 0.15f)
val compositeColor = white150.compositeOver(gray)
Card の背景は不透明じゃないと影が変になる。デフォルトでは Card の backgroundColor は MaterialTheme の surface なので、MaterialTheme の surface に半透明の色を指定しているときは、こんな感じで compositeOver で合成した色を backgroundColor に指定するとよい。
Card(
backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background),
) {
}
MaterialTheme(
colors = lightColors(
background = Color(0xFF232323),
surface = Color.White.copy(alpha = 0.15f)
)
) {
Surface(color = MaterialTheme.colors.background) {
Row(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxSize()
) {
Card(
modifier = Modifier
.padding(start = 8.dp, bottom = 8.dp)
.size(136.dp)
) {
// 左のカード
}
Card(
backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background),
modifier = Modifier
.padding(start = 8.dp, bottom = 8.dp)
.size(136.dp)
) {
// 右のカード
}
}
}
}
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(top = 16.dp)
) {
repeat(10) {
Card(
modifier = Modifier
.padding(
start = 8.dp,
// bottom padding がない
)
.size(136.dp)
) {
}
}
}
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(top = 16.dp)
) {
repeat(10) {
Card(
modifier = Modifier
.padding(
start = 8.dp,
bottom = 8.dp, // 追加
)
.size(136.dp)
) {
}
}
}
Modifier
.paddingFromBaseline(top = 24.dp, bottom = 16.dp)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.editText)
editText.filters = editText.filters + MyInputFilter()
}
}
class MyInputFilter : InputFilter {
override fun filter(
source: CharSequence,
start: Int,
end: Int,
dest: Spanned,
dstart: Int,
dend: Int
): CharSequence? {
var i: Int = start
while (i < end) {
if (source[i] == '\n') {
break
}
i++
}
if (i == end) {
return null
}
val filtered = SpannableStringBuilder(source, start, end)
val start2 = i - start
val end2 = end - start
for (j in end2 - 1 downTo start2) {
if (source[j] == '\n') {
filtered.delete(j, j + 1)
}
}
return filtered
}
}
plugins {
...
id "org.jetbrains.kotlin.plugin.serialization" version "1.4.31"
}
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
}
@Serializable
data class Dog(val name: String, val age: Int, val sex: Sex, val kind: Kind)
enum class Sex {
MALE,
FEMALE
}
@Serializable
sealed class Kind {
@Serializable
object Hybrid : Kind()
@Serializable
data class PureBlood(val name: String) : Kind()
}
class DogTest {
@Test
fun list() {
val dogs = listOf(
Dog("White", 10, Sex.MALE, Kind.Hybrid),
Dog("Black", 20, Sex.FEMALE, Kind.PureBlood("Husky"))
)
val json = Json.encodeToString(dogs)
println(json)
// [{"name":"White","age":10,"sex":"MALE","kind":{"type":"net.yanzm.serialize.Kind.Hybrid"}},{"name":"Black","age":20,"sex":"FEMALE","kind":{"type":"net.yanzm.serialize.Kind.PureBlood","name":"Husky"}}]
val decoded = Json.decodeFromString<List<Dog>>(json)
assertThat(decoded).isEqualTo(dogs)
}
}