跪拜 Guibai
← Back to the summary

Main-safe Isn't a Threading Rule — It's the Contract That Makes Android Layers Real

Main-safe: The Real Watershed in Modern Android Architecture

In Android architecture design, "main-safe" is often mentioned, but most people only understand it as a simple statement:

"The Repository should switch threads; the UI layer doesn't need to care about IO."

But if you stop at this understanding, you're still far from grasping its essence.

Main-safe is not merely a threading specification; its essence is an architectural contract—a promise from the Data layer to its callers. And precisely because of this, it becomes a watershed that measures whether an architecture is truly layered.


1. What is Main-safe?

Main-safe means:

The caller can always invoke it on the Main thread without needing to care whether IO / network / database / CPU computation is involved internally.

For example:

viewModelScope.launch {
    val user = getUserUseCase()
}

The caller does not need to write:

withContext(Dispatchers.IO)

Nor does it need to know:


2. The Essence of Main-safe: Does Complexity Spread?

What main-safe truly solves is not just the threading problem, but a deeper issue:

Does complexity spread to the caller?

Let's compare two architectures.

❌ Non-main-safe Architecture (Complexity Exposed)

viewModelScope.launch(Dispatchers.IO) {
    val user = api.getUser()
}

Or:

viewModelScope.launch {
    withContext(Dispatchers.IO) {
        api.getUser()
    }
}

The surface problem is threading, but the essential problem is: the UI layer must know which operations are IO, which are CPU, and when to switch threads.

👉 The UI becomes a "dispatch center."

Threading logic starts appearing everywhere: ViewModel manages threads, UseCase manages threads, Repository may also manage threads. The end result is the same concern being solved repeatedly across three layers.

✅ Main-safe Architecture (Complexity Converged)

The UI layer only expresses intent:

viewModelScope.launch {
    val user = getUserUseCase()
}

The Repository handles IO internally:

class UserRepository(
    private val api: Api
) {
    suspend fun getUser(): User =
        withContext(Dispatchers.IO) {
            api.getUser()
        }
}

If the api above is Retrofit, withContext(Dispatchers.IO) is actually unnecessary.

Comparison Dimension Non-main-safe Main-safe
Does UI care about threads? Yes No
Location of IO logic Everywhere Unified in Data layer
Call complexity High Low
Cognitive load Heavy Light

3. What Contract is Main-safe? — Four "No Leaks"

Understanding main-safe is not as simple as "Repository uses withContext(IO)." Its essence is a set of calling contracts that the Data layer promises to the UI layer, concretely embodied in four dimensions of "no leaks."

1. No Leakage of Threading Model

UI does not need to know:

Callers should not see words like Dispatchers.IO or Dispatchers.Default appear in ViewModel or UseCase—this is the internal implementation of the Data layer, not the responsibility of the UI layer.

2. No Leakage of Blocking Risk

UI does not need to worry about:

The caller only needs suspend fun. Whether behind it is Retrofit's coroutine adapter, Room's suspend query, or manual withContext, none of this should be exposed.

3. No Leakage of Concurrency Semantics

UI does not need to know:

These concurrency details belong to the internal state management of the Data layer. Once the UI layer needs to understand concurrency semantics to call correctly, the boundary has already been broken.

4. No Leakage of Call Cost

UI does not need to guess:

When a caller sees a suspend fun, they should be able to call it confidently inside viewModelScope.launch {} without needing to first check the Repository's implementation to confirm whether it switches threads.


Summary: Main-safe is not describing "how to write a Repository," but rather "what the UI layer does not need to know." These four "no leaks" are the Data layer's promise to the upper layer and the verification standard for whether architectural boundaries truly land.


4. Why is Main-safe the Architectural Watershed?

Because it changes a key question:

Who controls the complexity?

🟥 Non-main-safe: UI Controls Complexity

UI must know IO, network, DB, concurrency—UI becomes an execution layer, not an expression layer.

🟩 Main-safe: Data Layer Controls Complexity

UI does only one thing: issue requests + display state.

Complexity is converged into the Data layer: IO, threading, scheduling, data source switching—all resolved there.


👉 UI No Longer Holds "Execution Authority"

UI only holds:

👉 Data Layer Holds "Execution Authority"

The Data layer is responsible for:


5. The Deeper Problem After Main-safe

Many projects still become messy after achieving main-safe. The reason is:

Main-safe only solves "thread spillover," not "business logic spillover."

❌ Wrong Structure: Repository Makes Business Decisions

class UserRepository {
    suspend fun getUser(): User {
        val user = api.getUser()
        if (user.level > 10) {
            cache.save(user)
        }
        return user
    }
}

The Repository starts making business decisions; the data layer becomes a "god object," and business logic cannot be reused.

✅ Correct Structure: Responsibility Convergence

Repository is only responsible for data:

class UserRepository {
    suspend fun getUser(): User = api.getUser()
}

UseCase is responsible for business decisions:

class GetUserUseCase(
    private val repo: UserRepository,
    private val cache: Cache
) {
    suspend operator fun invoke(): User {
        val user = repo.getUser()
        if (user.level > 10) {
            cache.save(user)
        }
        return user
    }
}

6. Three Levels of Understanding Main-safe

Level Meaning Core Question
Level 1: Thread Safety UI is not responsible for IO Who switches threads?
Level 2: Responsibility Safety Data is not responsible for business judgment Who makes decisions?
Level 3: Structural Safety Each layer does only one thing, no boundary crossing Where is the boundary?

7. Summary

The essence of main-safe is not merely thread switching, but whether complexity converges in the Data layer.

Through four "no leaks"—no leakage of threading model, no leakage of blocking risk, no leakage of concurrency semantics, no leakage of call cost—it constructs the true architectural boundary between the UI layer and the Data layer.

If the Data layer cannot achieve main-safe, complexity remains scattered throughout the call chain, and the so-called "layered architecture" is merely a formal structure, not a genuine boundary design.