Many examples in Kotlin assume that you are always on the "happy path". But real-world applications must deal with a lot of types of errors: validation, network, and others.
Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
View.kt // Action dispatcher in a View okButton.setOnClickListener { val name = nameTextField.value val email = emailTextField.value val action = Action.UpdateUser(userId, name, email) viewModel.dispatch(action) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
Name is blank or email not valid Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
Name is blank or email not valid Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
Name is blank or email not valid User not found Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { validate(action) val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { val isValid = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { val isValid = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result { val isValid = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
• Each use case will be equivalent to a single function • The function will return a sum type with two cases: Success or Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
• Each use case will be equivalent to a single function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
• Each use case will be equivalent to a single function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
• Each use case will be equivalent to a single function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • Each step will be combined into a single result
public inline fun Result.map(transform: (value: T) -> R): Result { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.map(transform: (value: T) -> R): Result { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.map(transform: (value: T) -> R): Result { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.map(transform: (value: T) -> R): Result { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.flatMap( transform: (value: T) -> Result ): Result { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.flatMap( transform: (value: T) -> Result ): Result { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.flatMap( transform: (value: T) -> Result ): Result { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
public inline fun Result.flatMap( transform: (value: T) -> Result ): Result { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
GetUserById.kt fun getUserById(input: Action.UpdateUser): Result { val user = repository.getUserByIdOrNull(input.id) return when { user == null -> Result.failure("User not found.") else -> Result.success(user) } }
NameIsBelowMaxLength.kt fun nameIsBelowMaxLength(user: User) = when { user.name.length > 50 -> Result.failure("Name must not be longer than 50 chars.") else -> Result.success(user) }
• A strategy for handling errors in a functional way • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow. “Do or do not, there is no try”
• A strategy for handling errors in a functional way • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow. “Do or do not, there is no try”
• A strategy for handling errors in a functional way • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow “Do or do not, there is no try”
• A strategy for handling errors in a functional way • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow “Do or do not, there is no try.”