← Back to list

The Functional Way to Build Reliable Apps — Arrow’s Either

“Good engineers fix bugs. Great engineers design systems where bugs can’t hide.”

Dharma Kshetri · 2025-10-15 21:15 · 32 claps · 4.1 min read paywalled
#either #arrow #android #android-app-development #jetpack-compose
Open on Medium ↗
Wiki topics: PRD · Product Design

The Functional Way to Build Reliable Apps — Arrow’s Either

“Good engineers fix bugs. Great engineers design systems where bugs can’t hide.”

Error handling is one of the most overlooked yet critical parts of Android development. Too often, we rely on exceptions and try-catch blocks, which can clutter code, hide errors, and make testing a nightmare.

Enter **Arrow — a functional programming library for Kotlin that gives developers elegant, type-safe tools to write predictable and maintainable code. One of its most powerful constructs is [Either](https://apidocs.arrow-kt.io/arrow-core/arrow.core/-either/index.html) — a monadic type that represents two possible outcomes: success (Right) or failure* (Left*).

What Is Either?

Either<L, R> is a sealed class that holds one of two possible values:

  • Left<L>→ typically represents failure or error
  • Right<R>→ represents success
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}

It’s the functional equivalent of saying:

“A function either returns the correct result, or an error — but never both.”

This makes your data flow explicit, type-safe, and fully testable.

Why Use Either in Android?

Concept Exceptions Either Error propagation Implicit, via stack trace Explicit, via types Predictability Unclear failure points Controlled and typed Testing Harder to mock failures Easy to simulate both paths Functional purity Side effects Pure and composable

Example:

val result = repository.getUserDetails("123")
val message = result.fold(
    ifLeft = { "Error: $it" },
    ifRight = { "User: ${it.name}" }
)

Project Architecture Overview

We’ll use a Clean MVVM architecture with Arrow’s Either.

com.example.eitherapp/
│
├── data/
│   ├── model/User.kt
│   ├── remote/ApiService.kt
│   └── repository/UserRepository.kt
│
├── domain/
│   ├── model/Failure.kt
│   ├── usecase/GetUserDetailsUseCase.kt
│
├── presentation/
│   ├── viewmodel/UserViewModel.kt
│   ├── ui/UserScreen.kt
│   └── ui/state/UserUiState.kt
│
└── di/
    └── AppModule.kt

Setting Up Dependencies

Add Arrow to your Gradle:

dependencies {
    implementation("io.arrow-kt:arrow-core:2.1.2")
}

Enable plugin (optional):

plugins {
    id("io.arrow-kt.arrow") version "2.1.2"
}

Step 1: Define Model & API

// data/model/User.kt
data class User(
    val id: String,
    val name: String,
    val email: String
)
// data/remote/ApiService.kt
interface ApiService {
    @GET("users/{id}")
    suspend fun getUserDetails(@Path("id") id: String): Response<User>
}

Step 2: Handle Failures with a Sealed Class

// domain/model/Failure.kt
sealed class Failure {
    object NetworkError : Failure()
    object NotFound : Failure()
    data class UnknownError(val message: String?) : Failure()
}

Step 3: Repository Using Either

class UserRepository @Inject constructor(
    private val apiService: ApiService
) {
    suspend fun getUserDetails(id: String): Either<Failure, User> {
        return try {
            val response = apiService.getUserDetails(id)
            if (response.isSuccessful && response.body() != null) {
                Either.Right(response.body()!!)
            } else {
                Either.Left(Failure.NotFound)
            }
        } catch (e: IOException) {
            Either.Left(Failure.NetworkError)
        } catch (e: Exception) {
            Either.Left(Failure.UnknownError(e.message))
        }
    }
}

Step 4: UseCase Layer

class GetUserDetailsUseCase @Inject constructor(
    private val repository: UserRepository
) {
    suspend operator fun invoke(userId: String): Either<Failure, User> {
        return repository.getUserDetails(userId)
    }
}

Step 5: ViewModel — Consuming Either

@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserDetailsUseCase: GetUserDetailsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState
    fun fetchUser(userId: String) {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading
            when (val result = getUserDetailsUseCase(userId)) {
                is Either.Left -> {
                    val message = when (result.value) {
                        is Failure.NetworkError -> "Network connection issue."
                        is Failure.NotFound -> "User not found."
                        is Failure.UnknownError -> "Something went wrong."
                    }
                    _uiState.value = UserUiState.Error(message)
                }
                is Either.Right -> {
                    _uiState.value = UserUiState.Success(result.value)
                }
            }
        }
    }
}

Step 6: Jetpack Compose UI — Reactive & Clean

