Fundamentals 10 min read

Mastering UseCase: Avoid Common Pitfalls and Follow Best Practices in Android Architecture

This article examines the role of UseCase in Android's modern architecture, clarifies its responsibilities, demonstrates proper naming, thread‑safety, signature design, referential transparency, and interface abstraction, and provides concrete Kotlin examples of both incorrect and correct implementations.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Mastering UseCase: Avoid Common Pitfalls and Follow Best Practices in Android Architecture

Introduction

Android’s latest official architecture adds a Domain layer that is composed of individual UseCase classes. A UseCase should encapsulate a single, reusable piece of business logic that is independent of UI and data‑access concerns.

UseCase Responsibility

Each UseCase must obey the single‑responsibility principle: it performs one well‑defined action and can be invoked from a ViewModel or another UseCase. The class should be lightweight enough to be instantiated by a DI container (e.g., @Inject).

class SendPaymentUseCase(private val repo: PaymentRepo) {
    suspend operator fun invoke(amount: Double, checkId: String): Boolean {
        val transactionId = repo.startTransaction(checkId)
        repo.sendPayment(amount, checkId, transactionId)
        return repo.finalizeTransaction(transactionId)
    }
}

Naming Conventions

UseCase names follow the pattern verb‑noun‑UseCase in present tense, e.g., FormatDateUseCase, GetChatUserProfileUseCase. The invoke operator can be overloaded so the UseCase can be called like a function.

class SendPaymentUseCase @Inject constructor(private val repo: PaymentRepo) {
    // concise call
    suspend operator fun invoke(): Boolean = /* … */

    // alternative explicit name
    suspend fun send(): Boolean = /* … */
}

class HomeViewModel @Inject constructor(private val sendPaymentUseCase: SendPaymentUseCase) {
    fun startPayment() {
        sendPaymentUseCase()          // calls invoke
        sendPaymentUseCase.send()     // calls explicit method
    }
}

Thread‑Safety (Main‑Safe)

A UseCase must be safe to call from the main thread. Any CPU‑intensive work should be dispatched to a background CoroutineDispatcher inside the UseCase.

// Bad: heavy work runs on the main thread
class BadUseCase @Inject constructor() {
    suspend operator fun invoke(): List<String> {
        val list = mutableListOf<String>()
        repeat(1000) { list.add("Item $it") }
        return list.sorted()
    }
}

// Good: work is shifted to an IO dispatcher
class GoodUseCase @Inject constructor(@IoDispatcher private val dispatcher: CoroutineDispatcher) {
    suspend operator fun invoke(): List<String> = withContext(dispatcher) {
        val list = mutableListOf<String>()
        repeat(1000) { list.add("Item $it") }
        list.sorted()
    }
}

Do not add an extra dispatcher if the repository itself is already main‑safe; unnecessary context switches waste resources.

Signature Design

UseCase APIs must not depend on Android framework types (e.g., Context) or UI models. They should accept plain data objects and return domain‑level results such as error codes or domain models.

// Bad: directly uses Android Context
class AddToContactsUseCase @Inject constructor(@ApplicationContext private val ctx: Context) {
    operator fun invoke(name: String?, phone: String?) {
        ctx.addToContacts(name, phone)
    }
}

Referential Transparency

Ideally a UseCase behaves like a pure function: given identical inputs it always yields identical outputs and does not retain mutable state.

// Bad: mutable internal list breaks idempotence
class PerformSomethingUseCase @Inject constructor() {
    private val list = mutableListOf<String>()
    suspend operator fun invoke(): List<String> {
        repeat(1000) { list.add("Item $it") }
        return list.sorted()
    }
}

Interface Abstraction (Optional)

Defining a single‑method interface can help enforce the single‑responsibility rule, but it is often unnecessary. Kotlin’s fun interface provides a concise way to declare such an interface.

// Traditional interface
interface GetSomethingUseCase {
    suspend operator fun invoke(): List<String>
}

class GetSomethingUseCaseImpl @Inject constructor(private val repo: ChannelsRepository) : GetSomethingUseCase {
    override suspend operator fun invoke(): List<String> = repo.getSomething()
}

// Kotlin function interface
fun interface GetSomethingUseCase : suspend () -> List<String>

val getSomethingUseCase = GetSomethingUseCase { repository.getSomething() }

When a UseCase needs to be injected, bind the implementation in the DI container (e.g., using @Bind or a module provider). For simple, stateless UseCases you can also declare them as an object.

Summary of Best Practices

Encapsulate a single, reusable business operation.

Name the class with a clear verb‑noun‑UseCase pattern.

Make the UseCase main‑safe; shift heavy work to a dispatcher.

Avoid Android framework types in the public signature.

Keep the implementation pure and free of mutable internal state.

Use a single‑method interface or Kotlin fun interface only when it adds real value.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AndroidKotlinbest practicesClean ArchitectureDomain LayerUseCase
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

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.