Wear Os Daily Face Changer
This guide will walk you through the key components, using modern Android development practices with Jetpack Compose for Wear OS and StateFlow for reactive data handling.
Project Goal
We will build a watch face that has a collection of backgrounds. Each day, at midnight, the watch face will automatically cycle to the next background in the sequence, creating a fresh look.
Prerequisites
* Android Studio (latest version).
* A Wear OS emulator or a physical Wear OS device for testing.
* Basic knowledge of Kotlin.
Step 1: Project Setup
* Open Android Studio.
* Click New Project.
* Select the Wear OS tab.
* Choose Watch Face.
* Click Next.
* Configure your project:
* Name: Daily Changer Watch Face
* Package name: com.example.dailychanger
* Language: Kotlin
* Build configuration: Groovy DSL or KTS
* Minimum SDK: API 28 or higher is recommended.
* Click Finish. Android Studio will generate a template watch face project.
Step 2: Define the Daily Watch Faces
First, let's define the "faces" we want to cycle through. For simplicity, our "faces" will just be different background images.
* Add a few sample background images to your res/drawable folder. Let's call them bg_day1.png, bg_day2.png, bg_day3.png, etc.
* Create a new Kotlin file named WatchFaceRepo.kt. We'll define our available faces here using an enum for type safety and easy management.
<!-- end list -->
// file: WatchFaceRepo.kt
package com.example.dailychanger
import androidx.annotation.DrawableRes
enum class DailyBackground(
@DrawableRes val backgroundRes: Int
) {
FOREST(R.drawable.bg_forest), // Make sure you have these drawables
OCEAN(R.drawable.bg_ocean),
MOUNTAIN(R.drawable.bg_mountain),
CITY(R.drawable.bg_city);
companion object {
fun fromIndex(index: Int): DailyBackground {
// Cycle through the enum values
return values()[index % values().size]
}
}
}
Replace the drawable names with the ones you added.
Step 3: Create the Watch Face Composable UI
We'll use Compose for Wear OS to draw our watch face. The generated project already has a basic structure. Let's modify it.
Open the file named WatchFace.kt (or similar, from the template) and replace its content with this:
// file: WatchFace.kt
package com.example.dailychanger
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.wear.compose.material.Text
import java.time.LocalTime
import java.time.format.DateTimeFormatter
@Composable
fun DailyWatchFace(
background: DailyBackground,
time: LocalTime
) {
val timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// 1. The daily changing background
Image(
painter = painterResource(id = background.backgroundRes),
contentDescription = "watch background",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// 2. The time display
Text(
text = time.format(timeFormatter)
)
}
}
@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
@Composable
fun DailyWatchFacePreview() {
DailyWatchFace(
background = DailyBackground.FOREST,
time = LocalTime.now()
)
}
Step 4: Manage State and Daily Logic
This is the core of our application. We need a way to:
* Check the current date.
* Decide which face to show.
* Save this choice so it persists.
* Update when the day changes.
We will use SharedPreferences for simple data storage. Create a new file DailyFaceStateHolder.kt.
// file: DailyFaceStateHolder.kt
package com.example.dailychanger
import android.content.Context
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.time.LocalDate
object DailyFaceStateHolder {
private const val PREFS_NAME = "DailyWatchFacePrefs"
private const val KEY_LAST_UPDATE_DATE = "last_update_date"
private const val KEY_CURRENT_FACE_INDEX = "current_face_index"
// Use StateFlow to notify the watch face of changes reactively
private val _currentBackground = MutableStateFlow(DailyBackground.FOREST)
val currentBackground: StateFlow<DailyBackground> = _currentBackground.asStateFlow()
fun updateFaceIfNeeded(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val today = LocalDate.now().toString()
val lastUpdateDate = prefs.getString(KEY_LAST_UPDATE_DATE, null)
// Only update if the date has changed
if (today != lastUpdateDate) {
val lastIndex = prefs.getInt(KEY_CURRENT_FACE_INDEX, -1)
val nextIndex = lastIndex + 1
val newFace = DailyBackground.fromIndex(nextIndex)
_currentBackground.value = newFace
// Save the new state
with(prefs.edit()) {
putString(KEY_LAST_UPDATE_DATE, today)
putInt(KEY_CURRENT_FACE_INDEX, nextIndex)
apply()
}
} else {
// On boot or first run, just load the saved face
val savedIndex = prefs.getInt(KEY_CURRENT_FACE_INDEX, 0)
_currentBackground.value = DailyBackground.fromIndex(savedIndex)
}
}
}
Step 5: Implement the Daily Update Trigger
To change the face at midnight, we need a mechanism that runs reliably. The best tool for this is AlarmManager combined with a BroadcastReceiver.
* Create the BroadcastReceiver:
Create a new file DailyUpdateReceiver.kt. This receiver will be triggered by our alarm and will simply call our state update logic.
// file: DailyUpdateReceiver.kt
package com.example.dailychanger
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class DailyUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
// When the alarm fires, trigger the face update logic
DailyFaceStateHolder.updateFaceIfNeeded(context)
}
}
* Schedule the Alarm:
Create a helper object to manage scheduling. Create AlarmScheduler.kt.
// file: AlarmScheduler.kt
package com.example.dailychanger
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import java.util.Calendar
object AlarmScheduler {
fun scheduleDailyUpdate(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, DailyUpdateReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Set the alarm to start at approximately midnight
val calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 1) // A minute past midnight to be safe
}
// Use inexact repeating to save battery. The system will fire it around midnight.
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
}
}
Step 6: Update the Watch Face Service
Now, let's tie everything together in our main service file (DailyChangerWatchFaceService.kt or similar). This service will:
* Listen to the StateFlow from our DailyFaceStateHolder.
* Schedule the daily alarm when it's created.
* Render the Composable UI.
Replace the content of your WatchFaceService file:
// file: DailyChangerWatchFaceService.kt
package com.example.dailychanger
import android.view.SurfaceHolder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.wear.watchface.ComplicationSlotsManager
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.style.CurrentUserStyleRepository
import kotlinx.coroutines.flow.StateFlow
import java.time.LocalTime
class DailyChangerWatchFaceService : WatchFaceService() {
override fun onCreate() {
super.onCreate()
// Check the face state on service creation (e.g., after boot or update)
DailyFaceStateHolder.updateFaceIfNeeded(applicationContext)
// Schedule the alarm that will trigger future daily updates
AlarmScheduler.scheduleDailyUpdate(applicationContext)
}
override suspend fun createWatchFace(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository
): WatchFace {
return WatchFace(
watchFaceType = WatchFaceType.DIGITAL,
renderer = WatchFaceRenderer(
surfaceHolder = surfaceHolder,
watchState = watchState,
currentUserStyleRepository = currentUserStyleRepository,
// Pass our reactive state flow to the renderer
backgroundStateFlow = DailyFaceStateHolder.currentBackground
)
)
}
}
// The renderer now takes the StateFlow
private class WatchFaceRenderer(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
currentUserStyleRepository: CurrentUserStyleRepository,
private val backgroundStateFlow: StateFlow<DailyBackground>
) : WatchFaceRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState
) {
override suspend fun getComplicationState(complicationId: Int) = null
@Composable
override fun Build() {
// Collect the state flow as compose state.
// When the flow emits a new value, this composable will re-render.
val background by backgroundStateFlow.collectAsState()
// We check the state here as well in case the watch face becomes visible
// after being dormant for a day.
DailyFaceStateHolder.updateFaceIfNeeded(context)
DailyWatchFace(
background = background,
time = LocalTime.now()
)
}
}
Note: Depending on your template, you might need to adjust the renderer class structure. The key is to pass DailyFaceStateHolder.currentBackground to it and use collectAsState().
Step 7: Update AndroidManifest.xml
Finally, we must declare our receiver and the necessary permissions in the manifest.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<service
android:name=".DailyChangerWatchFaceService"
android:exported="true"
android:label="@string/watch_face_name"
android:permission="android.permission.BIND_WALLPAPER">
</service>
<receiver
android:name=".DailyUpdateReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
How to Run
* Connect your Wear OS device or start an emulator.
* Run the application from Android Studio.
* On your watch, long-press the current watch face to open the watch face selector.
* Find and select your "Daily Changer Watch Face".
It will now show the first face. To test the daily change logic without waiting, you can either change the date on your test device or add a temporary button somewhere to call DailyFaceStateHolder.updateFaceIfNeeded(context). The watch face will update automatically every day around midnight.
Comments
Post a Comment