Announcing the Vonage Kotlin Server SDK
最后更新 October 25, 2024

Introduction

When Kotlin first appeared in the early 2010s, few could have imagined it would be a top 20 programming language that it is today, surpassing other established JVM languages such as Groovy and Scala. Since becoming the de facto language for Android app development, the ecosystem has only expanded with rich features and a dedicated annual conference.

Despite the updated release cadence and influx of language features to stay relevant, Kotlin remains a popular language beyond mobile applications. For server-side development, Kotlin is an excellent choice for several reasons: a rich ecosystem of existing libraries (something all JVM-based languages benefit from), even the latest 2.0 release is still compatible with the now decade-old Java 8 bytecode, it is elegantly designed from the ground up without legacy features and generally makes programming a joy with minimal boilerplate and powerful yet intuitive syntactic and semantic sugar.

I am pleased to announce that, after working on it full-time for one quarter, the Vonage Kotlin SDK is now officially available and supported as of v1.0.0! It is published to Maven Central, where you will also find instructions for using it in your build system. With this SDK, you can use all GA Vonage APIs in Kotlin to make voice calls, send & receive SMS, and create video applications. In this blog post, I will provide how and why this SDK came to be.

Background

Recognising the increasing prevalence of Kotlin on server-side, and the general eye-rolling that many Kotlin developers feel when they have to work with Java, we were thinking about providing Kotlin code snippets for the Java Server SDK. After all, one of the benefits of Kotlin is its seamless compatibility with Java. However, we decided to go one step further. We wanted to provide a truly first-class experience for Kotlin developers but without the cost and maintenance burden of developing an SDK completely from scratch. We aimed to create a proof-of-concept in a relatively short period, which would eventually lead to a fully-fledged SDK, whilst still using the Java SDK underneath for its implementation and data model. The rest is... well, in the commit history!

Motivation

The natural question that sprung to mind is: what's wrong with the Java SDK? After all, I have been maintaining it more or less full-time since Q2 2022, and Kotlin itself does a lot of heavy lifting in ensuring compatibility. The answer lies in the delta between Kotlin and Java's language features.  Java's reputation for verbosity, combined with enterprise reluctance to migrate away from Java 8 (or at least, doing so at a glacial pace), and the fact that compatibility between the two languages is at the bytecode-level, means that what may be idiomatic in Java does not necessarily translate well to Kotlin. Moreover, Kotlin's impressive arsenal of powerful language features can only be leveraged by code written in Kotlin.

Nullability

