Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidknights 2023 - 정태훈 - Compose로 위젯을 만든다고?! G...

Taehun, Jeong
September 12, 2023

Droidknights 2023 - 정태훈 - Compose로 위젯을 만든다고?! Glance를 이용한 차량 위젯 개발기

Compose 와 유사한 Code Style로 App Widget / Wear Tile 을 개발할 수 있는 Glance 라이브러리에 대해 다룹니다. Google docs 에 제시되어 있는 Glance 라이브러리에 대한 전반적인 내용을 다루는 동시에 이를 실제 서비스 중인 앱에 적용하였던 경험을 공유합니다.

https://developer.android.com/jetpack/compose/glance
https://developer.android.com/jetpack/androidx/releases/glance

Taehun, Jeong

September 12, 2023
Tweet

Other Decks in Technology

Transcript

  1. Taehun Jeong ❏ 현대자동차 (2019 ~) ❏ 클러스터 설계 (2019

    ~ 2021) ❏ Android 개발 (2022 ~ ing) ❏ 1년 9개월 째 … Image goes here...
  2. 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.
  3. 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
  4. AndroidManifest.xml <receiver android:name=".glance.MyReceiver" android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data

    android:name="android.appwidget.provider" android:resource="@xml/my_app_widget_info" /> </receiver> // https://developer.android.com/jetpack/compose/glance/create-app-widget
  5. My_app_widget_info.xml <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="40dp" android:minHeight="40dp" android:targetCellWidth="1" android:targetCellHeight="1" android:maxResizeWidth="250dp" android:maxResizeHeight="120dp" android:updatePeriodMillis="86400000"

    android:description="@string/example_appwidget_description" android:previewLayout="@layout/example_appwidget_preview" android:initialLayout="@layout/example_loading_appwidget" android:configure="com.example.android.ExampleAppWidgetConfigurationActivity" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen" android:widgetFeatures="reconfigurable|configuration_optional"> </appwidget-provider> // https://developer.android.com/develop/ui/views/appwidgets#MetaData
  6. 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() } } }
  7. 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)) } }
  8. actionStartActivity Example @Composable fun MyContent() { // .. Button( text

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

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

    = "Send", onClick = actionSendBroadcast<MyReceiver>() ) } 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) }
  11. actionRunCallback Example - actionRunCallback @Composable private fun MyContent() { //

    .. Image( provider = ImageProvider(R.drawable.ic_hourglass_animated), modifier = GlanceModifier.clickable( onClick = actionRunCallback<RefreshAction>() ), contentDescription = "Refresh" ) }
  12. 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) } }
  13. actionRunCallback Example – Parameters (Sender) private val destinationKey = ActionParameters.Key<String>(

    NavigationActivity.KEY_DESTINATION ) @Composable private fun MyContent() { // .. Button( text = "Home", onClick = actionStartActivity<NavigationActivity>( actionParametersOf(destinationKey to "home") ) ) Button( text = "Work", onClick = actionStartActivity<NavigationActivity>( actionParametersOf(destinationKey to "work") ) ) }
  14. actionRunCallback Example – Parameters (Receiver) class RefreshAction : ActionCallback {

    private val destinationKey = ActionParameters.Key<String>( NavigationActivity.KEY_DESTINATION ) override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { val destination: String = parameters[destinationKey] ?: return // ... } }
  15. Run lambda actions Example Text( text = "Submit", modifier =

    GlanceModifier.clickable { submitData() } ) Button( text = "Submit", onClick = { submitData() } ) Beta +
  16. 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()
  17. 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 } } } }
  18. 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)
  19. 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 } }
  20. 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
  21. 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
  22. 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) }
  23. 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)
  24. 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) } }
  25. MY GENESIS Widget Issue 2 – Layout Composable supports up

    to 10 child elements Box, Column, Row …
  26. 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() )
  27. Wrap up ❏ Compose UI에 익숙한 사람이 위젯 개발을 하기

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