@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    LaunchedEffect(Unit) {
        viewModel.fetchUser("1")
    }
    when (uiState) {
        is UserUiState.Loading -> LoadingView()
        is UserUiState.Success -> SuccessView((uiState as UserUiState.Success).user)
        is UserUiState.Error -> ErrorView((uiState as UserUiState.Error).message) {
            viewModel.fetchUser("1")
        }
    }
}

Reusable Sub-Composables

@Composable
fun LoadingView() = Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) { CircularProgressIndicator() }
@Composable
fun SuccessView(user: User) = Column(
    Modifier.fillMaxSize().padding(16.dp)
) {
    Text("User Details", style = MaterialTheme.typography.titleLarge)
    Spacer(Modifier.height(8.dp))
    Text("Name: ${user.name}")
    Text("Email: ${user.email}")
}
@Composable
fun ErrorView(message: String, onRetry: () -> Unit) = Column(
    Modifier.fillMaxSize().padding(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) {
    Text(message, color = Color.Red)
    Spacer(Modifier.height(8.dp))
    Button(onClick = onRetry) { Text("Retry") }
}

Step 7: Dependency Injection with Hilt

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
    fun provideApiService(): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://jsonplaceholder.typicode.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

Step 8: Unit Testing with JUnit, Turbine & MockK

Add dependencies:

testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("app.cash.turbine:turbine:1.1.0")
testImplementation("io.mockk:mockk:1.13.10")

Coroutine Test Rule

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineTestRule : TestWatcher() {
    val dispatcher = StandardTestDispatcher()
    override fun starting(description: Description) {
        Dispatchers.setMain(dispatcher)
    }
    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

ViewModel Tests

@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
@get:Rule val coroutineRule = CoroutineTestRule()
    private lateinit var useCase: GetUserDetailsUseCase
    private lateinit var viewModel: UserViewModel
    @Before fun setup() {
        useCase = mockk()
        viewModel = UserViewModel(useCase)
    }
    @Test
    fun `when fetchUser succeeds emit Loading then Success`() = runTest {
        val mockUser = User("1", "Dharma", "dharma@example.com")
        coEvery { useCase("1") } returns Either.Right(mockUser)
        viewModel.fetchUser("1")
        viewModel.uiState.test {
            assert(awaitItem() is UserUiState.Loading)
            val success = awaitItem() as UserUiState.Success
            assertEquals("Dharma", success.user.name)
            cancelAndIgnoreRemainingEvents()
        }
    }
    @Test
    fun `when fetchUser fails emit Loading then Error`() = runTest {
        coEvery { useCase("404") } returns Either.Left(Failure.NotFound)
        viewModel.fetchUser("404")
        viewModel.uiState.test {
            assert(awaitItem() is UserUiState.Loading)
            val error = awaitItem() as UserUiState.Error
            assertTrue(error.message.contains("not found", true))
            cancelAndIgnoreRemainingEvents()
        }
    }
}

Test Output

> Task :testDebugUnitTest
UserViewModelTest > when fetchUser succeeds emit Loading then Success PASSED
UserViewModelTest > when fetchUser fails emit Loading then Error PASSED

Remember:

Concept              Description
-------              -----------
Either               Type-safe, functional error handling
No try/catch         Clear data flow with predictable results
Clean Architecture   Repository → UseCase → ViewModel → UI
Reactive UI Jetpack  Compose observes StateFlow
Unit Tested          Verify both success and failure paths
Composable Retry     Clean, reusable error handling in UI

My Thoughts

Functional programming might seem abstract at first, but once you integrate tools like Arrow’s Either, you’ll notice:

  • Cleaner architecture
  • Safer code
  • More predictable UI state management
  • And simpler testing

In a world of asynchronous calls and complex data flows, Either is your best ally for reliability and clarity.

Connect: LinkedIn | GitHub | Website

[embed]The Android Engineer's Interview Guide: Preparing for Success in 2025 The Android Engineer's Interview Guide: Preparing for Success in 2025 [Kshetri, Dharma] on Amazon.com. FREE shipping…a.co

📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! 👇

Happy coding! 💻


메타데이터
post_id
2fc3ed297297
slug
the-functional-way-to-build-reliable-apps-arrows-either-2fc3ed297297
url
https://medium.com/@dharmakshetri/the-functional-way-to-build-reliable-apps-arrows-either-2fc3ed297297
canonical_url
https://medium.com/@dharmakshetri/the-functional-way-to-build-reliable-apps-arrows-either-2fc3ed297297
author_url
https://medium.com/@dharmakshetri
status
ok
fetched_at
2026-07-16 19:00:48