One of the distinguishing features of Kotlin compared to Java is its distinction between nullable and non-nullable values. This is not achieved through a monadic structure like java.util.Optional; it is native to the language and its syntax. Therefore, Any (Kotlin's equivalent of Object) and Any? are two distinctly different types: the former being non-nullable and the latter being nullable. Since Java does not have this distinction (except for primitive types), Kotlin has the notion of "Platform types", which are denoted by an exclamation mark (e.g. Any!). This bypasses Kotlin's nullability feature for compatibility. Creating a Kotlin SDK provides an opportunity to be explicit about what is and isn't nullable, both in function parameters and return types.

Named, Default & Optional Parameters

The main benefit of explicit nullability is perhaps best realised in function arguments. Using Optional in Java is not exactly elegant, but in Kotlin, optional parameters can be made explicit in code by declaring them as nullable and setting them to null by default. This allows users to specify only what they truly want and need. Furthermore, Kotlin has named function parameters, which means arguments need not be provided based on their position; they can be explicitly declared, allowing the user to skip certain optional parameters, and to make explicit which parameter they are providing without having to rely on code inlays from an IDE - particularly useful when there are multiple parameters of the same type. Furthermore, the ability to set default values, even for non-nullable parameters, has the benefit that it explicitly documents the default, and means that only one method needs to be declared for each function, rather than overloads with a varying number of arguments for each combination, as is the case in Java.

For example, a common operation on most of our APIs is listing resources - typically, this is the central resource of an API - Users, Calls, Applications, Broadcasts etc. The method signature of listRender in the Video API illustrates the point: default values for the count and offset parameter are provided. Since both of these are of the same type, providing 4 different variants of this method in Java would be impossible. In Kotlin, just one method is required to cover all four cases: no parameters, offset only, count only and both parameters. These would be invoked as follows:

video.listRenders()            // Default params
video.listRenders(count = 25)  // Count only
video.listRenders(offset = 10) // Offset only
video.listRenders(25, 10)      // Both params

Lambdas for Builders

Another eyesore with the Java SDK is its heavy reliance on builders. I have previously written about the Builder pattern, and how it only exists because the language does not have named and default (optional) constructor parameters. In some cases, Kotlin allows us to eliminate using builders entirely, relying only on named / optional parameters. A good example of this is the Number Insight API implementation. Notice how under the covers, the builder from the Java SDK is used, but Kotlin's expressiveness allows us to delineate required and optional parameters using the method signature alone.

However, reimplementing everything using this pattern would require more work (and duplication) than was in scope for this project and increase the maintenance burden. Instead, the builder pattern established in the Java SDK can be reused in Kotlin quite elegantly using "trailing lambdas"; a feature which is commonly used in creating domain-specific languages (DSLs). To illustrate, let's start with some basic examples. In the Messages API implementation, there are utility methods for each valid message type and channel combination. Each function takes as input a lambda expression with the appropriate Builder class as its receiver. That means from a user's perspective, sending an SMS would look like this:

vonage.messages.send(smsText {
  from("Kotlin SDK")
  to(System.getenv("TO_NUMBER"))
  text("Hello, World!")
})

The methods from, to and text all come from the Java SDK's SmsTextRequest.Builder and its superclass MessageRequest.Builder, including the documentation and parameters. However, from the perspective of a Kotlin developer, the whole process of obtaining the builder and calling the build() method is hidden. Combined with default parameters, that means if all the parameters on a builder are optional, it can be omitted entirely. For example, here is the implementation of the createSession function in the Video API:

fun createSession(properties: CreateSessionRequest.Builder.() -> Unit = {}): CreateSessionResponse =
  client.createSession(CreateSessionRequest.builder().apply(properties).build())

For default parameters, it can be called as follows:

val session = vonage.video.createSession()

To provide optional parameters, the trailing lambda syntax can be used, like so:

val session = vonage.video.createSession {
  mediaMode(MediaMode.RELAYED)
  archiveMode(ArchiveMode.MANUAL)
}

It is also possible to use trailing lambdas for providing optional values. Perhaps the most complex examples lie in the Voice API implementation, where the complexity of defining NCCOs in the Java SDK benefits most from this approach. To illustrate, here is an example of creating a voice call to a phone number with two actions - Talk and Connect - in the Java SDK along with some other configuration, written in Kotlin:

val callEvent = javaClient.voiceClient.createCall(Call.builder()
  .to(PhoneEndpoint("448001234567", "1p2#5"))
  .fromRandomNumber(true)
  .advancedMachineDetection(AdvancedMachineDetection.builder().build())
  .ncco(
    TalkAction.builder("Hello, this is a text-to-speech call.")
      .language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
      .premium(true)
      .build(),
    ConnectAction.builder()
      .endpoint(SipEndpoint.builder("sip:me@example.org").build())
      .ringbackTone("http://example.com/ringback.mp3")
      .build()
  )
  .ringingTimer(30)
  .eventUrl("https://example.com/webhooks/events")
  .build()
)

Whilst it's somewhat readable with appropriate indentation, it's not quite as elegant as a more idiomatic approach. Here is the same thing written using the Kotlin SDK:

val callEvent = vonage.voice.createCall {
  toPstn("448001234567", "1p2#5")
  fromRandomNumber(true)
  advancedMachineDetection()
  ncco(
    talkAction("Hello, this is a text-to-speech call.") {
      language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
      premium(true)
    },
    connectToSip("sip:me@example.org") {
      ringbackTone("http://example.com/ringback.mp3")
    }
  )
  ringingTimer(30)
  eventUrl("https://example.com/webhooks/events")
}

Notice how the DSL syntax flows more naturally. In particular, the toPstn function frees the user from having to call the to method directly and notice how, in the absence of any configuration for Advanced Machine Detection, no builder or lambda is required. However, for the talkAction, the mandatory parameter - text to be spoken - is part of the constructor, whilst optional parameters are in the lambda. Similarly for the connectToSip action, the URI is mandatory, with an optional lambda for additional configuration. In both cases, the lambda can be omitted, like so:

val callEvent = vonage.voice.createCall {
  toPstn("448001234567", "1p2#5")
  fromRandomNumber(true)
  advancedMachineDetection()
  ncco(
    talkAction("Hello, this is a text-to-speech call.")
    connectToSip("sip:me@example.org")
  )
  ringingTimer(30)
  eventUrl("https://example.com/webhooks/events")
}

You can see how this is implemented in the connectTo* functions of the Voice API; which combine two builders into a single function call. The parameters to the functions are for the endpoint being connected to, and the trailing lambda is for configuring the Connect action itself.

Resource-Based Endpoints

Aside from capitalising on Kotlin's neat features to reduce boilerplate, another distinction between the Kotlin and Java SDK is that the Kotlin SDK has a resource-based approach to making API calls. In most of Vonage's APIs, there is at least one resource which can be queried, created, updated and deleted. For each of these resources, a class corresponding to that resource exists which provides access to these endpoints. What these resources all have in common, is a unique identifier. Rather than having to cache this elsewhere to repeatedly make API calls on a particular resource, the SDK takes care of this, so that the ID does not need to be provided every time. These resources can even be nested, as exemplified by the Video API implementation. Here is an example:

val existingSession = vonage.video.session(sessionId)
val streams = existingSession.listStreams()
existingSession.connection(connectionId).sendDtmf("1234")

This will make a lot more sense when using an IDE with auto-completion. In this regard, API calls are grouped by resource type. Especially in complex APIs such as Video where there are nested resources and IDs, this makes the task of providing an ID less error-prone, since the required parameter is provided by construction. This allows method signatures for nested resources to be decluttered, so you can focus on providing meaningful parameters separately from addressing the resource. By contrast, the Java SDK is "flat/contextless" in the sense that there is usually a one-to-one mapping between endpoints in the API specification and endpoints in the SDK. This approach further enhances the separation of required and optional parameters and means that a request is more likely to be correct by construction.

Take a look at the API specification for muting a video stream as an example. It is clear that the coordinates to the endpoint require the session ID and stream ID. In the Java SDK implementation, it is possible to mistakenly mix up the session ID and stream ID by passing them in the wrong order. By contrast, the Kotlin SDK's implementation does not require any parameters, since no data is being sent. Similarly for adding a Stream to an Archive, the Kotlin SDK's implementation has a single method where it is clear the stream ID is the parameter that should be passed, given the context. By contrast, the Java SDK implementation has two methods (due to optional parameters) and relies on the method naming and documentation to portray how it works and what it does.

It is perhaps a matter of personal preference; neither approach can be argued to be objectively better. However, the consistency with which the Kotlin SDK follows this approach in all APIs where possible is a significant distinguishing aspect of it, especially when considering the Java SDK's heavy reliance on builders, even for mandatory parameters.

Extensions

Another beautiful feature in Kotlin is extension functions; the ability to define methods on existing classes without extending them. This is especially useful for enhancing the builders defined in the Java SDK to make them more idiomatic in Kotlin. Take, for example, the Application API. Each Vonage Application can have multiple capabilities, one of each type. Each Capability has one or more Webhooks, and each Webhook has a type, such as answer_url, status_url, event_url etc. However, depending on the type of Capability, only certain Webhooks are applicable. For example, the Verify capability only has status_url, whilst the Voice capability has answer_url, fallback_answer_url and event_url. At the time of writing, the Java SDK's implementation of this is quite clunky. To illustrate, here is an example of updating an existing application's name, removing the Messages capability and updating webhooks for Verify and Voice. Note that to update an application, it has to be retrieved first because whatever configuration is provided will overwrite the existing application. Here it is written using the Java SDK:

val ac = javaClient.applicationClient
val existing = ac.getApplication(appId)
val updated = ac.updateApplication(Application.builder(existing)
  .name("My Updated Application")
  .addCapability(Verify.builder()
    .addWebhook(Webhook.Type.STATUS, Webhook.builder()
      .address("https://example.org/webhooks/verify/status")
      .method(HttpMethod.POST)
      .build()
    )
    .build()
  )
  .addCapability(Voice.builder()
    .addWebhook(Webhook.Type.ANSWER, Webhook.builder()
      .address("https://example.org/webhooks/voice/answer")
      .method(HttpMethod.POST)
      .build()
    )
    .addWebhook(Webhook.Type.EVENT, Webhook.builder()
      .address("https://example.org/webhooks/voice/event")
      .method(HttpMethod.GET)
      .build()
    )
    .build()
  )
  .removeCapability(Capability.Type.MESSAGES)
  .build()
)

That's a lot of builders! Easy to get lost, and there's nothing in the design that prevents you from specifying the wrong webhook type for a capability. Compare that to the Kotlin SDK:

val ac = vonage.application
val application = ac.application(appId)
application.update {
  name("My Updated Application")
  verify {
    status {
      url("https://example.org/webhooks/verify/status")
      method(HttpMethod.POST)
    }
  }
  voice {
    answer {
      url("https://example.org/webhooks/voice/answer")
      method(HttpMethod.GET)
    }
    event {
      url("https://example.org/webhooks/voice/event")
      method(HttpMethod.POST)
    }
  }
  removeCapability(Capability.Type.MESSAGES)
}

Notice how we didn't have to even get the application using the endpoint - the SDK handles this for you. Furthermore, the extension functions defined on each Capability constrain the webhooks that can be defined, so that it is correct by construction. This will make more sense when viewed in an IDE with auto-completion, but basically, when you are inside e.g. the voice block, the only options that will appear are answer, fallbackAnswer and event, because these are the only ones defined on the builder as an extension. Not a single mention of "build" anywhere, and you don't even need to specify the webhook type directly: you just declare the appropriate element.

It's little things like this which can improve developer experience and productivity. Of course, since we don't auto-generate any of our SDKs at Vonage, this could be applied in Java too. In fact, in the process of developing the Kotlin SDK, I have made a list of things to change in the Java SDK, both big and small. At the time of writing, this list could be converted into 55 JIRA tickets! However, it is a nice touch that easy wins can be made with extension functions. Of course, not all of the improvements in the Kotlin SDK can nor should be backported to the Java SDK.

Documentation

The Kotlin SDK is documented using KDocs. Every single function and class has hand-written documentation on parameters, return type, exceptions and a description of what it does. Furthermore, these are published in the modern Dokka HTML format, keeping in the style of the Kotlin API documentation as opposed to the comparatively dated-looking Javadocs format. You can browse the documentation using any service that can render docs from JAR files, such as Javadoc.io. Of course, the documentation is also available directly in your IDE.

Code Samples

You will find code snippets for all supported APIs on our Developer Portal. Just browse to the product you're interested in, and under 'Build Your Solution', you will find the snippets where Kotlin will be one of the languages. You can also check them out directly and run them using the Vonage Kotlin Code Snippets repo on GitHub. For example, here is a "Hello World" text-to-speech snippet using the Voice API in the Kotlin SDK. Each code snippet, as rendered on the documentation page, also has a link to the backing source code on GitHub. To run the examples, you just need to check out the repo and set the environment variables, then run it via Gradle or your IDE. Instructions for this can be found in the repository's README.

Testing

The SDK has 99% code coverage at the time of writing. Every single function is tested, with required and optional parameters. Even elements that reside in the Java SDK are tested for completeness. The approach taken is end-to-end / integration testing since the Java SDK already tests the data model and parameter validation. I decided to use WireMock for this; a framework for API testing.

I built up a small library of high-level functions to facilitate asserting request and response bodies & headers. The result is that each test reads like a specification; just as all good tests should. The anatomy of a test is mocking the expected endpoint URL, HTTP method, request body (JSON or query params), headers (auth, content type, user agent), expected response parameters (if any, in JSON format) and HTTP status code. This essentially mirrors the API specification in the code. To make writing tests less verbose and error-prone, the request and response bodies are not written directly as JSON, but as a Map<String, Any>. This can be serialised using Jackson, so all that is needed is to provide the correct property names and expected values.

Here's an example which tests sending a new request to a user in the Verify API. You can see all elements at play here, the declarative approach to defining the specification and how it captures all elements of the OpenAPI specification. In practice, most tests in the SDK use even higher-level methods declared in the same file to further reduce duplication and complexity. Furthermore, every parameter is tested, so oftentimes each endpoint will have two test methods: one for all parameters and one for required parameters only. The most complex tests are of Voice API; even still, the declarative approach makes these much more readable and maintainable than the comparatively verbose tests which use JSON directly in the Java SDK. For instance, compare tests of Video API in the Java SDK and Kotlin SDK, I'm sure you'll agree!

The benefit of having tests which are written ground-up independently of the underlying implementation, is that in the future, anyone could re-write the SDK in pure Kotlin without using the Java SDK. The tests describe the specification, so only the actual usages of the SDK would ever need to change, not the mocks. The only thing that the Kotlin SDK does not test is parameter validation; i.e. whether certain combinations of parameters are valid, or required parameters in builders. The data model validation is tested in the Java SDK, and purposely not duplicated in the Kotlin SDK. Some may even argue that the API backend should do the validation and return an error response, not the SDK. To avoid duplicating validation logic and further bloating tests and development time, this was intentionally left out of scope, as it is an orthogonal concern.

Next Steps

Although the SDK is now officially supported with implementations of all General Availability status Vonage APIs, this is just the beginning. As well as keeping up with new features and APIs in tandem with the Java SDK, there are tutorials to write, integrations to build and miscellaneous improvements to be made based on user feedback. Speaking of which, we very much welcome community input and suggestions, even if it means breaking changes. Try out the SDK and let us know what you think!

Signing Off

That's all for now. If you do come across any issues or have suggestions for enhancements, feel free to raise an issue on GitHub, reach out to us on X (formerly Twitter) or drop by our Community Slack. I hope you have a pleasant experience using the Kotlin SDK, and look forward to any feedback.

Sina MadaniJava Developer Advocate

Sina is a Java Developer Advocate at Vonage. He comes from an academic background and is generally curious about anything related to cars, computers, programming, technology and human nature. In his spare time, he can be found walking or playing competitive video games.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.

Subscribe to Our Developer Newsletter

Subscribe to our monthly newsletter to receive our latest updates on tutorials, releases, and events. No spam.