As millions of LINE app users celebrate birthdays and cultural festivities, a carefully crafted system works behind the scenes to make these moments magical. As an Android engineer at LY Corporation, I'm excited to share how we built the Home tab event effects system that powers these celebrations.
This article dives deep into the technical architecture that transforms ordinary app moments into delightful celebrations. You'll learn how we orchestrate precise timing, manage resources efficiently, and ensure reliable delivery of celebratory animations across millions of Android devices.
We'll explore how we leveraged Android's powerful toolkit—Kotlin Flow for reactive updates, Room database for reliable storage, and WorkManager for precise scheduling—to overcome challenging engineering hurdles. Whether you're an Android developer interested in building robust animation systems or simply curious about how LINE app creates these magical moments, this deep dive will offer valuable insights into modern Android architecture.


LINE app's Home tab during a birthday celebration (left) and Christmas celebration (right).
Event effects system overview
Core components
At its heart, LINE app's event effects system orchestrates two distinct types of celebrations:
Client-side events: Personal celebrations, like birthdays, operate entirely on the user's device. We prioritize:
- Privacy and reliability through local event management
- Real-time responsiveness to profile updates
Server-pushed events: Cultural celebrations, like New Year's, require coordinated experiences across regions:
- Server-driven configurations enable culturally tailored experiences
- Dynamic updates support region-specific timing and content
Visual experiences
Each celebration comes to life through two carefully designed visual components:
Full-screen effects
- Create an immersive celebration moment
- Display once when the event triggers
- Orchestrate dynamic animations across the entire screen
Profile background effects
- Provide persistent celebration ambiance
- Enhance the user's profile display
- Complement the full-screen experience with subtle animations
Engineering challenges
Creating a special atmosphere on the Android version of the LINE app's Home tab for birthdays and holidays is more complex than it seems. Here are some of the key challenges we faced:
- Precision in timing: A celebration is only as good as its timing. Ensuring that events trigger exactly when intended, even after app restarts or device reboots, required meticulous attention to timing mechanisms within the app.
- Efficient resource management: Given the frequency with which users access the Home tab, maintaining performance was critical. Although celebration assets are not large, we needed to manage their lifecycle smartly—deciding when to load, how long to retain, and when to discard them to optimize app startup time and storage usage.
- Frequent edge cases: Changes in user settings, network failures, and app terminations are common in mobile environments. Each scenario demanded robust handling to maintain a consistent and enjoyable celebration experience.
To address these challenges, we built our solution on Android's robust foundation:
- Kotlin Flow for reactive data updates
- Room database for reliable local storage
- WorkManager for precise task scheduling and retry
This architecture ensures reliable celebration delivery while maintaining app performance and user experience.
The following sections will dive deep into how we implemented these solutions, starting with our overall system architecture.
The architecture
Creating magical celebration moments requires precise orchestration of multiple system components. Let's first understand the complete journey of an event through our system, then explore how we structured our solution to handle this complexity.
Event journey
- Receive the event notification: The system promptly detects and acknowledges incoming events.
- Download the event metadata: Essential details, such as asset URLs and checksums, are retrieved.
- Download and validate effect resources: Ensures all necessary assets are ready and verified for use.
- Maintain readiness during idle periods: Keeps the system prepared for the event without unnecessary resource usage.
- Display effect at the designated time: Executes the celebration precisely when intended.
- Clean up resources: Efficiently removes assets and metadata post-celebration.
- Schedule the next occurrence for recurring events: Prepares for future events with minimal disruption.
Implementation strategy
To transform this journey into reliable code, we divided our implementation into four operational phases, each handling specific responsibilities while maintaining system stability:
This phase-based architecture provides several key benefits:
- Clear separation of concerns
- Independent error handling in each phase
- Simplified testing and maintenance
- Reliable state management
Let's explore each phase in detail to understand how they work together to create seamless celebrations.
Phase architecture details
Phase 1: Event discovery
First things first: how do we know when to celebrate? As mentioned above, our event effects system handles two types of celebrations—server-pushed events and client-side moments. Each comes with a simple showtime tag that tells us when the party should start. For server-pushed events, users receive push notifications to inform them about celebrations. Client-side moments are detected through two triggers: when users update their profile information or when the app launches, ensuring accurate timing through client-side verification.
Detection and initial processing
When an event arrives, the system:
- Validates the event data and timing
- Checks if we're already handling this event
- Determines if it's new, updated, or cancelled
- Initiates appropriate scheduling or cleanup actions
Smart scheduling strategy
To ensure reliable celebration delivery, we implement a proactive scheduling approach:
5-day buffer window
- Allows time for network retries and error recovery
- Accommodates potential event updates before showtime
- Prevents unnecessary resource fetching for cancelled events
Scheduling implementation Here's how we implement this scheduling logic using WorkManager:
// class ResourceAcquisitionWorkerRegister
fun registerMetadataFetcher(event: Event) {
val delayMillis = event.showTime - 5.days.inWholeMilliseconds - currentTimeMillis()
val workData = workDataOf(
"eventId" to event.id,
"retryUntil" to (event.showTime + 1.day.inWholeMilliseconds)
)
val workRequest = OneTimeWorkRequestBuilder<ResourceAcquisitionWorker>()
.setInputData(workData)
.setInitialDelay(delayMillis, TimeUnit.MILLISECONDS)
.setBackoffCriteria(EXPONENTIAL, 30, TimeUnit.MINUTES)
.setConstraints(...)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
This implementation includes built-in retry logic to ensure reliable scheduling.
Design philosophy
We intentionally keep event discovery separate from resource handling. This separation of concerns:
- Enables quick response to event updates
- Simplifies system maintenance
- Reduces complexity in handling user preference changes
- Improves overall system reliability
Phase 2: Resource acquisition
Phase 2 is where the system performs its most critical tasks: fetching event metadata and preparing celebration assets. This phase ensures that all resources are ready well before the celebration time, handling everything from initial metadata retrieval to final asset validation.
The journey through Phase 2 consists of three crucial sub-tasks:
- Metadata fetching: Retrieves and validates event configurations, providing essential information like asset URLs and checksums
- Asset download and validation: Ensures celebration assets are complete and verified, preventing any issues during the actual celebration
- Worker registration: Sets up showtime and cleanup processes for precisely timed celebrations
Let's explore each sub-task in detail to understand how they work together to ensure reliable celebration delivery.
Metadata fetching
This sub-phase is responsible for retrieving and validating essential event information. A crucial feature of our design is the retryUntil
mechanism, which ensures we don't waste resources trying to fetch metadata for expired events.
When the Resource Acquisition Worker starts, it first checks if the current time is before the retryUntil
timestamp (event's showtime plus one day) for is executable
. This validation ensures we don't waste resources on expired events or events that can no longer be displayed.
// class ResourceAcquisitionWorker
override suspend fun doWork(): Result {
val eventId = inputData.getString("eventId") ?: return Result.failure()
val retryUntil = inputData.getLong("retryUntil", 0)
if (System.currentTimeMillis() > retryUntil) {
return Result.failure()
}
try {
val event = eventRepository.getEvent(eventId)
val metadata = fetchEventMetadata(event)
eventRepository.updateEventMetadata(eventId, metadata)
assetDownloadWorkerRegister.registerAssetDownloadWorker(event)
return Result.success()
} catch (e: Exception) {
return Result.retry()
}
}
Upon successful metadata retrieval, the system:
- Stores the metadata in the Room database
- Schedules the asset download worker for the next phase
- Returns a success result to WorkManager
The metadata structure in our Room database looks like this:
data class Event(
val id: String, // Unique identifier for the event
val showTime: Long, // When to display the celebration
val endTime: Long, // When to end the celebration
val resourceUrl: String, // URL for downloading assets
val checksum: String, // For asset validation
val state: EventState // Current event state (PENDING/READY/FULL_SCREEN_EFFECT_COMPLETED)
)
The state field helps track the event's progress through the system:
enum class EventState(val dbValue: Int) {
PENDING(0), // Initial state, awaiting showtime
READY(1), // Ready to display
FULL_SCREEN_EFFECT_COMPLETED(2) // Full-screen effect finished
}
If any step fails, the worker utilizes WorkManager's retry mechanism with exponential backoff, ensuring we have multiple opportunities to fetch the metadata before the event's showtime.
Asset download and validation
After successful metadata retrieval, we move on to the most complex sub-phase: downloading and validating celebration assets. This process involves multiple verification steps to ensure a flawless celebration experience.
Process overview
The asset download worker follows a strict verification sequence:
- Availability check: Verifies the event is still valid and not expired.
- Asset download: Retrieves assets using the URL from metadata
- Integrity check: Validates downloaded assets against stored checksums
- Content verification: Ensures all required components are present and valid
- Storage setup: Safely stores validated assets for the celebration
Here's how we implement this robust verification process:
// class AssetDownloadWorker
override suspend fun doWork(): Result {
val eventId = inputData.getString("eventId")
val event = eventRepository.getEvent(eventId) ?: return Result.failure()
// Verify event is still valid
if (event.endTime < System.currentTimeMillis()) {
eventRepository.deleteEvent(eventId)
return Result.failure()
}
try {
// Download and verify asset integrity
val asset = downloadAsset(event)
validateAsset(asset, event.checksum)
// Extract and verify content
fileStorage.extractAndSave(eventId, asset)
assetVerifier.verifyAssetContent(eventId)
// Register workers for next phases
cleanupWorkerRegister.registerCleanupWorker(event)
showTimeWorkerRegister.registerShowTimeWorker(event)
return Result.success()
} catch (e: Exception) {
return Result.retry()
}
}
Worker registration
The worker registration process is intentionally separate from metadata fetching:
// AssetDownloadWorkerRegister
fun registerAssetDownloadWorker(event: Event) {
val workData = workDataOf("eventId" to event.id)
val workRequest = OneTimeWorkRequestBuilder<AssetDownloadWorker>()
.setInputData(workData)
.setBackoffCriteria(EXPONENTIAL, 30, TimeUnit.MINUTES)
.setConstraints(...)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
Final steps
Upon successful completion of asset download and validation:
- The cleanup worker is registered to manage resources after the celebration
- The showtime worker is scheduled for precise celebration timing
- The event enters an idle period, maintaining readiness without consuming unnecessary resources
Phase 3: Show time
Phase 3 is the most interesting part of the event effects system, not just because it's where the magic happens, but also because it's where all components of the system come together to create a seamless and delightful user experience. Here's how we ensure that the celebration is displayed at the designated time.
Handling multiple events
Since users might have multiple celebrations occurring simultaneously (imagine a birthday falling on New Year's Day), we need a way to determine which celebration takes precedence. Each event has a priority property that helps us make this decision:
- Client events (like birthdays) have higher priority, as they are personal celebrations
- Server events (like holidays) have lower priority, to ensure personal moments take precedence
- Among events of the same type, the most recently triggered event takes precedence
For example, if a user's birthday coincides with a New Year celebration, they'll see their birthday celebration first. This prioritization ensures that personal moments are highlighted while still maintaining the festive atmosphere of cultural celebrations.
State management and transitions
The UI state transitions through three key stages during this phase:
Precise effect timing
Remember the showtime worker we scheduled in Phase 2? The task of this worker is very simple: change the state of the event to READY
when it's time to display the celebration.
// class ShowTimeWorker
override suspend fun doWork(): Result {
val eventId = inputData.getString("eventId") ?: return Result.failure()
eventRepository.updateEventState(eventId, EventState.READY)
return Result.success()
}
Once the event state changes to READY
, Room's reactive query notifies the UI layer about the database change, which tells the UI when to display the celebration:
// class HomeEventEffectDao
@Query(
"""
SELECT *
FROM events
WHERE state >= :READY
"""
)
fun getDisplayableEventFlow(): Flow<List>
One important requirement is to show the effect when the user is opening the Home tab to ensure the celebration is not missed or causing any interruption when the user is using other features. We use a Flow to observe the Home tab visibility and combine it with getDisplayableEventsFlow()
:
// class HomeViewModel
val displayableEventFlow: Flow<Event?> = combine(
homeEventEffectDao.getDisplayableEventsFlow(),
homeTabVisibilityFlow
) { events, isHomeVisible ->
events.takeIf { isHomeVisible }
?.filter { it.endTime > currentTimeMillis() }
?.maxByOrNull { it.priority }
}
Visual effect orchestration
The system manages two types of effects, each requiring careful coordination:
// class HomeViewModel
val fullScreenEventEffectFlow: Flow<Event?> = displayableEventFlow
.filter { it.state == EventState.READY }
val profileBackgroundEventEffectFlow: Flow<Event?> = displayableEventFlow
.filter { it.state == EventState.FULL_SCREEN_EFFECT_COMPLETED }
// class HomeFragment
viewModel.fullScreenEventEffectFlow.collect { event ->
showFullScreenEffect(event) // null event means no effect to show
}
viewModel.profileBackgroundEventEffectFlow.collect { event ->
showProfileBackgroundEffect(event) // null event means to hide the effect
}
The transition between effects is managed by tracking the completion of the full-screen effect:
// class FullScreenEffectScreen
fun onEffectCompleted() {
viewModel.markFullScreenEffectComplete(eventId)
}
// class HomeViewModel
fun markFullScreenEffectComplete(eventId: String) {
viewModelScope.launch {
eventRepository.updateEventState(eventId, EventState.FULL_SCREEN_EFFECT_COMPLETED)
}
}
This orchestration ensures a smooth transition from the initial full-screen celebration to the subtle profile background effect, creating a cohesive celebration experience.
Phase 4: Clean up
We understand the importance of efficient resource management, especially after an event concludes or is canceled. Our cleanup phase is designed to keep the Home tab responsive and clutter-free, ensuring the app remains performant for our millions of users.
Cleanup strategy
Our system registers the cleanup worker right after the asset download process in Phase 2, ensuring reliable resource management even if users don't open the app during the celebration period. To prevent interruption for late-joining users (e.g., at 23:59:59), we schedule the cleanup one day after the event's official end time. This grace period ensures everyone can fully experience their special moment without disruption from resource cleanup.
fun registerCleanupWorker(event: Event) {
val workData = workDataOf("eventId" to event.id)
val cleanupDelay = event.endTime + 1.days.inWholeMilliseconds - currentTimeMillis()
val workRequest = OneTimeWorkRequestBuilder<CleanupWorker>()
.setInputData(workData)
.setInitialDelay(cleanupDelay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
Resource cleanup
The cleanup worker handles both assets and metadata:
// class CleanupWorker
override suspend fun doWork(): Result {
val eventId = inputData.getString("eventId") ?: return Result.failure()
try {
// Clean up assets
fileStorage.deleteEventAssets(eventId)
// Remove from database
eventRepository.deleteEvent(eventId)
// Handle recurring events
val event = eventRepository.getEvent(eventId)
if (event.isRecurring) {
scheduleNextOccurrence(event)
}
return Result.success()
} catch (e: Exception) {
return Result.retry()
}
}
Handling recurring events
For recurring events like birthdays, we calculate and schedule the next occurrence during cleanup:
// class CleanupWorker
private suspend fun scheduleNextOccurrence(event: Event) {
val nextEventTime = calculateNextOccurrence(event)
val nextEvent = event.copy(
id = event.id,
showTime = nextEventTime,
endTime = nextEventTime + event.duration
)
// Register new event using Phase 1 logic
resourceAcquisitionWorkerRegister.registerMetadataFetcher(nextEvent)
}
This architecture ensures that our cleanup process runs independently of whether the user sees the celebration, maintaining optimal app performance and resource management across all usage patterns.
Conclusion
Developing the Android version of the LINE app's Home Tab event effects system has demonstrated how thoughtful architecture and Android's native tools can work together to deliver joy at scale. Here are the key architectural decisions that made our system successful:
Design principles in action
- Early resource preparation with a 5-day buffer ensures reliable celebration delivery
- Clear separation between event discovery and resource management simplifies maintenance
- Independent phases with well-defined responsibilities enable robust error recovery
- User-centric timing decisions, like the 1-hour cleanup delay, enhance the celebration experience
Leveraging Android's foundation
- Kotlin Flow powers our reactive UI system, ensuring smooth visual transitions
- Room database provides reliable local storage with reactive queries
- WorkManager handles precise scheduling across app restarts and device reboots
These technical choices have enabled us to create a celebration system that's not just reliable and maintainable, but also brings genuine moments of delight to millions of users worldwide.
Whether you're building a similar feature or tackling complex Android implementations, we hope our journey offers valuable insights into creating scalable, user-focused systems.