Compose and UIs
Arrow provides several features which are very interesting when developing interactive applications, especially in combination with libraries with a similar functional flavor, such as Compose.
Projects using Compose and Arrow can be found in the corresponding section.
All the libraries under the Arrow umbrella are Multiplatform-ready. This means you can use them in your Android applications using Jetpack Compose, and in Desktop, iOS, or Web alongside Compose Multiplatform.
Compose is functional
As opposed to other frameworks where stateful components are the norm, the
architecture
promoted by Compose brings many concepts traditionally associated with a
more functional approach. For example, the UI is defined as a function
taking as arguments the values from the current state. Updating the state
is also explicitly marked, and often separated in a ViewModel
.
As a consequence, Arrow and Compose make great dancing partners. Below we discuss a few feature which we think are an immediately gain for Android (and with Compose Multiplatform, other UI) developers. In the same vein, our design section readily applies to projects using Compose.
Simpler effectful code
Most applications don't live in a vacuum, they need to access other services
or data sources. In those cases we write effectful code, where suspend
and
coroutines become relevant.
Arrow Fx introduces high-level concurrency as a way to simplify code where different actions must happen concurrently, ensuring that all rules of Structured Concurrency are followed, but without the hassle.
class UserSettingsModel: ViewModel() {
private val _userData = mutableStateOf<UserData?>(null)
val userData: State<UserData?> get() = _userData
suspend fun loadUserData(userId: UserId) =
parZip(
{ downloadAvatar(userId) },
{ UserRepository.getById(userId) }
) { avatarFile, user ->
// this code is called once both finish
_userData.value = UserData(
id = userId,
details = user,
avatar = Avatar(avatarFile)
)
}
}
Anything outside your own application is the wilderness: connections are down, services are unavailable. Arrow's resilience module provides several ready-to-use patterns to better handle those situations, including retry policies and circuit breakers.
Built-in error types
One key part of every application is how the domain is modelled. Arrow emphasizes using immutable data. In particular, sealed hierarchies take the important role of describing the different states.
Although every application is unique, a common scenario in interactive applications involve having a "success state" and an "error state". For example, correctly loading the user data, or encountering a problem with connection or authentication. Instead of rolling your own types, Arrow (and our sibling library Quiver) provide out-of-the-box solutions:
Either
describes a model in which the application has either completely succeeded, or some amount of errors have occured. Validation is a prime example, since we usually require for all fields to be valid before moving forward with the data.Ior
introduces a third option, namely succeeding but still with some problems along the way. This type is useful to model domains where we can work with some erroneous or missing information.Outcome
models success, failure, and absence. The latter case is useful when the application may be in loading state: still no problems, but no data ready either.
Given the commonalities, Arrow provides a uniform API to work with values of those types.
Updating the model
One potential drawback of using
immutable data to model your state
is that updating it can become quite tiresome, because Kotlin provides
no dedicated feature other than copy
for this task.
class UserSettingsModel: ViewModel() {
private val _userData = mutableStateOf<UserData?>(null)
val userData: State<UserData?> get() = _userData
fun updateName(
newFirstName: String, newLastName: String
) {
_userData.value = _userData.value.copy(
details = _userData.value.details.copy(
name = _userData.value.details.name.copy(
firstName = newFirstName, lastName = newLastName
)
)
)
}
}
Arrow Optics addresses these drawbacks, providing
tools for manipulating and transforming immutable data.
The code above can be rewritten without boring repetition using
the dedicated copy
for MutableState
.
class UserSettingsModel: ViewModel() {
private val _userData = mutableStateOf<UserData?>(null)
val userData: State<UserData?> get() = _userData
fun updateName(
newFirstName: String, newLastName: String
) {
_userData.updateCopy {
inside(UserData.details.name) {
Name.firstName set newFirstName
Name.lastName set newLastName
}
}
}
}