Why Talisman?
-
Safety
Safely and correctly catching exceptions is not hard, but surely annoying to deal with. Introducing
Seal
.Seal
is designed in a very much same way askotlin.Result
except you can return it from methods, works regardless of Kotlin version and doesn't require any command line flags. -
Separation of concerns
You can try as much as you'd like, however rule of having your classes do one thing and one thing only is tempting to break. By using
UseCase
you're no longer tempted by that bad habit. As the name implies it's just a use-case, one input - one output. That's it. -
Extendability
All existing APIs in Talisman are designed to be extensible. No more "oh my gawd, I need to use this variable but it's internal". Extend existing APIs, submit pull requests. Easy.
-
Reusability
Having separated concerns this is already easy but needs to be mentioned. Every bit of code, every use-case can be reused everywhere in your app, significantly cutting down on the amount of copy pasted code.
Please note that all examples assume import of com.skoumal.grimoire.talisman.*
. Make sure to
update import settings in IntelliJ (Android Studio) so you're always importing talisman packages
with an *
UseCases are a simple interface with powerful extension methods. Very simple example is to sum all numbers provided to the interface and return the latest value.
class SumAllAddedNumbers : UseCase<Int, Int> {
@Volatile
private var sum: Int = 0
override suspend fun use(input: Int): Int {
sum += input
return sum
}
}
// and use it like
runBlocking {
val uc = SumAllAddedNumbers()
println(uc(1).getOrNull()) // -> 1
println(uc(2).getOrNull()) // -> 3
println(uc(3).getOrNull()) // -> 6
}
You can see that invocations on the
uc
variable are not done throughuse(input: Int)
method. That's because we're using an orchestrator mechanism integrated to talisman. You should at all times calluc.invoke(input)
or simplyuc(input)
to trigger the orchestrator. You can read more about what they do here
Then you will not be always adding simple numbers, but rather requesting some network information from remote host.
class RegisterUser(
private val api: MyApiService
) : UseCase<RegisterUser.Input, RegisterUser.Output> {
class Input(
// JvmField doesn't generate additional methods (get/set)
@JvmField val name: String,
@JvmField val surname: String,
@JvmField val nickname: String,
@JvmField val password: String
)
class Output(
@JvmField val token: String
)
override suspend fun use(input: Input): Output {
// throws when encounters any error, and that's ok!
val result = api.registerUser(
name = "%s %s".format(input.name, input.surname),
username = input.nickname,
password = input.password
)
return Output(
token = result.token
)
}
suspend operator fun invoke(
name: String,
surname: String,
nickname: String,
password: String
) = invoke(
Input(
name = name,
surname = surname,
nickname = nickname,
password = password
)
)
}
runBlocking {
val uc = RegisterUser(fetchApi())
val output = uc(fetchInput())
}
If you want to use internal input like so you might also want to declare operator function so the UseCase is immediately callable with named params without the need to initialize the
Input
class explicitly.
Another simple interface allowing you to observe the same data that your UseCase already provides. Consider this example with Room database library.
class SumAllWages(
private val dao: WagesDao
) : UseCase<Unit, Double>, UseCaseFlow<Unit, Double> {
override suspend fun use(input: Unit): Double {
return dao.wagesSum()
}
override fun flow(input: Unit): Flow<Double> {
return dao.observeWagesSum()
}
}
runBlocking {
val uc = SumAllWages(provideDao())
val currentValue: Double = uc().getOrThrow()
val flow: Flow<Double> = uc.observe()
}
Once again, notice calling
.observe()
on the UseCase. This is mainly to bypass the annoyingUnit
in parameters.
Seal is in functionality almost indistinguishable from kotlin.Result
. That is, without using
inline classes. With inline classes still being in EAP or Opt-in programs, you cannot return them
as method results and many other issues.
// start with
runSealed {}
// or
val seal = letSealed { listOf() }
// transform values
seal.map {}
seal.mapSealed {}
seal.flatMap {}
seal.listMap {}
// … and many more operators
// and finally get the data output
seal.getOrThrow()
seal.getOrNull()
seal.throwableOrNull()
seal.throwableOrThrow()
seal.onSuccess {}
seal.onFailure {}
seal.onFailureReturn(Unit)
seal.fold({}, {})
Logo by smalllikeart