7 Kotlin Libraries and Frameworks for Backend Development You Should Know
Let me venture to say that nowadays, a modern programming language without a rich ecosystem of libraries and frameworks will sooner or later become just a fancy toy with no practical usage.
Fortunately, this doesn’t apply to Kotlin at all. Why? Firstly because of Kotlin's great interoperability with Java - the whole Java ecosystem can be easily used with Kotlin without much fuss. Secondly because of the already great number of native Kotlin libraries and frameworks available, written purely in this language and dedicated to it.
Ok, so you are here, reading this blog post. Maybe because you are a backend Java developer and you just decided to give Kotlin a try for your next project. Or maybe you are a mobile Kotlin developer, already familiar with Kotlin, but you’d like to transition to backend development while sticking to your favorite language of choice. One way or another, a question might arise: are there any Kotlin native libraries and frameworks among the ones mentioned above that will allow you to unleash the full potential of Kotlin on the backend?
The short answer is: yes.
The longer answer is: yes, of course.
Let’s dive together into the world of some of the greatest Kotlin libraries and frameworks out there, that will help you efficiently build production-ready, enterprise-grade backend applications the Kotlin way!
Ktor - a Kotlin framework for building web apps
In my experience, when people say “backend development” they usually mean web apps or microservices. Thus, to support backend development, a programming language needs to have some comprehensive web frameworks in its toolbox. And that’s also true for Kotlin.
If you’re coming from the Java world, most probably the first thing that comes to your mind when thinking about web frameworks is Spring Boot. So a question might arise: Can I just write Kotlin applications using Spring Boot? And don’t worry - you absolutely can, and that’s quite common practice - Spring Boot plays very well with Kotlin. But if you’d like to go a step further and be able to take full advantage of Kotlin while developing your next server-side application, then the Ktor framework might be your thing.
Ktor is a framework for building web applications developed by JetBrains, the same people that brought Kotlin to life. This fact itself already shows that the framework and the language are meant to synergize and support each other’s features. And that becomes clear when we start writing some code with Ktor. Coroutines, extension functions, trailing lambdas - all those Kotlin features are first-class citizens and foundations of Ktor.
Ktor is also meant to be as lightweight and simple as possible. It is designed with asynchronicity in mind and can be hosted on any servlet container with Servlet 3.0+ API support such as Tomcat, or standalone using Netty or Jetty. Take a look at the code snippet below, which shows the minimal Ktor application serving a REST endpoint:
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
install(ContentNegotiation) {
json()
}
routing {
get ("/greetings/{name}") {
val name = call.parameters["name"] ?: "Anonymous"
call.respond(Greeting("Hello $name!"))
}
}
}
@Serializable
data class Greeting(val message: String)
Of course, Ktor can do so much more than that. It’s a modular framework, whose philosophy is to allow Kotlin developers to pick exactly what they need for their application and nothing more. As a result, Ktor offers many plugins that you can include in the project either during the project creation or at a later stage, in the form of Gradle plugins and dependencies. The library of available plugins is already very rich and offers various add-ons from categories like security, HTTP, monitoring, databases, and many more.
Apart from that, Ktor offers plenty of other features that you might expect from a mature web framework, some of them being:
- property-based configuration
- HTTP/2 support (including SSL certificates)
- Content-negotiation and serialization
- Web sockets
- Various templating libraries (you might want to take a look at our another blog post about Pebble templating vs. HTML DSL in Ktor)
- HTTP sessions
Similarly, as for Spring Boot, Ktor comes with a convenient project builder. This tool allows you to quickly configure and generate the initial structure of your project, together with necessary dependencies, configuration files, and sample codes. The builder can be either used via a web browser or directly from within the IntelliJ IDEA.
Ktor certainly deserves a blog post on its own, and we're just working on something in that space, so stay tuned!
Koin - a lightweight dependency injection framework for Kotlin
If you’ve not been in software development since yesterday, you must have heard about Dependency Injection (DI). This concept, somehow related to the famous Uncle Bob Martin’s Dependency Inversion principle from SOLID, has been adopted and implemented by many libraries and frameworks and is nowadays widely used across the industry. Enough to say, that Spring itself started as a dependency injection container before it finally became a framework so huge and multipurpose, that it effectively needed its own framework (I’m looking at you, Spring Boot!). So it won’t be surprising if I tell you that Kotlin also offers a solution in that space. Please welcome Koin - a lightweight dependency injection framework for Kotlin.
Similarly, as with all the libraries or frameworks described here, Koin is also written purely in Kotlin. Thus, one of its selling points and advantages is its really concise, Kotlin-way syntax, which greatly utilizes the DSL-like approach to writing code.
Another great thing about Koin is that it’s multi-platform by design. It can be used in server-side applications, both in bare Kotlin or Ktor, as well as in client-side applications, be it native Android, Jetpack Compose or mobile multiplatform apps. Going even further, Koin's official documentation explicitly describes those use cases.
Now, let's see the simplest possible usage of Koin in a bare backend application using the Koin DSL approach:
interface GreetingService { // 1
fun sayHello(name: String)
}
class GreetingServiceImpl : GreetingService { // 2
override fun sayHello(name: String) = println("Hello $name!")
}
val greetingsModule = module { // 3
single<GreetingService> { GreetingServiceImpl() }
}
class GreetingsApplication : KoinComponent { // 4
private val greetingsService: GreetingService by inject() // 5
fun run() = greetingsService.sayHello("Rupert")
}
fun main() {
startKoin { // 6
modules(greetingsModule)
}
GreetingsApplication().run()
}
As you can see, there are several important stages here:
- We define an interface, which we will then use as a contract for our dependency. It's worth noting that this step is optional. We can inject actual implementations as dependencies directly without declaring intermediate interfaces.
- Then we declare the actual implementation of this interface.
- This is when Koin first comes into play - by defining a module we can bind actual implementations to all of our dependencies. In this case, we define that the
GreetingService
interface will be a singleton backed byGreetingServiceImpl
class. Apart from singletons, we can also define factories. It’s worth mentioning that Koin allows us to define multiple modules with various sets of dependencies. - To be able to inject the dependencies managed by Koin, we need a class that extends
KoinComponent
. In this case, it’s aGreetingsApplication
, but imagine it can be any entry point class in our application, like REST controller or JMS event listener. - This is how the actual dependency injection is invoked in Koin. Inside a
KoinComponent
we can inject any dependency defined in any Koin module using the by inject() delegate function. - Finally, to make it all work, we need to start the Koin container at our main entry point. The
startCoin
function accepts all the modules we want to include in our application, but it also allows us to configure it further by specifying a logger or some external properties.
If you’re a fan of annotation magic, Koin also has something for you. You can achieve the same as above by using annotations:
interface GreetingService { // 1
fun sayHello(name: String)
}
@Single
class GreetingServiceImpl : GreetingService { // 2
override fun sayHello(name: String) = println("Hello $name!")
}
@Module
@ComponentScan("org.example")
class GreetingsModule // 3
class GreetingsApplication : KoinComponent { // 4
private val greetingsService: GreetingService by inject() // 5
fun run() = greetingsService.sayHello("Rupert")
}
fun main() {
startKoin { // 6
modules(GreetingsModule().module)
}
GreetingsApplication().run()
}
And this is what is happening here:
- Nothing new, we just define an interface for our dependency, the same as in the example above.
- The actual implementation of the interface. This time, however, we used a @Single annotation on the
GreetingServiceImpl
. This is the annotation way to say that this class is going to be a singleton dependency bound to theGreetingService
interface. - At this stage, we’re defining a module class. This class is like a marker telling Koin what the scope of the module is and where it should look for components (i.e. annotated classes) that are part of the module.
- The
KoinComponent
is where the dependencies can be injected. This is exactly the same as in the example without annotations. - Annotations just change how the dependencies are defined, but not how they are injected. So this line is the same as in the DSL version of Koin API.
- Here we’re again starting the Koin container with modules. But mind how differently we pass modules to it in comparison with the previous example. This time we’re calling a
module
property on ourGreetingsModule
class defined in point 3. Mind that we haven’t declared such property in our class, and it’s not extending anything. This is where the annotation magic comes into play - Koin uses a KSP (Kotlin Symbol Processing) plugin that reads the Koin annotations and generates required methods and properties in compile time.
That’s it when it comes to the Koin basics. Just to remind you - as I mentioned before, Koin provides full interoperability with the Ktor framework, so that they can be used together seamlessly to build web applications even more effectively.
Exposed - a lightweight SQL and ORM persistence framework
It’s hard to imagine an IT system without persistence. Sooner than later, you’ll need to store some data in your backend application. Considering that nowadays the most popular databases are still relational DBs, you’d most likely want to have a solid SQL-based persistence framework available for you when working with Kotlin. And, fortunately, there is a perfect fit for your needs. It’s called Exposed.
Exposed is a lightweight SQL and ORM persistence framework written in Kotlin. Similarly to Ktor, it’s also maintained by JetBrains which, just to remind you, is the company behind Kotlin language itself.
Exposed works on top of the Kotlin JDBC driver and, at the time of this writing, supports the following databases: H2, MariaDB, MySQL, Oracle, PostgreSQL, Microsoft SQL Server, SQLite. To configure a DB connection, you only need to do the following:
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
Remember that the first parameter is a connection URL and the second is a DB driver. This is of course just one of the ways the DB connection can be configured, and it’s the simplest one. There are also more complex calls to Database.connect()
, with more parameters. What is more, there is a possibility of configuration DB connection by passing a javax.sql.DataSource
instance into Database.connect()
, which brings all the sophisticated features of the DB connection configuration that DataSource provides, like DB connection pooling, etc.
Apart from configuring a DB connection, we also need some database tables to work on. This is how a table can be defined with Exposed:
const val VARCHAR_LENGTH = 128
object Employees: IntIdTable("employees") {
val firstName = varchar("first_name", VARCHAR_LENGTH)
val lastName = varchar("last_name", VARCHAR_LENGTH)
val email = varchar("email", MAX_VARCHAR_LENGTH).uniqueIndex()
val isFullTime = bool("full_time").default(true)
}
Our sample table above has just columns of simple types, but it’s worth mentioning that Exposed comes with some optional extensions, which allow us to use more complex and specific data types like encrypted data, datetime, JSON, or money.
Ok, so we have a DB connection configuration and a table. It’s time for some queries! One of the most interesting things about Exposed is that it gives Kotlin developers the flexibility to choose between database interaction styles they prefer: either SQL DSL or lightweight ORM with DAO and entities. Below I’m gonna show you some minimal code snippets, only to give you an idea of how those two database access styles look like in Exposed.
First, take a look at the SQL DSL, which is a default way of querying the database in Exposed:
// insert
val employeeId = Employees.insert {
it[firstName] = "John"
it[lastName] = "Doe"
it[email] = "john.doe@example.com"
} get Employees.id
// select all
val allEmployees = Employees.selectAll().toList()
// select aggregated
Employees.select(Employees.id.count(), Employees.isFullTime)
.groupBy(Employees.isFullTime).forEach {
println("${it[Employees.isFullTime]}: ${it[Employees.id.count()]} ")
}
// update
Employees.update({ Employees.id eq employeeId }) {
it[isFullTime] = false
}
// select single row
val updatedEmployee = Employees.select(
Employees.firstName,
Employees.lastName,
Employees.isFullTime
).where(Employees.id eq employeeId).single()
// delete
Employees.deleteWhere { id eq employeeId }
Then, let's take a look at how we can interact with the data inside the employees table using Exposed DAO. First, we need to define an entity class for Employee:
class EmployeeEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<EmployeeEntity>(Employees)
var firstName by Employees.firstName
var lastName by Employees.lastName
var email by Employees.email
var isFullTime by Employees.isFullTime
}
Once we have our entity, we can now invoke various operations on it:
// insert
val employee = EmployeeEntity.new {
firstName = "John"
lastName = "Doe"
email = "john.doe@example.com"
}
// select all
val allEmployees = EmployeeEntity.all().toList()
// update
employee.isFullTime = false // this will be flushed to the DB at the end of transaction
val updatedEmployee = EmployeeEntity.findByIdAndUpdate(employee.id.value) {
it.email = "john.doe@newdomain.com"
}
// select single row
val foundEmployee = EmployeeEntity.findById(employee.id.value)
// delete
employee.delete()
It is also worth mentioning that Exposed is fully supported by the Ktor framework and is available as one of its plugins.
Would you like to learn more about the Exposed framework? Stay tuned for our upcoming blog posts about Exposed!
Kotest - testing framework for Kotlin
I hope it won’t be an exaggeration to say that nowadays it’s hard to imagine a software without tests (unless it’s some kind of PoC or a prototype). Thus a reliable and convenient testing framework is a must for any programming language. Does Kotlin offer something like this?
Ladies and gentlemen, I give you Kotest!
Before I start elaborating on Kotest features a bit more, I’m going to stop now for a second and try to address a question or a doubt that might have just popped up in your mind: if Kotlin works so well with Java, then why another testing framework? Why not just use, for example, a well-established and battle-tested JUnit
? Well, my answer would be: you absolutely can do it! But you’ll be missing out a lot. That’s because Kotest, like every other tool mentioned in this article, makes the best out of Kotlin's built-in features, such as its sweet and concise syntax, extension functions, infix methods, coroutines, etc.
With that in mind, let’s take a closer look at some of Kotest’s most important features. However, keep in mind that we are going to scratch the surface only, because Kotest is really an extensive and rich framework.
Kotest is designed to be modular and for now, it consists of three main modules: Test Framework, Assertions Library, and Property Testing. They all can be used together or in conjunction with other testing frameworks, like JUnit or Spock. Kotest also goes very well with Spring tests. Let me quickly go through each of the components:
- Test Framework is a core and foundation of Kotest. It allows us to define test cases, extend them with lifecycle hooks, and execute them on many different supported platforms.
- Assertions Library is a module of Kotest that provides a really comprehensive set of various matchers, for different data types.
- Property Testing is all about automatically generating a bunch of random test data, including various edge cases and regular cases, and running the test multiple times feeding all this generated data to it.
As I mentioned before, Kotest is a huge framework with many features and curiosities, and I won’t be able to fit them all in this article, so just to name a few that might be most interesting:
- Testing styles - many testing frameworks offer one, specific way of defining test cases, but Kotest goes far beyond that. It basically offers ten different testing styles! That means every developer or team can pick the syntax that suits them best.
- Extensions - we are not limited to pre-defined, popular lifecycle hooks, like
beforeSpec
orafterTest
. Kotest also offers the capability to implement and plug in our custom extensions and hooks. - Conditional evaluation - ability to fully disable specific test cases, or enable/disable them just for some specific conditions.
- Coroutines support - the ability to run test cases inside coroutines, with a possibility to manipulate time, like pretending that some time has passed inside a test case, etc.
- Data-driven testing - running the same test case with different data sets. Known also as parametrized testing.
- Custom matchers - if you’re still lacking some matchers in the standard library, you might want to implement your own. Remember that you can fully utilize Kotlin extension functions and infix methods for that!
- Soft assertions - a useful feature if you don't want your test to fail fast, but to also invoke other assertions before reporting failure.
- Non-deterministic testing - as the name suggests, the set of various tools and utilities to test potentially non-deterministic behaviors, like waiting for something to happen, retrying, etc.
- Custom generators for property-based tests - if you think that the set of predefined generators is not enough for you, or if you’d like to define a generator for your own classes, you can do it with Kotest and its custom generators.
- Shrinking - a strategy specific to property-based testing, which is about simplifying failing cases to find out the minimal reproducible case. Kotest does it all for you.
Talk is cheap, show me the code! Let me then show you a small snippet of the code demonstrating Kotest capabilities in a nutshell:
class MyFunSpec: FunSpec({
test("basic Kotest features and matchers") { // test case definition in Fun Spec style
(2 + 2) shouldBe 4 // basic, universal matcher
"text" shouldContain "x" // string-specific matcher
"{\"someProp\":\"value\"}" shouldContainJsonKey "someProp" // json matcher
val numbers = listOf(2, 4, 6, 8, 10)
numbers.forAll { it.shouldBeEven() }
numbers.forExactly(2) { it shouldBeGreaterThan 7 } // sample collection inspectors
}
test("test fibonacci for n > 1 using forAll") {
forAll(Arb.int(2..10)) { n -> // property-based test using forAll with Arbitrary generator
fibonacci(n) == fibonacci(n - 1) + fibonacci(n - 2)
}
}
test("test fibonacci for n > 1 using checkAll") {
checkAll(Exhaustive.ints(2..10)) { n -> // property-based test using checkAll with Exhaustive generator
fibonacci(n) shouldBe fibonacci(n - 1) + fibonacci(n - 2)
}
}
})
class MyBehaviorSpec: BehaviorSpec({
given("Number 2") { // test case definition in Behavior Spec style
val num1 = 2
and("another number 2") {
val num2 = 2
`when` ("adding those numbers") {
val result = num1 + num2
then("the result should be 4") {
result shouldBe 4
}
}
}
}
})
As you can see, Kotest is a comprehensive and deep testing framework, with so many great features. I encourage you to try this out on your own! Please also keep an eye out for our blog, as I’m currently working on a separate article dedicated entirely to Kotest.
MockK - a Kotlin library for mocking dependencies in unit tests
Let’s stay for a while in a testing vibe. This time I’m going to show you a library that greatly extends and integrates with Kotest, but can be as well used in conjunction with any other testing framework. Let me briefly introduce you to the MockK - a library for mocking dependencies in unit tests.
Actually, MockK can do most of the things that you may expect and know from other mocking libraries, like Mockito or Spock, e.g. partial mocking, spies, interaction verification, argument matchers, and argument capturing. Thus I won’t go through them one by one in detail.
Let me just show you an aggregated example of those simple use cases in the code:
test("basic transfer money test with mockk") {
val bankAccountMock = mockk<BankAccount>() // basic mock
every { bankAccountMock.transferMoneyTo(any()) } returnsMany listOf(0.0, 10.0, 20.0) // subsequent calls will return different results
every { bankAccountMock.transferMoneyTo(any()) } returns 0.0 andThen 10.0 andThen 20.0 // same as above
every { bankAccountMock.transferMoneyTo(more(1000.0)) } throws IllegalArgumentException("Too much money!") // argument matcher
val actualBankAccount = BankAccount()
val bankAccountSpy = spyk(actualBankAccount) // a spy
val transferMoneySlot = slot<Double>()
every { bankAccountSpy.transferMoneyFrom(capture(transferMoneySlot)) } returns 100.0 // capturing arguments
MoneyTransfer().transfer(20.0, bankAccountSpy, bankAccountMock)
val unusedBankAccountMock = mockk<BankAccount>()
verify { // interactions verification
bankAccountMock.transferMoneyTo(20.0)
bankAccountSpy.transferMoneyFrom(20.0)
unusedBankAccountMock wasNot Called
}
transferMoneySlot.captured shouldBe 20.0 // verifying the captured argument
}
Now that you have the idea of the MockK basics, I’d like to draw your attention to some of the outstanding MockK features that make it so unique among other non-Kotlin mocking libraries:
- Object mocks - if you’re familiar with Kotlin
objects
, then you know that they are like singletons, with just a single instance across the whole application. MockK is able to create mocks for objects, overriding the actual implementation of their methods. - Hierarchical mocking - MockK makes great use of Kotlin's concise syntax to provide hierarchical mocks, where you can easily and clearly mock classes and their nested properties.
- Coroutines - as the native Kotlin library befits, MockK has built-in support for mocking suspend functions.
- Mocking top-level functions - another feature of MockK, addressing the Kotlin-specific syntax is the ability to mock top-level functions.
- Mocking extension functions - similarly as for top level functions, extension functions can be mocked as well with MockK.
It’s worth exploring those features in detail and generally trying out MockK, either combined with Kotest or any other testing framework of your choice.
Konform - Kotlin validation library
Validation is a crucial part of any business logic in our applications. Input data must be validated to ensure reliability and safety when processed or stored.
The next library I’m going to show you is purely about that.
Konform is a Kotlin validation library with quite a rich set of features and a very convenient and concise DSL. Even if Konform has only one purpose - to validate your data - it has surprisingly a lot of small flavors and use cases for various situations. They are all clearly described on the Konform website, so instead of elaborating on each of them separately and repeating all the contents here, let me just give you a quick sample of the library’s capabilities, so that you can get an idea of what to expect.
So assume we have the following model in our application:
data class Order(
val id: String,
val lines: List<OrderLine>,
val invoiceId: String?
)
data class OrderLine(
val id: String,
val product: Product
)
data class Product(
val name: String,
val price: Double
)
We can define the following validators for our classes:
val validateOrder = Validation<Order> { // validator for Order class
Order::id { // validator for specific field
minLength(3)
maxLength(128) hint "'{value}' is too long, must be between 3 and 128" // optional hint for the validation
}
Order::invoiceId ifPresent { // validation for optional field
pattern("INV-\\d{5}")
}
Order::lines { // collection validation
maxItems(100) hint "order can have at most 100 lines"
}
Order::lines onEach { // collection items validation
run(validateOrderLine) // delegating validation to external validator
}
}
val validateOrderLine = Validation<OrderLine> { // external validator for OrderLine
OrderLine::id {
constrain("line ID cannot contain whitespaces") { !it.contains("\\s") } // custom validation
}
OrderLine::product {
Product::price dynamic { product -> // dynamic validation
if (product.name.startsWith("Business ")) minimum(10_000.0)
else minimum(0.0)
}
}
}
Then we can start using our validators:
val invalidOrder = Order(
id = "ID",
lines = listOf(
OrderLine(
id = "line_id 123",
product = Product(
name = "Business Super Product",
price = 500.0
)
)
),
invoiceId = "INV-abc"
)
val orderValidationResult = validateOrder(invalidOrder)
println(orderValidationResult)
As a result, orderValidationResult
will hold the information if the object is valid or not, plus all the potential validation failures. It’s worth mentioning that, by default, Konform performs the full validation no matter if it finds some invalid fields early or not. It can be however configured explicitly to fail fast if needed.
To sum up, Konform seems to be a very promising validation library for Kotlin projects. However, there is one more thing you might want to know - at the time of this writing, Konform’s latest available version is 0.10.0 and it has “just” 700 stars on GitHub. But hey, everyone has to start somewhere!
Detekt - a static code analyser for Kotlin
The last item I have for you in this article differs from all the previous ones described here. Up until now, I have shown you either frameworks or libraries that offer some specific features for your application or boost your productivity in various areas.This time, I’m going to introduce you to a tool that will help you ensure that your codebase quality is the best possible.
Detekt is a static code analyser for Kotlin. If you come from the Java world, then most probably what comes to your mind right now is Sonar. And basically, that would be it - Detekt works in a very similar way, but it’s tailored for Kotlin entirely. The main features Detekt has to offer are:
- code smell analysis,
- generating reports in various formats including HTML, Markdown, SARIF, XML,
- integration with various build tools, including CLI, Maven, Gradle,
- extensibility - can be configured with external rule sets, code formatters, custom reports.
Let’s see how you can integrate Detekt into your project. Assuming you have a Gradle project, the only thing you need to do is add the following plugin to your build file:
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.7"
}
And that’s it! Detekt is already working for you with its default configuration. There are, of course, a lot of ways you can further configure Detekt to exactly match your needs. You can do it inside the detekt
closure:
detekt {
buildUponDefaultConfig = true
allRules = false
config.setFrom("$projectDir/config/detekt.yml")
baseline = file("$projectDir/config/baseline.xml")
}
These are just a few sample settings. A full list of possible configuration options is available on the dedicated Detekt documentation page.
If you like to customize Detekt reports for your builds, there is also built-in support for that:
tasks.withType<Detekt>().configureEach {
reports {
html.required.set(true)
html.outputLocation.set(file("build/reports/detekt.html"))
xml.required.set(true)
sarif.required.set(true)
md.required.set(true)
custom {
reportId = "CustomJsonReport"
outputLocation.set(file("build/reports/detekt.json"))
}
}
}
The full list of possible report customization options is also described in the Reports section of the Detekt official documentation.
Now to run the code analysis, all you need to do is invoke the following Gradle task:
gradle detekt
Detekt is also automatically associated with the Gradle build task so that the analysis will also run if you invoke the:
gradle build
To sum up, if you need a comprehensive tool assuring that your Kotlin codebase is always of the best quality, Detekt is certainly your thing.
Wrap up
That would be it. In this blog post, I tried to guide you through a list of some great tools that should be enough to bootstrap your Kotlin server-side application of any type, be it a microservice with REST API, command line application, or a serverless function like AWS Lambda. All of them fully utilize the Kotlin language with all its features, which means full compatibility between all components in your system.
I hope that you found something interesting here that caught your attention and that at least some of the Kotlin frameworks and libraries that I briefly described in this article will prove to be helpful for you. Don't hesitate to explore the documentation and guides, try them on your own, pick those you found most useful, give them a chance, and who knows - maybe they will become game changers for your next project?
Reviewed by: Rafał Maciak, Rafał Wokacz