Mobile Development 19 min read

Designing a Multi‑Scope Architecture for Bilibili’s Integrated Playback Page Using Dagger and Kotlin Coroutines

To merge Bilibili’s UGC and OGV playback pages, the team built a multi‑scope architecture that aligns Dagger‑managed dependency‑injection components with Kotlin CoroutineScopes—PageScope, BizScope, and VideoPlayScope—so each business module is instantiated only when needed, automatically disposed with its parent lifecycle, and remains memory‑leak‑free.

Bilibili Tech
Bilibili Tech
Bilibili Tech
Designing a Multi‑Scope Architecture for Bilibili’s Integrated Playback Page Using Dagger and Kotlin Coroutines

Bilibili’s video playback page is a core consumption scenario for its app. The existing playback pages (UGC, Story, Live, and OGV) each contain a large amount of business logic, resulting in high code complexity and maintenance cost.

To reduce development cost and improve efficiency, the company decided to merge the UGC playback page with the OGV playback page. This creates a new “integrated playback page” that must host the combined functionality of three previous pages, demanding a new architectural approach.

Requirement Clarification

The goal is to build an architecture that can accommodate multiple business modules while keeping the codebase maintainable. The six functional groups identified are:

General video playback, UGC‑specific playback, OGV‑specific playback

General page interaction, UGC‑specific interaction, OGV‑specific interaction

Each group contains many sub‑features, many of which share common dependencies (e.g., a shared casting SDK). The challenge is to model these inter‑related features without creating a tangled, monolithic code structure.

Analysis of the Problem

The problem is reduced to three steps:

How to create business units (modules) elegantly.

How to establish their dependency relationships safely.

How to minimize the maintenance cost of the overall graph.

Creating business units via constructors can lead to circular dependencies. The solution is to use a dependency‑injection (DI) framework that can manage object lifecycles and resolve dependencies automatically.

To illustrate, the article presents a pseudo‑code example that listens to screen‑state and playback‑state events and updates UI visibility. The initial version uses manual registration/unregistration, which is verbose and prone to back‑pressure issues.

// Pseudo‑code (manual registration)
@BizScope
class BizService @Inject constructor(
    screenStateService: ScreenStateService,
    playingStateService: PlayingStateService
) {
    var isHalfScreen = false
    var isPause = false
    val vm = BizViewModel
    val screenStateCallback = object : ScreenStateCallback {
        override fun onScreenStateChange(isHalf: Boolean) { isHalfScreen = isHalf }
    }
    val playingStateCallback = object : PlayingStateCallback {
        override fun onScreenStateChange(isPause: Boolean) { this.isPause = isPause }
    }
    fun start() {
        screenStateService.register(screenStateCallback)
        playingStateService.register(playingStateCallback)
    }
    fun refreshViewState() { vm.showTargetView = isHalfScreen && isPause }
    fun clear() {
        screenStateService.unregister(screenStateCallback)
        playingStateService.unregister(playingStateCallback)
    }
}

Replacing the manual approach with Kotlin Coroutines simplifies the code dramatically. Using combine , distinctUntilChanged , and collectLatest ensures that UI updates only occur when the combined state actually changes and that rapid state changes do not cause back‑pressure.

// Pseudo‑code (Kotlin Coroutines)
@BizScope
class BizService @Inject constructor(
    coroutineScope: CoroutineScope,
    screenStateService: ScreenStateService,
    playingStateService: PlayingStateService
) {
    val vm = BizViewModel
    init {
        coroutineScope.launch {
            combine(
                screenStateService.isHalfScreenStateFlow,
                playingStateService.isPauseStateFlow
            ) { half, pause -> half to pause }
                .distinctUntilChanged()
                .collectLatest { (half, pause) ->
                    vm.showTargetView = half && pause
                }
        }
    }
}

The article then defines the concept of a Scope – a logical unit with a well‑defined lifecycle. Scopes can be nested (parent‑child) and are used both by Dagger (DI) and Kotlin Coroutines. The integrated playback page is decomposed into three hierarchical layers, yielding five concrete scopes:

PageScope : lives for the whole page (lifecycle of the Activity/Fragment).

BizScope : child of PageScope, either UGC‑BizScope or OGV‑BizScope, created based on the server‑returned business type.

VideoPlayScope : child of the corresponding BizScope, handling video‑specific logic (e.g., charging, VIP checks).

By aligning each business unit with the appropriate scope, developers avoid attaching long‑lived objects to a page‑wide lifecycle when their actual lifetime is much shorter. This reduces bugs caused by stale data and memory leaks.

Concrete Solution

The chosen DI framework is Dagger (instead of Koin or Hilt) because Bilibili’s Android codebase already uses Dagger and compile‑time validation is preferred. Three Dagger scope annotations are defined: PageScope , BizScope , and OGVBizScope . Corresponding components (PageComponent, UGCBizComponent, OGVBizComponent, etc.) form a parent‑child hierarchy that mirrors the runtime scopes.

CoroutineScope qualifiers ( PageCoroutineScope , BizCoroutineScope , EpCoroutineScope ) are also defined so Dagger can inject the correct CoroutineScope into each component.

The following snippet shows how a BizScopeDriver uses the page‑level CoroutineScope to listen to the page‑level data flow, then creates the appropriate child component (UGC or OGV) via Dagger, and finally keeps the anchor object alive until cancellation.

// Pseudo‑code (Dagger + Coroutines driver)
@PageScope
class BizScopeDriver @Inject constructor(
    viewRepository: ViewRepository,
    @PageCoroutineScope private val coroutineScope: CoroutineScope
) {
    init {
        coroutineScope.launch {
            viewRepository.collectLatest { result ->
                coroutineScope {
                    when (result.bizType) {
                        BizType.UGC -> {
                            val component = UGCComponentBuilder
                                .bindBizCoroutineScope(this)
                                .build()
                            component.ugcScopeAnchor()
                        }
                        BizType.OGV -> {
                            val component = OGVComponentBuilder
                                .bindBizCoroutineScope(this)
                                .build()
                            component.ogvScopeAnchor()
                        }
                        else -> {
                            // No business, keep coroutine alive
                        }
                    }
                    awaitCancellation()
                }
            }
        }
    }
}

With this design, each business module is instantiated only when needed, automatically disposed when its parent scope ends, and runs inside a CoroutineScope that respects the Android lifecycle, preventing memory leaks.

Conclusion

The article demonstrates a practical approach to decomposing a complex Android page into multiple, lifecycle‑aware scopes using Dagger and Kotlin Coroutines. The three‑step recipe is:

Split the large business graph into independent scopes that reflect real lifetimes.

Develop each business unit within its own scope, leveraging CoroutineScope for asynchronous work.

Drive the creation and destruction of scopes via event‑driven flows, ensuring that only the necessary modules are active at any time.

Although the implementation uses Dagger and Coroutines, the same principles can be applied with other DI frameworks (Koin) or reactive libraries (RxJava) as long as the core idea—event‑driven, scope‑based module activation—is preserved.

Mobile DevelopmentAndroidKotlindependency injectioncoroutinesDaggerScope Architecture
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.