Slide 1

Slide 1 text

Compose로 위젯을 만든다고?! Glance를 이용한 차량 위젯 개발기 정태훈 / 현대자동차

Slide 2

Slide 2 text

Taehun Jeong ❏ 현대자동차 (2019 ~) ❏ 클러스터 설계 (2019 ~ 2021) ❏ Android 개발 (2022 ~ ing) ❏ 1년 9개월 째 … Image goes here...

Slide 3

Slide 3 text

MY GENESIS

Slide 4

Slide 4 text

MY GENESIS

Slide 5

Slide 5 text

MY GENESIS ❏ Function to check vehicle status ❏ Function to remotely control vehicle

Slide 6

Slide 6 text

Jetpack Glance

Slide 7

Slide 7 text

Jetpack Glance https://developer.android.com/jetpack/androidx/releases/glance Google I/O 2023

Slide 8

Slide 8 text

Jetpack Glance ❏2021.12.15 1.0.0-alpha01 ❏2022.10.05 1.0.0-alpha05 ❏2023.05.10 1.0.0-beta01 ❏2023.07.26 1.0.0-rc01 ❏2023.09.06 1.0.0 🎉 🎉 https://developer.android.com/jetpack/androidx/releases/glance Build layouts for remote surfaces using a Jetpack Compose-style API.

Slide 9

Slide 9 text

Glance Setup https://developer.android.com/jetpack/compose/glance/create-app-widget ❏Version Requirement AGP >= 7.0.0 Compose >= 1.1.0 compileSDK >= 31 … 34 (rc01) minSDK >= 21

Slide 10

Slide 10 text

