Share
Embed
Jetpack Compose, having and not.
Jetpack Compose에 있는 것, 없는 것
드로이드나이츠 2021 행사에서 발표한 내용입니다.
https://sites.google.com/view/dk21/
Sungyong An
September 25, 2021
Transcript
86 slides
-
Timeline Stable! I/O '19 Early Preview '19 05. 11 Weeks
of Android Alpha '20 08. '21 05. I/O '21 '19 10. ADS '19 Beta -
mordern UI toolkit ٘ хࣗ ҙ declarative UI ъ۱ೠ ࢿמ
ѐߊ ࣘب ೱ࢚ Kotlin APIs -
mordern UI toolkit ٘ хࣗ ҙ declarative UI ъ۱ೠ ࢿמ
ѐߊ ࣘب ೱ࢚ Kotlin APIs ҕध ѐߊ ޙࢲীࢲח ৈ۞ ٜ աৌغয ݅, ѐߊ݃ ܲ ࢤпਸ ೞҊ ਸ Ѫ э. -
Әө ࢿ೧য়؍ XML ߑध UI ҳഅীࢲ Jetpack Compose۽ ߄۽
ജ೧ঠ ೞաਃ? 🤔 -
M V P M V VM M V I M
V C ӝઓ Jetpack ۄ࠳۞ܻ৬ ܰѱ Jetpack Composeח জ . -
02 01 03 ৵ Composeܳ ࢎਊ೧ঠ ೡө Composeী হח Ѫ
Compose۽ ജਸ ળ࠺ೞӝ ݾର -
୶ୌ ࢚ (ҕध) Understanding Compose AndroidDevSummit '19 I/O '19 Declarative
UI patterns Thinking in Compose 11 Weeks of Android Compose ӝࠄ ѐ֛ী ೧ࢲח ܖ ঋणפ. ҙ۲೧ࢲח ҕध ࢚ दਸ ୶ୌפ. -
୶ୌ ࢚ (ೠҴয) Jetpack Compose: I/O 2021 Ӓ റ I/O
Extended Korea Android 2021 I/O Extended with doubleS Compose दೞӝ ৢ೧ ೠҴয ࣁ࣌ب ਵפ, ҙब ਵन ٜ࠙ द೧ࠁࣁਃ. -
✅ ۖಬ ઙࣘࢿ ୭ࣗച Composeח Jetpack ۄ࠳۞ܻ۽ ߓನغয ۖಬী ؏
ઙࣘ. ۖಬ Viewীח APIܳ ࢎਊೡ ࣻ ח ୭ࣗ ߡ ઁড . ܳ ೧ࣗೞ۰Ҋ, Compat APIܳ ઁҕೞ݅ ೠ҅о . -
✅ ࢚ࣘਵ۽ ੋೠ ޙઁ ೧ࣗ public class TextView extends View
{} public class Button extends TextView {} @Composable fun Button(onClick: () -> Unit, ...) { ... } @Composable fun Text(text: String, ...) { ... } @Composable fun Sample() { Button(onClick = { ... }) { Text(text = "BUTTON") } } Viewח ߈٘द ࢚ࣘ೧ঠ݅ ೞҊ, Ӓېࢲ ਗ ঋח ࠗݽ ӝמب ыѱ ػ. Composeח composable ೣٜࣻ۽ ҳࢿغয, ਃೠ ӝמ݅ ѱ ਬೡ ࣻ . -
✅ ۨইਓ ࢿמ <RelativeLayout> <ImageView /> <ImageView /> <RelativeLayout>
<TextView /> <LinearLayout> <TextView /> <RelativeLayout> <EditText /> </RelativeLayout> </LinearLayout> <LinearLayout> <TextView /> <RelativeLayout> <EditText /> <ConstraintLayout> <ImageView /> <ImageView /> <TextView /> <EditText /> <TextView /> <TextView /> <EditText /> <Button /> <Button /> <TextView /> </ConstraintLayout> @Composable fun Sample() { Column { Image() Image() Column { Text() Column { Text() Box { TextField() } } Column { Text() Box { TextField() ViewGroupী ٮۄ measureܳ ৈ۞ߣ ࣻ೯ೡ ࣻ যࢲ ୭ೠ ۨইਓ غ ঋب۾ न҃ॄঠ ೮. Composeח measureܳ ೠߣ݅ ࣻ೯ೞب۾ ъઁغয ۨইਓীࢲب ࢿמ ѱ ೞغ ঋח. -
✅ UI ࢎਊࢿ <!-- titlebar.xml —> <merge> <FrameLayout> <ImageView />
</FrameLayout> </merge> <LinearLayout> <include layout="@layout/titlebar"/> <TextView /> ... </LinearLayout> @Composable fun TitleBar() { Box { Image() } } @Composable fun Sample() { Column { TitleBar() Text() ... } } Viewח includeܳ ਊೞৈ ۨইਓਸ ࢎਊೡ ࣻ . ࢿמ ޙઁ۽ mergeܳ Ҋ۰ೡ ࣻب . Composeח ࢎਊ ਃೠ ࠗ࠙ਸ ߹ѐ composable ೣࣻ۽ ܻ࠙ೞݶ ػ. -
✅ ো ୡӝച <FrameLayout> <ViewStub android:id="@+id/failedStub" android:layout_width="match_parent" android:layout_height="match_parent" android:inflatedId="@+id/failed" android:layout="@layout/loading_failed"
android:visibility="gone" /> </FrameLayout> // show error View failedStub = findViewById(R.id.failedStub); failedStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub stub, final View inflated) { ... } }); failedStub.inflate(); @Composable fun Sample() { if (showError) { Column { Text() Button() } } } 😱 ViewStub ࢎਊೞӝী ݒ ࠛಞೞ. Composeח ઑѤޙ ݅ਵ۽ب рױ ҳഅೡ ࣻ . -
✅ Single Source Of Truth public class TextView extends View
{ private @Nullable CharSequence mText; ... } public class EditText extends TextView {} viewModel.name.observe(this) { editText.setText(it) } editText.doOnTextChanged { text, _, _, _ -> viewModel.onNameChanged(text) ) @Composable fun Sample() { val viewModel: SampleViewModel = viewModel() val name by viewModel.name.observeAsState() TextField( value = name, onValueChange = { viewModel.onNameChanged(it) } ) } Viewח ۪؊݂ ݾਵ۽ ؘఠ৬ ߹ѐ ࢚కܳ ыҊ . Composeח ߹ѐ ࢚కܳ ы ঋח. class SampleViewModel : ViewModel() { val name: LiveData<String> = ... fun onNameChanged(name: String) { ... } } -
✅ ؘఠ ߄ੋ٬ @BindingAdapter("onTextChanged") fun EditText.onTextChanged(listener: (String) -> Unit){ doOnTextChanged
{ text, _, _, _ -> listener(text.toString()) } } <layout> <data> <variable name="viewModel" type="SampleViewModel" /> </data> <EditText android:id="@+id/name_edit_text" android:text="@{viewmodel.name}" android:onTextChanged="@{viewmodel.onNameChanged}" /> </layout> @Composable fun Sample() { val viewModel: SampleViewModel = viewModel() val name by viewModel.name.observeAsState() TextField( value = name, onValueChange = { viewModel.onNameChanged(it) } ) } Viewীࢲח ؘఠ ߄ੋ٬ ݾਵ۽ XMLҗ BindingAdapterܳ ࢿ೧ঠ ೠ. Composeח ࢚క݅ ೞݶ زਵ۽ ߄ੋ٬ػ. ߹ѐ composable ೣࣻ۽ ܻ࠙ೞݶ ػ. -
✅ Custom View ҳഅ <declare-styleable name="AppCompatImageView"> <attr name="srcCompat" format="reference" />
</declare-styleable> public class AppCompatImageView extends ImageView { public AppCompatImageView(...) { final int id = a.getResourceId( R.styleable.AppCompatImageView_srcCompat, -1); ... } } <androidx.appcompat.widget.AppCompatImageView app:srcCompat="@drawable/image" /> @Composable fun Image( painter: Painter, contentDescription: String?, modifier: Modifier = Modifier, ... ) { ... } @Composable fun Sample() { Image(painter = painterResource(R.drawable.image)) } Viewীࢲ Custom Viewܳ ٜ݅ ٸب XML ٘ܳ ࢿ೧ঠ ೠ. Composeח Kotlin ٘݅ ࢿೞݶ ػ. -
✅ ݾ۾ഋ UI ҳࢿ recyclerView.layoutManager = ... recyclerView.adapter = ...
// RecyclerView.Adapter(...) // RecyclerView.ViewHolder(@layout/item) recyclerView.addItemDecoration( DividerItemDecoration(...)) @Composable fun Sample(dataSet: Array<String>) { LazyColumn { items(dataSet.size) { index -> Text(text = dataSet[index]) Divider() } } } Viewীࢲח ݾ۾ഋ UIܳ ҳࢿೡ ٸ ցޖ ݆ ٘ܳ ࢿ೧ঠ ೠ. Composeח ܲ composable ೣࣻ৬ زੌೠ ߑߨਵ۽ औѱ ҳࢿೡ ࣻ . -
• Viewܳ ݽف ઁѢೞҊ Compose݅ ࢎਊೞݶ, APK ӝ৬ ࠽٘ दр
хࣗೠ. • Compose৬ Viewܳ ೣԋ ॳݶ, APK ӝ৬ ࠽٘ दр ডр ૐоೠ. • Composeীࢲب Viewܳ ࢎਊೡ ࣻ . (AndroidView) • Java৬ XML۽ ࢿػ ۨইਓ ٘ܳ Kotlinਵ۽ ജೞח ࠺ਊ • View ਬࠁࣻ ࠺ਊ vs Compose ण ࠺ਊ • View৬ ࠺तೞѱ AOT ஹੌਸ ਗೠ. (ProfileInstaller) ⚠ ࠺Үೞӝী ઑӘ গݒೠ ٜࠗ࠙ -
• min sdk ઁডࢎ೦ . (API 21+) • (Java/Kotlinҗ ׳ܻ)
View৬ Composeח ࢲ۽ ߸ജغ ঋח. • Material Theme݅ ӝࠄਵ۽ ਗೠ. • ۨইਓ XML ࠺ Preview ࢿמ ڄযઉࢲ ࢤࢿ ڄয. • ۨইਓ XMLਸ ࢎਊೡ ٸࠁ ܻ࠭ೡ ٘о טযդ. • ਗೞח 3rd party ۄ࠳۞ܻо . ❌ Viewࠁ Composeীࢲ ইए Ѫ? -
⚠ Theme <activity android:theme="@style/Theme.Sample"> <style name="Theme.Sample" parent="Theme.MaterialComponents"> <item name="colorPrimary">@color/red</item> </style>
Theme.AppCompat android:Theme.DeviceDefault SampleTheme { … } @Composable fun SampleTheme(content: @Composable() () -> Unit) { MaterialTheme( colors = lightColors(primary = Red, ...), typography = Typography, shapes = Shapes, content = content ) } // X // X ӒܻҊ ӝઓ ۽ંী Composeܳ ਊೞݶ Themeо ف ߥ ػ. (XML, Kotlin) -
✅ Theme: MDC-Android Compose Theme Adapter Link: https://github.com/material-components/material-components-android-compose-theme-adapter @Composable fun
SampleTheme(content: @Composable() () -> Unit) { MdcTheme(content = content) } LocalContext.current themeܳ ӝ߈ਵ۽ MaterialTheme composable ࣘࢿਸ ҳࢿ೧ળ. -
⚠ Theme: AppCompat Compose Theme Adapter Link: https://github.com/google/accompanist/tree/main/appcompat-theme AppCompat Themeܳ
ࢎਊೞҊ ਸ ٸ, ࢎਊೡ ࣻ ח ۄ࠳۞ܻ. (Accompanist) MaterialTheme composable ࣘࢿਸ ҳࢿ೧ળ. ೞ݅ MDC Themeо ؊ ݆ ࣘࢿ ыҊ . @Composable fun SampleTheme(content: @Composable() () -> Unit) { AppCompatTheme(content = content) } -
Custom Theme Link: https://github.com/android/compose-samples/tree/main/Jetsnack ӝࠄ Themeী ৈ۞о ࣘࢿਸ ୶оೞח ١
߹ب ٣ੋ दझమਸ ҳ୷ೞҊ ݶ, Custom Themeܳ ҳഅೞח Ѫب Ҋ۰೧ࠅ ࣻ . ݅ compose-materialী ח ݆ࣻ ஹನքٜਸ Ӓ۽ ࢎਊೞӝ য۰ਕ ࣻ . पઁ ҳഅೞח ߑߨ উ٘۽٘ ҕध ࢠ ۽ં Jetsnackਸ ଵҊ೧ࠁ. -
⚠ Resources context.getString(R.string.message) context.resources.getDimensionPixelSize(R.dimen.size) ContextCompat.getColor(context, R.color.red) ContextCompat.getColorStateList(context, R.color.red) ContextCompat.getDrawable(context, ...)
R.drawable.bitmap R.drawable.vector R.drawable.shape // <shape> R.drawable.selector // <selector> R.drawable.asld // <animated-selector> R.drawable.nine_patch // 9-patch context.resources.getInteger(R.integer.value) context.resources.getBoolean(R.bool.value) stringResource(R.string.message) dimensionResource(R.dimen.size) colorResource(R.color.black) // X painterResource(R.drawable.bitmap) painterResource(R.drawable.vector) // X // X // X, b/168848337 // X, b/193907127 integerResource(R.integer.value) booleanResource(R.bool.value) -
ImageView TextView Button ImageButton RecyclerView EditText ProgressBar SeekBar Space CheckBox
WebView LinearLayout FrameLayout RelativeLayout CardView ConstraintLayout ScrollView ListView GridView TableLayout View -
View ImageView TextView Button ImageButton RecyclerView EditText ProgressBar SeekBar Space
CheckBox WebView LinearLayout FrameLayout RelativeLayout CardView ConstraintLayout ScrollView ListView GridView TableLayout Widget -
View Compose Image Button + Text Button ImageView Slider CheckBox
CircularProgressIndicator Text TextView ❌ ProgressBar SeekBar Spacer Space ❌ TextureView ❌ SurfaceView WebView IconButton + Image ImageButton EditText TextField View Compose CheckedTextView Checkbox + Text IconToggleButton - LinearProgressIndicator - ❌ Switch Switch + Text RadioButton RadioButton + Text ❌ ViewStub -
⚠ View <View android:layout_width="match_parent" android:layout_height="50dp" android:layout_margin="4dp" android:padding="8dp" android:background="@color/red" android:foreground="@color/blue" android:clipToOutline="true"
android:elevation="4dp" android:contentDescription="view" android:visibility="invisible" /> Box(Modifier .fillMaxWidth() .height(50.dp) // X, paddingਵ۽ .padding(8.dp) .background(Color.Red) // X, Box۽ .clipToBounds() .graphicsLayer(shadowElevation = with(LocalDensity.current) { 5.dp.toPx() }) .semantics { contentDescription = "view" } // X, b/158837937 ) -
✅ View: Listener view.setOnClickListener { ... } view.setOnLongClickListener { ...
} view.setOnDragListener { view, dragEvent -> ... } view.setOnTouchListener { view, motionEvent -> ... } view.setOnKeyListener { view, i, keyEvent -> ... } Box(Modifier.clickable { ... }) Box(Modifier.combinedClickable( // experimental onClick = { ... }, onLongClick = { ... }) ) Box(Modifier.pointerInput(key) { detectDragGestures { change, dragAmount -> ... }} ) Box(Modifier.pointerInteropFilter( // experimental onTouchEvent = { motionEvent -> ... } )) Box(Modifier.onKeyEvent( onKeyEvent = { keyEvent -> ... } )) -
⚠ TextView <TextView android:text="text" android:textColor="@color/red" android:textSize="12sp" android:textStyle="bold" android:fontFamily="sans-serif" android:maxLines="1" android:ellipsize="end"
android:autoSizeTextType="uniform" android:includeFontPadding="false" android:autofillHints="emailAddress" android:drawableStart="@drawable/icon" android:textIsSelectable="true" /> <androidx.emoji.widget.EmojiTextView /> Text( text = "text", color = Color.Red, fontSize = 12.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.SansSerif, maxLines = 1, overflow = TextOverflow.Ellipsis, // X // X, b/171394808 // X, b/176949051, Unofficial Blog ଵҊ ) Row { Icon(painterResource(id = R.drawable.icon)) Text("text") } SelectionContainer { Text("text") } // X, b/139326806 -
✅ EditText editText.doOnTextChanged { text, start, count, after -> …
} <EditText android:hint="hint" android:inputType="number" android:imeOptions="actionDone" /> editText.setOnEditorActionListener { textView, i, keyEvent -> … } TextField( value = text, onValueChange = { text -> ... }, placeholder = { Text("hint") }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Done ), keyboardActions = KeyboardActions(onDone = { ... }), ) -
✅ Button <Button android:text="text" /> @style/Widget.MaterialComponents.Button" @style/Widget.MaterialComponents.Button.UnelevatedButton" @style/Widget.MaterialComponents.Button.OutlinedButton" @style/Widget.MaterialComponents.Button.TextButton" Button(onClick
= { ... }) { Text("text") } Button {} Button(elevation = null) {} OutlinedButton {} TextButton {} -
⚠ ImageView <ImageView android:src="@drawable/icon" android:tint="@color/red" android:scaleType="centerInside" android:adjustViewBounds="true" /> Image( painter
= painterResource(id = R.drawable.icon), colorFilter = ColorFilter.tint(Color.Red), contentScale = ContentScale.Inside, // X, b/196358649 ) -
✅ ImageButton <!-- res/drawable/selector_icon.xml --> <selector> <item android:state_pressed="true" android:drawable="@drawable/pressed" />
<item android:drawable="@drawable/normal" /> </selector> <ImageButton android:src="@drawable/selector_icon" /> val source = remember { MutableInteractionSource() } val pressed by source.collectIsPressedAsState() val iconResId = if (pressed) { R.drawable.pressed } else { R.drawable.normal } IconButton( onClick = { ... }, interactionSource = source ) { Icon(painterResource(id = iconResId)) } -
✅ CheckBox <CheckBox android:checked="true" android:buttonTint="@color/red" /> checkBox.setOnCheckedChangeListener { compoundButton, checked
-> ... } Checkbox( checked = true, colors = CheckboxDefaults.colors( checkedColor = Color.Red ), onCheckedChange = { checked -> ... }, ) -
⚠ ProgressBar <ProgressBar style="@style/Widget.AppCompat.ProgressBar" android:indeterminate="true" /> <ProgressBar style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:indeterminate="true" />
<ProgressBar style="@style/Widget.AppCompat.ProgressBar.Horizontal" /> android:min="0" android:max="100" android:progress="1" android:secondaryProgress="10" /> CircularProgressIndicator() LinearProgressIndicator() LinearProgressIndicator( // min: 0f // max: 1f progress = 0.01f, // X ) -
⚠ SeekBar <SeekBar android:min="0" android:max="100" android:progress="1" android:secondaryProgress="10" android:thumb="@drawable/thumb" /> seekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged( seekBar: SeekBar, progress: Int, fromUser: Boolean ) { ... } }) Slider( valueRange = 0f..100f, steps = 1, value = 1f, // X // X, b/195042785 onValueChange = { offset -> ... }, ) -
⚠ Custom View: Canvas class CanvasView(...) : View(...) { override
fun draw(canvas: Canvas) { super.draw(canvas) canvas.withTranslation(x = width / 5f) { withRotation(degrees = 45F) { drawRect(...) } } drawable.draw(canvas) } } Canvas(modifier = Modifier.fillMaxSize()) { val canvasSize = size val canvasWidth = size.width val canvasHeight = size.height withTransform({ translate(left = canvasWidth / 5F) rotate(degrees = 45F) }) { drawRect(...) } drawIntoCanvas { it -> drawable.draw(it.nativeCanvas) } } -
View ImageView TextView Button ImageButton RecyclerView EditText ProgressBar SeekBar Space
CheckBox WebView LinearLayout FrameLayout RelativeLayout CardView ConstraintLayout ScrollView ListView GridView TableLayout ViewGroup -
Column LazyColumn RecyclerView LinearLayout ModalDrawer ViewPager Modifier.scrollable() ScrollView ConstraintLayout BottomNavigationView
❌ RelativeLayout LazyRow Toolbar Box FrameLayout DrawerLayout ❌ AppBar TopAppBar Row ConstraintLayout View Compose View Compose ❌ - - BottomNavigation (BottomSheet) Scaffold CoordinatorLayout SwipeRefreshLayout ❌ b/194911951 ❌ -
✅ FrameLayout <FrameLayout> <ImageView /> <TextView android:text="text" android:layout_gravity="center" /> </FrameLayout>
Box { Image() Text(text = "text", modifier = Modifier.align(Alignment.Center)) } Box(contentAlignment = Alignment.Center) { ... } -
✅ LinearLayout <LinearLayout android:orientation="vertical" android:gravity="center"> <ImageView android:layout_weight="1" /> <TextView />
</LinearLayout> <LinearLayout android:orientation="horizontal" android:gravity="center"> <ImageView /> <TextView /> </LinearLayout> Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Image(modifier = Modifier.weight(1f)) Text() } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Image() Text() } -
✅ ScrollView <ScrollView> <LinearLayout android:orientation="vertical"/> </ScrollView> <HorizontalScrollView> <LinearLayout android:orientation="horizontal"/> </HorizontalScrollView>
Column( Modifier.verticalScroll(rememberScrollState()) ) { ... } Row( Modifier.horizontalScroll(rememberScrollState()) ) { ... } -
⚠ RecyclerView val adapter = CustomAdapter(...) recyclerView.adapter = adapter adapter.submitList(dataSet)
recyclerView.addItemDecoration( DividerItemDecoration(...)) recyclerView.itemAnimator = ... app:fastScrollEnabled="true" @Composable fun Sample(dataSet: Array<String>) { LazyColumn { items(dataSet.size) { index -> Text(text = dataSet[index]) Divider() } } } // X, b/188855907, Divider۽ оמ // X, b/150812265 // X, b/182393097 -
⚠ RecyclerView: LayoutManager <androidx.recyclerview.widget.RecyclerView android:orientation="vertical" app:layoutManager="...LinearLayoutManager" app:reverseLayout="false" /> android:orientation="horizontal" app:layoutManager="...LinearLayoutManager"
/> android:orientation="vertical" app:layoutManager="...GridLayoutManager" app:spanCount="3" /> android:orientation="horizontal" app:layoutManager="...GridLayoutManager" /> android:orientation="vertical" app:layoutManager="...StaggeredGridLayoutManager" /> LazyColumn( reverseLayout = false ) { ... } LazyRow { ... } LazyVerticalGrid( // experimental cells = GridCells.Fixed(3) ) { ... } // X, b/191238807 // X, b/190787900 -
✅ Card <androidx.cardview.widget.CardView app:cardCornerRadius="4dp" app:cardBackgroundColor="@color/white" app:cardElevation="8dp"> ... </androidx.cardview.widget.CardView> Card( shape
= RoundedCornerShape(4.dp), backgroundColor = Color.White, elevation = 8.dp ) { ... } -
✅ CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout> <AppBarLayout /> <LinearLayout app:layout_behavior= "@string/appbar_scrolling_view_behavior"/> <BottomNavigationView />
<FloatingActionButton /> </androidx.coordinatorlayout.widget.CoordinatorLayout> Scaffold( topBar = { TopAppBar { ... } }, content = { }, bottomBar = { BottomNavigation(...) }, floatingActionButton = { FloatingActionButton { ... } } ) // X, b/194911953 Tooltip -
❌ Toast Toast.makeText( context, "message", Toast.LENGTH_SHORT ).show() // X, ইېۢ
ࢎਊ оמ Toast.makeText( LocalContext.current, "message", Toast.LENGTH_SHORT ).show() -
✅ Snackbar Snackbar .make( view, "Snackbar Message", Snackbar.LENGTH_SHORT ) .setAction("Action")
{ /* onActionPerformed */ } .addCallback(object : Snackbar.Callback() { override fun onDismissed( transientBottomBar: Snackbar, event: Int ) { ... } }) .show() val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { Button(onClick = { scope.launch { val result = scaffoldState.snackbarHostState .showSnackbar( message = "Snackbar Message", actionLabel = "Action" ) when (result) { SnackbarResult.ActionPerformed -> { ... } SnackbarResult.Dismissed -> { ... } } } }) { ... } } -
⚠ Dialog AlertDialog.Builder(context) .setTitle("title") .setMessage("message") .setPositiveButton("") { ... } .setNegativeButton("")
{ ... } .show() // Additional Functions var isDialogShown by remember { mutableStateOf(true) } if (isDialogShown) { AlertDialog( title = { Text("title") }, text = { Text("message") }, confirmButton = { TextButton(onClick = {}) { ... }}, dismissButton = { TextButton(onClick = {}) { ... }}, onDismissRequest = { isDialogShown = false }, ) } // X, b/194911954 -
❌ Fragment override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(KEY, value)
} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) value = savedInstanceState?.getString(KEY) ?: viewModel.generateValue() } // Multiple back stacks fragmentManager.saveBackStack("notifications") fragmentManager.restoreBackStack("profile") val value = rememberSaveable { // Null X, b/196995035 mutableStateOf(viewModel.generateValue()) } val current = ... // "notifications" -> "profile" val saveableStateHolder = rememberSaveableStateHolder() saveableStateHolder.SaveableStateProvider(current.route) { when (current) { Profile -> Profile() Notifications -> Notifications() } } -
⚠ Interpolator interface TimeInterpolator FastOutSlowInInterpolator() FastOutLinearInInterpolator() LinearOutSlowInInterpolator() LinearInterpolator() PathInterpolator(0.33f, 0f)
// Quad PathInterpolator(0.33f, 0f, 1f, 1f) // Cubic PathInterpolator((Path().apply {...}) // Path AccelerateInterpolator() OvershootInterpolator() ... interface Easing FastOutSlowInEasing FastOutLinearInEasing LinearOutSlowInEasing LinearEasing // X, b/168854254 CubicBezierEasing(0.33f, 0f, 1f, 1f) // X, b/168854254 // X // X // ... -
✅ ViewPropertyAnimator val fraction: Float = if (enabled) 1f else
0f view.animate().cancel() view.animate() .setStartDelay(300L) // ms .setDuration(1000L) // ms .setInterpolator(FastOutSlowInInterpolator()) .translationX(lerp(0f, 100f, fraction)) .translationY(lerp(0f, 50f, fraction)) .rotationX(lerp(0f, 90f, fraction)) .rotationY(lerp(0f, 90f, fraction)) .scaleX(lerp(1f, 1.5f, fraction)) .scaleY(lerp(1f, 0.5f, fraction)) .alpha(lerp(1f, 0f, fraction)) .withLayer() val fraction: Float by animateFloatAsState( targetValue = if (enabled) 1f else 0f, animationSpec = tween( delayMillis = 300, durationMillis = 1000, easing = FastOutSlowInEasing)) Box(modifier = Modifier.graphicsLayer { translationX = lerp(0f, 100f, fraction) translationY = lerp(0f, 50f, fraction) rotationX = lerp(0f, 90f, fraction) rotationY = lerp(0f, 90f, fraction) scaleX = lerp(1f, 1.5f, fraction) scaleY = lerp(1f, 0.5f, fraction) alpha = lerp(1f, 0f, fraction) // ೞ٘ਝয оࣘ ӝࠄ, b/193123882 }) -
✅ DynamicAnimation val spring = SpringAnimation( view, DynamicAnimation.TRANSLATION_X ).withSpringForceProperties {
dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY stiffness = SpringForce.STIFFNESS_LOW } val targetValue = if (enabled) 100f else 0f spring.animateToFinalPosition(targetValue) val fraction: Float by animateFloatAsState( targetValue = if (enabled) 1f else 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow ) ) Box(modifier = Modifier.graphicsLayer { translationX = lerp(0f, 100f, fraction) }) -
✅ ValueAnimator: Infinite Animation ValueAnimator.ofArgb( Color.RED, Color.GREEN ).apply { repeatCount
= ValueAnimator.INFINITE repeatMode = ValueAnimator.REVERSE duration = 1_000L interpolator = LinearInterpolator() addUpdateListener { val color = it.animatedValue as Int view.setBackgroundColor(color) } }.start() val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( repeatMode = RepeatMode.Reverse animation = tween( durationMillis = 1000, easing = LinearEasing), ) ) Box(Modifier.background(color)) -
⚠ Transition // FragmentA.kt override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
exitTransition = MaterialFadeThrough() } // FragmentB.kt override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enterTransition = MaterialFadeThrough() } supportFragmentManager .beginTransaction() .replace(R.id.fragment_container, FragmentB()) .commit() val screen = mutableStateOf("A") AnimatedContent( targetState = screen, transitionSpec = { fadeIn() + scaleIn() with // 1.1ࠗఠ, b/191325593 fadeOut() } ) { currentScreen -> when (currentScreen) { "A" -> ScreenA() "B" -> ScreenB() } } screen.value = "B" -
⚠ Transition // ViewGroup Child View ݽفী ਊػ TransitionManager.beginDelayedTransition( viewGroup,
AutoTransition()) child.isVisible = show AutoTransition = <transitionSet android:transitionOrdering="sequential"> <fade android:fadingMode="fade_out" /> <changeBounds /> <fade android:fadingMode="fade_in" /> </transitionSet> Box { // пп ѐ߹ਵ۽ ਊ೧ঠ ೠ AnimatedVisibility( visible = show, enter = fadeIn() + expandIn(), // EnterTransition exit = shrinkOut() + fadeOut() // ExitTransition ) { Text(text = "child") } } // X, b/142451118 -
❌ View Animation fragmentManager.beginTransaction() .setCustomAnimations( R.anim.slide_in_right, // enterAnim R.anim.slide_out_left, //
exitAnim ) .replace(...) .commit() <set> <alpha /> <translate /> <scale /> <rotate /> </set> // X // X // X // X // X -
❌ Shared Elements fragmentManager.beginTransaction() .addSharedElement(view, "transitionName") .replace(...) .setReorderingAllowed(true) .commit() view.transitionName
= "transition_name" class FragmentB : Fragment() { override fun onCreate(...) { super.onCreate(savedInstanceState) sharedElementEnterTransition = ... sharedElementReturnTransition = ... } } // X -
Jetpack ۄ࠳۞ܻ ✅ ❌ Activity ViewPager ViewPager2 Preference Navigation Hilt
ViewModel Paging Emoji ConstraintLayout Wear Link: https://developer.android.com/jetpack/compose/libraries -
Jetpack ۄ࠳۞ܻ: ConstraintLayout ConstraintLayout { val (button, text) = createRefs()
Button( onClick = {}, modifier = Modifier.constrainAs(button) { top.linkTo(parent.top, margin = 16.dp) } ) { Text("Button") } Text("Text", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 16.dp) }) } Link: https://developer.android.com/jetpack/compose/layouts/constraintlayout ConstraintLayout ۨইਓ ਵ۽ ੋೠ ࢿמ ೞܳ ѐࢶೞ۰ח ݾਵ۽ ୶оغয, ۨইਓਸ ҳࢿೡ ٸ ࢎਊਸ ӂೞҊ . Jetpack Composeীࢲח ۨইਓ غ؊ۄب ࢿמী ѱ ೱਸ ঋਵ۽, ConstraintLayout न ػ ۨইਓਵ۽ ҳࢿೞח Ѫ ѐߊ ࢤࢿҗ ࢿמ ݶীࢲ ؊ աਸ ࣻب . MotionLayout ই ਗೞ ঋח. -
৻ࠗ ۄ࠳۞ܻ ✅ ❌ PhotoView Coil Glide Lottie Picasso ExoPlayer
Accompanist FlexboxLayout -
Coil val painter = rememberImagePainter( data = "https://www.example.com/image.jpg", builder =
{ crossfade(true) } ) Image( painter = painter, contentDescription = null, modifier = Modifier.size(128.dp) ) when (painter.state) { is ImagePainter.State.Loading -> { ... } is ImagePainter.State.Success -> { ... } is ImagePainter.State.Error -> { ... } } Link: https://coil-kt.github.io/coil/compose/ Kotlin Coroutines ӝ߈ Android ۽٬ ۄ࠳۞ܻ. ܲ ۽٬ ۄ࠳۞ܻח Jetpack Compose ਗ ҅ദ হ. ౠ Picasso(#2203)ח ؊࢚ ӝמਸ ୶оೞ ঋਸ Ѫਵ۽ ࠁੋ. Glide(#4459), Fresco ۄ࠳۞ܻܳ ࢎਊೞח ۽ંীࢲח Coil۽ migrationೞѢա (Migration о٘ ޙࢲ) Landscapist ۄ࠳۞ܻܳ উਵ۽ Ѩష೧ࠅ ࣻب ਸ Ѫ э. -
Accompanist ѐߊೡ ٸ ੌ߈ਵ۽ ਃೞ݅ Jetpack Composeীࢲח ই ࢎਊೡ ࣻ
হח ӝמٜਸ ઁҕೞח ۄ࠳۞ܻ. ୶റ Composeীࢲ ӝמਸ ҕधਵ۽ ਗೞѢա, ৻ࠗ ۄ࠳۞ܻীࢲ ೞח ҃ীח ۄ࠳۞ܻо ઁѢؼ ࣻ . (ex. ୭Ӕ ۽٬ ۄ࠳۞ܻо ઁѢغ.) Link: https://github.com/google/accompanist • Coil • Glide • Insets • Permissions • Navigation Animation • Navigation Material • AppCompat Theme • Drawable Painter • System UI Controller • Swipe Refresh • Pager layouts • Flow layouts • Placeholder -
@Composable fun DrawDrawable() { val context = LocalContext.current val drawable
= ContextCompat.getDrawable( LocalContext.current, R.drawable.shape // <shape> ) Image( painter = rememberDrawablePainter(drawable), contentDescription = null ) } Accompanist: Drawable Painter painterResource() ח BitmapDrawable ژח VectorDrawable݅ ਗೠ. ӝઓী ࢎਊೞ؍ <shape>৬ э Drawableਸ ࢎਊೡ ࣻ ѱ ೧ח ۄ࠳۞ܻ. ݅ <selector>, <level-list> ۢ ࢚కী ٮۄ о ׳ۄח Drawable ਗغ ঋח. -
Accompanist: Swipe Refresh val viewModel: MyViewModel = viewModel() val isRefreshing
by viewModel.isRefreshing.collectAsState() SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = { viewModel.refresh() }, ) { // list } SwipeRefreshLayoutਸ ೞח ۄ࠳۞ܻ. -
Accompanist: Pager layouts // Display 10 items val pagerState =
rememberPagerState(pageCount = 10) HorizontalPager(state = pagerState) { page -> Text(text = "Page: $page") } VerticalPager(state = pagerState) { page -> Text(text = "Page: $page") } ViewPagerܳ ೞח ۄ࠳۞ܻ. -
Accompanist: Placeholder Text( text = "Content to display after content
has loaded", modifier = Modifier .placeholder( visible = true, highlight = PlaceholderHighlight.fade(), ) ) Placeholder ӝמਸ ઁҕೞח ۄ࠳۞ܻ. -
ਵ۽ ਊೞӝ • ӝઓ জী ਵ۽ Compose ਊೞӝ https://developer.android.com/jetpack/compose/interop
• Architecture https://developer.android.com/jetpack/compose/architecture • Accessibility https://developer.android.com/jetpack/compose/accessibility • Testing https://developer.android.com/jetpack/compose/testing • List of modifiers https://developer.android.com/jetpack/compose/modifiers-list -
Issue Tracker: ग ١۾ೞӝ / Star ־ܰӝ Link: https://issuetracker.google.com/issues?q=componentid:610764 componentid:
856989 → foundation 742043 → material 610764 → compiler / runtime 610478 → ui / animation -
Summary • ✅ ؊ Ҋ ҙੋ ٘۽ UIܳ ѐߊೡ ࣻ
. • 📚 নೠ ҕध ޙࢲ৬ ٘ەਸ ా೧ ઑӘঀ ण೧ࠁ. • ⚠ ࢎਊೞҊ ח View API৬ زੌೠ APIо ઁҕغח ԙ ഛੋ೧ࠁ. • 🚀 APIо হਵݶ Jetpack Compose Roadmapী ӝੑغয ח ଵҊ೧ࠁ. • 👾 ӝמ/ࢸ҅ য়ܨܳ ߊѼೞݶ, IssueTrackerী गܳ թѹࠁ. (زੌೠ गо ਵݶ ⭐ ܳ ־ܰ.) • 💬 п Compose ਊೞӝ ೠ दਸ Ҋ೧ࠁ.