Data Store (1400 x 500 px) (4).png

В этой статье я собираюсь показать вам Android фичу, представленную в 2012 году, и попробую написать для неё UI на Compose.

Изучая недра Android, я наткнулся на один Service, который привлек моё внимание. Класс, который я обнаружил, не только заинтриговал меня своим названием, но и снова удивил интересными возможностями, скрытыми внутри Android.

Ещё в 2012 году команда Android System UI представила в Android 4.2 новую функцию Daydream, которая позволяет показывать контент пользователю, пока телефон находится в спящем режиме. Вы можете просто отрисовать любой UI или анимацию для пользователя, пока его телефон заряжается. UI-элементы также могут содержать кнопки, так что в целом можно создать полноценный экран для продакшена. К сожалению, эту функцию нельзя включить автоматически. Пользователь должен включить её в настройках дисплея.

Архитектура этой функции похожа на добавление обычного Service. Сначала нужно объявить новый компонент в Manifest:

<service
	android:name=".MyDreamService"
	android:exported="true"
	android:description="@string/dream_service_description"
	android:icon="@drawable/dream_service_icon"
	android:label="@string/dream_service_label"
	android:permission="android.permission.BIND_DREAM_SERVICE">

	<intent-filter>
      <action android:name="android.service.dreams.DreamService" />
      <category android:name="android.intent.category.DEFAULT" />
	</intent-filter>
</service>

Простой пример кода из официальной документации:

class MyDreamService : DreamService() {

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()

        isInteractive = false;
        isFullscreen = true;
        setContentView(R.layout.dream_service_layout);
    }
}

После этого вы можете увидеть ваш daydream в действии

Data Store (1400 x 500 px) (5).png

Затем у меня возник вопрос — могу ли я создать UI на Compose? Давайте попробуем реализовать это.

Поскольку setContentView принимает только View, нам нужно создать ComposeView:

val composeView = ComposeView(this).apply {
		setContent {
			DreamServiceTheme {
    			DreamScreen(
            		onOpenSettings = { 
                        openSettingsAndExitDream() 
                    }
            )
        }
     }
}

setContentView(composeView)

А вот тут начинается самое интересное. Android Service на самом деле не работает хорошо с View, и по этой причине у Service отсутствуют некоторые важные для нас API. DreamService не реализует:

  • ViewTreeLifecycleOwner

  • SavedStateRegistryOwner

Чтобы решить эту проблему, мы можем расширить наш DreamService, реализовав SavedStateRegistryOwner:

class MyDreamService : DreamService(), SavedStateRegistryOwner

После этого необходимо реализовать lifecycle и savedStateRegistry:

class MyDreamService : DreamService(), SavedStateRegistryOwner {

    private val lifecycleRegistry = LifecycleRegistry(this)
    private val savedStateRegistryController = SavedStateRegistryController.create(this)

    override val lifecycle: Lifecycle
        get() = lifecycleRegistry
    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry
        
    // ...  
}

Эти два свойства можно вызывать в каждом колбэке DreamService, чтобы lifecycle был корректно синхронизирован с Compose View. После этого мы можем установить owner и registry для вашего ComposeView и UI заработает с Compose:

val composeView = ComposeView(this).apply {
    setViewTreeLifecycleOwner(this@MyDreamService)
    setViewTreeSavedStateRegistryOwner(this@MyDreamService)
    // ... 
}

Надеюсь, эта фича обогатит ваш ответ на техническом интервью, ведь теперь вы знаете, что Activity — не единственный компонент, который может представлять UI.