build.gradle dependencies { // For AppWidgets support implementation "androidx.glance:glance-appwidget:1.0.0-rc01" // For interop APIs with Material 2 implementation "androidx.glance:glance-material:1.0.0-rc01" // For interop APIs with Material 3 implementation "androidx.glance:glance-material3:1.0.0-rc01" } // https://developer.android.com/jetpack/compose/glance/create-app-widget

Slide 11

Slide 11 text

AndroidManifest.xml // https://developer.android.com/jetpack/compose/glance/create-app-widget

Slide 12

Slide 12 text

My_app_widget_info.xml MyReceiver.kt class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { // Let MyAppWidgetReceiver know which GlanceAppWidget to use override val glanceAppWidget: GlanceAppWidget = MyAppWidget() }

Slide 13

Slide 13 text

My_app_widget_info.xml // https://developer.android.com/develop/ui/views/appwidgets#MetaData

Slide 14

Slide 14 text

MyAppWidget.kt class MyAppWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { // Load data needed to render the AppWidget. // Use `withContext` to switch to another thread for long running // operations. provideContent { // create your AppWidget here MyContent() } } }

Slide 15

Slide 15 text

MyContent.kt @Composable private fun MyContent() { Column( modifier = GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Top, horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = ”Hello Glance!", modifier = GlanceModifier.padding(12.dp)) } }

Slide 16

Slide 16 text

Handle user interaction https://developer.android.com/jetpack/compose/glance/user-interaction ❏actionStartActivity ❏ActionStartService ❏actionSendBroadcast ❏actionRunCallback ❏Run lambda actions

Slide 17

Slide 17 text

actionStartActivity Example @Composable fun MyContent() { // .. Button( text = "Go Home", onClick = actionStartActivity() ) } actionStartActivity( createGOAActivityIntent().apply { action = ACTION_VIEW data = Uri.parse(LINK_VEHICLE_CONTROL).buildUpon() .appendQueryParameter("id", subControlButton.id.toString()).build() } )

Slide 18

Slide 18 text

actionStartService Example @Composable fun MyButton() { // .. Button( text = "Sync", onClick = actionStartService( isForegroundService = true // define how the service is launched ) ) }

Slide 19

Slide 19 text

actionSendBroadcast Example @Composable fun MyButton() { // .. Button( text = "Send", onClick = actionSendBroadcast() ) } Intent(context, GOAWidgetVehicleControlReceiver::class.java).apply { action = GOA_INTENT_BIOMETRIC_RESULT this.putExtra(KEY_BIOMETRIC_RESULT, Result.ON_SUCCESS) this.putExtra(KEY_WIDGET_CONTROL, controlButton) }.also { return actionSendBroadcast(it) }

Slide 20

Slide 20 text

actionRunCallback Example - actionRunCallback @Composable private fun MyContent() { // .. Image( provider = ImageProvider(R.drawable.ic_hourglass_animated), modifier = GlanceModifier.clickable( onClick = actionRunCallback() ), contentDescription = "Refresh" ) }

Slide 21

Slide 21 text

actionRunCallback Example - ActionCallback class RefreshAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { // do some work but offset long-term tasks (e.g a Worker) MyAppWidget().update(context, glanceId) } }

Slide 22

Slide 22 text

actionRunCallback Example – Parameters (Sender) private val destinationKey = ActionParameters.Key( NavigationActivity.KEY_DESTINATION ) @Composable private fun MyContent() { // .. Button( text = "Home", onClick = actionStartActivity( actionParametersOf(destinationKey to "home") ) ) Button( text = "Work", onClick = actionStartActivity( actionParametersOf(destinationKey to "work") ) ) }

Slide 23

Slide 23 text

actionRunCallback Example – Parameters (Receiver) class RefreshAction : ActionCallback { private val destinationKey = ActionParameters.Key( NavigationActivity.KEY_DESTINATION ) override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { val destination: String = parameters[destinationKey] ?: return // ... } }

Slide 24

Slide 24 text

Run lambda actions Example Text( text = "Submit", modifier = GlanceModifier.clickable { submitData() } ) Button( text = "Submit", onClick = { submitData() } ) Beta +

Slide 25

Slide 25 text

Manage and update GlanceAppWidget https://developer.android.com/jetpack/compose/glance/glance-app-widget Use application state Whenever the state or the data changes, it is the app's responsibility to notify and update the widget. See Update GlanceAppWidget for more information. Update GlanceAppWidget As explained in the Manage GlanceAppWidget state section, app widgets are hosted in a different process. Glance translates the content into actual RemoteViews and sends them to the host. To update the content, Glance must recreate the RemoteViews and send them again. currentState()

Slide 26

Slide 26 text

Use application state class DestinationAppWidget : GlanceAppWidget() { // ... @Composable fun MyContent() { val repository = remember { DestinationsRepository.getInstance() } // Retrieve the cache data everytime the content is refreshed val destinations by repository.destinations.collectAsState(State.Loading) when (destinations) { is State.Loading -> { // show loading content } is State.Error -> { // show widget error content } is State.Completed -> { // show the list of destinations } } } }

Slide 27

Slide 27 text

Update GlanceAppWidget // Updates specific widget by glanceId MyAppWidget().update(context, glanceId) // Updates specific widgets by class type val manager = GlanceAppWidgetManager(context) val widget = GlanceSizeModeWidget() val glanceIds = manager.getGlanceIds(widget.javaClass) glanceIds.forEach { glanceId -> widget.update(context, glanceId) } // Updates all placed instances of MyAppWidget MyAppWidget().updateAll(context)

Slide 28

Slide 28 text

currentState @Composable fun test( prefs: Preferences = currentState() ) { val a = prefs[booleanPreferencesKey("KEY_A")] } updateAppWidgetState( context, PreferencesGlanceStateDefinition, glanceId ) { updateState -> updateState.toMutablePreferences().apply { this[booleanPreferencesKey("KEY_A")] = true } }

Slide 29

Slide 29 text

currentState

Slide 30

Slide 30 text

Build UI with Glance https://developer.android.com/jetpack/compose/glance/build-ui Glance Composables ❏1.0.0-alpha01 Box, Row, Column, Text, Button, LazyColumn, Image, Spacer, AndroidRemoteViews ❏1.0.0-alpha03 LazyVerticalGrid ❏1.0.0-alpha04 RadioButton ❏1.0.0-beta01 GlanceTheme, CheckBox, Switch

Slide 31

Slide 31 text

Glance Experimental Tools https://github.com/google/glance-experimental-tools

Slide 32

Slide 32 text

MY GENESIS Widget

Slide 33

Slide 33 text

MY GENESIS Widget Vehicle Status Vehicle Control Integrated

Slide 34

Slide 34 text

MY GENESIS Widget Data Flow Broadcast Receiver (Base Widget) actionRunCallback sendBroadcast startActivity (Bio Auth) startActivity (Main with PIN Intent) Logic onAction CMD Logic App Request / Response update

Slide 35

Slide 35 text

MY GENESIS Widget https://developer.android.com/develop/ui/views/appwidgets Support Theme configure

Slide 36

Slide 36 text

Configure Activity - Confirm Button onConfirm = { val context = this CoroutineScope(Dispatchers.IO).launch { updateAppWidgetState( context, PreferencesGlanceStateDefinition, glanceId ) { updateState -> updateState.toMutablePreferences().updateWidgetSettingPreferences( isAutoTheme.value, isDay.value, alpha.value ) } updateWidgetByGlanceId(context, viewModel.glanceId) finishConfiguration(viewModel.appWidgetId, RESULT_OK) }

Slide 37

Slide 37 text

updateWidgetByGlanceId.kt suspend fun updateWidgetByGlanceId(context: Context, glanceId: GlanceId) { when (glanceId) { in getStatusWidgetIds(context) -> GOAVehicleStatusWidget.update(context, glanceId) in getControlWidgetIds(context) -> GOAVehicleControlWidget.update(context, glanceId) in getIntegratedWidgetIds(context) -> GOAVehicleIntegratedWidget.update(context, glanceId) } } suspend fun getStatusWidgetIds(context: Context) = GlanceAppWidgetManager(context).getGlanceIds(GOAVehicleStatusWidget::class.java)

Slide 38

Slide 38 text

MY GENESIS Widget https://developer.android.com/jetpack/compose/glance/build-ui All Widget Display Same State

Slide 39

Slide 39 text

Base Widget Receiver open class GOAVehicleBaseWidget( override val glanceAppWidget: GlanceAppWidget ) : GlanceAppWidgetReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { GOA_INTENT_RESET_UPDATE_STATUS -> { ... } GOA_INTENT_BIOMETRIC_RESULT -> { ... } GOA_INTENT_APPWIDGET_UPDATE -> { ... } GOA_INTENT_APPWIDGET_CONTROL_BUTTON_LOADING_CHANGED -> { ... } } super.onReceive(context, intent) } }

Slide 40

Slide 40 text

MY GENESIS Widget https://developer.android.com/jetpack/compose/glance/build-ui Issue 1 – Need to import appropriate library

Slide 41

Slide 41 text

MY GENESIS Widget Issue 2 – Layout Composable supports up to 10 child elements Box, Column, Row …

Slide 42

Slide 42 text

MY GENESIS Widget https://developer.android.com/jetpack/compose/glance/interoperability Issue 3 – No support chronometer glance composable

Slide 43

Slide 43 text

ChronometerAndroidRemoteViews.kt AndroidRemoteViews( remoteViews = RemoteViews(context.packageName, R.layout.chronometer).apply { setChronometer( R.id.chronometer, updateDate - (System.currentTimeMillis() - SystemClock.elapsedRealtime()), "%s 전", true ) setTextColor( R.id.chronometer, getGOAWidgetButtonLabelColorProvider( context, isAutoBackground, isDay, bgAlpha ).getColor(context).toArgb() ) }, modifier = GlanceModifier.wrapContentSize() )

Slide 44

Slide 44 text

MY GENESIS Widget - https://developer.android.com/jetpack/androidx/releases/glance Issue 4 – No support custom font

Slide 45

Slide 45 text

MY GENESIS Widget - https://issuetracker.google.com/u/3/issues/288938865 Issue 5 – WorkManager Crash

Slide 46

Slide 46 text

Wrap up

Slide 47

Slide 47 text

Wrap up ❏ Compose UI에 익숙한 사람이 위젯 개발을 하기 위한 좋은 Tool ❏ Stable 버전이 아님에도 안정적인 동작을 함 (으로 느껴짐) ❏ 급하게 적용하면서 기능을 100% 활용하지 못한 것 같은 아쉬움 ❏ 다음엔 더 잘하자!

Slide 48

Slide 48 text

Wrap up

Slide 49

Slide 49 text

Thanks!