We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Lessons I’ve learned in Kotlin Multiplatform Library Development

00:00

Formale Metadaten

Titel
Lessons I’ve learned in Kotlin Multiplatform Library Development
Alternativer Titel
Lessons I’ve learned in Multiplatform Library Development
Serientitel
Anzahl der Teile
637
Autor
Lizenz
CC-Namensnennung 2.0 Belgien:
Sie dürfen das Werk bzw. den Inhalt zu jedem legalen Zweck nutzen, verändern und in unveränderter oder veränderter Form vervielfältigen, verbreiten und öffentlich zugänglich machen, sofern Sie den Namen des Autors/Rechteinhabers in der von ihm festgelegten Weise nennen.
Identifikatoren
Herausgeber
Erscheinungsjahr
Sprache
Produktionsjahr2021

Inhaltliche Metadaten

Fachgebiet
Genre
Abstract
Software development is hard. It’s even harder when you’re building libraries that other developers will depend on. I’ll talk about my experience with library development in Kotlin Multiplatform, trying to highlight challenges I’ve faced and mistakes I’ve made. We’ll look at this through the lens of recent updates I’ve made to the library I maintain, as well as the current state of the wider Kotlin library ecosystem
179
Vorschaubild
20:09
245
253
Vorschaubild
30:06
294
350
Vorschaubild
59:28
370
419
491
588
Vorschaubild
30:18
PortabilitätHumanoider RoboterCodeSystemplattformUmwandlungsenthalpieZeichenketteLogarithmusSoftwaretestArchitektur <Informatik>MultiplikationInformationsspeicherungTypentheorieSchwimmkörperFokalpunktAuswahlaxiomTabellePortabilitätDateiverwaltungCodeSchnittmengeDefaultAuswahlaxiomSchlüsselverwaltungQuick-SortHumanoider RoboterSystemplattformÄquivalenzklasseInformationsspeicherungKartesische KoordinatenApp <Programm>VersionsverwaltungEin-AusgabeBimodulDifferenteCASE <Informatik>ImplementierungInterface <Schaltung>LaufzeitfehlerSoftwaretestMereologieDeklarative ProgrammierspracheFormale SpracheFramework <Informatik>AdditionErwartungswertKlasse <Mathematik>BitInverser LimesVariablePrimitive <Informatik>BrowserQuellcodeElektronische PublikationWort <Informatik>GeradeSpeicherabzugAutorisierungComputerarchitekturZahlenbereichMathematische LogikFunktionalHalbleiterspeicherAggregatzustandWasserdampftafelProzess <Informatik>RelationentheorieNichtlineares ZuordnungsproblemBildschirmmaskeRechter WinkelWärmeleitfähigkeitPhysikalisches SystemGenerator <Informatik>SkriptsprachePoisson-KlammerNichtlinearer OperatorGüte der AnpassungMini-DiscGebäude <Mathematik>EliminationsverfahrenEnergiedichteVerband <Mathematik>Physikalischer EffektBefehlscodeStichprobenumfangGrenzschichtablösungCOMInternetworkingMaschinenschreibenDateiformatParametersystemNetzwerkbetriebssystemFlussdiagramm
AuswahlaxiomZeichenketteKlasse <Mathematik>Kontextbezogenes SystemDefaultKoroutineRechenwerkDefaultDifferenteInverser LimesBinärdatenSystemplattformIntegralKontrollstrukturImplementierungRefactoringVersionsverwaltungParametersystemAuswahlaxiomRückkopplungMathematikCASE <Informatik>MaßerweiterungSerielle SchnittstelleMereologieSchlüsselverwaltungKonfiguration <Informatik>MultiplikationsoperatorKartesische KoordinatenFunktionalSchnittmengeKategorie <Mathematik>Quick-SortSchreib-Lese-KopfPrimitive <Informatik>DateiformatTypentheorieUmsetzung <Informatik>PunktMessage-PassingHierarchische StrukturInnerer PunktBetriebsmittelverwaltungObjekt <Kategorie>AnalogieschlussKlasse <Mathematik>BitLogischer SchlussKoroutineRechter WinkelCodeBildschirmmaskeCodierung <Programmierung>AdditionMAPGrenzschichtablösungWurzel <Mathematik>DatenflussInformationsspeicherungLie-GruppeErhaltungssatzGüte der AnpassungGeradeVollständiger VerbandTablet PCURLComputerspielGraphfärbungWeb SiteSystemaufrufBitrateQuellcodeAggregatzustandGruppenoperationFreewareBestimmtheitsmaßAdressraumRoutingKette <Mathematik>EinsKontextbezogenes SystemComputeranimation
ZeichenketteDatenflussRechenwerkHumanoider RoboterKoroutineClientOrdinalzahlPortabilitätMobiles InternetKonfigurationsdatenbankSchnittmengeKartesische KoordinatenSpeicherabzugSchlüsselverwaltungVersionsverwaltungDatenflussTexteditorMultiplikationsoperatorMailing-ListeMAPFunktionalZentrische StreckungWeb-SeiteInterface <Schaltung>HypermediaStützpunkt <Mathematik>VerschlingungOpen SourceAutomatische DifferentiationInverser LimesBitPortabilitätDateiverwaltungVerzeichnisdienstSchreib-Lese-KopfRechter WinkelSuite <Programmpaket>MomentenproblemMereologieOrdnung <Mathematik>Quick-SortKoroutineMaßerweiterungImplementierungOrdinalzahlClientArithmetisches MittelInformationsspeicherungSelbst organisierendes SystemSystemplattformGemeinsamer SpeicherWechselsprungInstantiierungVorzeichen <Mathematik>AuswahlaxiomElektronische PublikationRechenschieberTwitter <Softwareplattform>AuswahlverfahrenVollständiger VerbandMinkowski-MetrikBildschirmmaskeComputerspielEinhängung <Mathematik>Stabilitätstheorie <Logik>PunktDifferentePlastikkarteEinsGruppenoperationCodeNichtlinearer OperatorGarbentheorieSystemaufrufMinimumHumanoider RoboterWeb SiteBestimmtheitsmaßWorkstation <Musikinstrument>MakrobefehlBildschirmfensterComputeranimation
Element <Gruppentheorie>Computeranimation
Transkript: Englisch(automatisch erzeugt)
Hello, FOSDEM. My name is Russell Wolf and this is Lessons I've Learned in Kotlin Multiplatform Library Development. I need to work on easier-to-say talk titles. Anyway, a couple quick words about me. I'm a developer at Touchlab, where we build apps using Kotlin Multiplatform.
I'm also the author of a library called Multiplatform Settings, which we're going to talk some about. So, let's start with a little bit of background on Multiplatform Kotlin and what that is. So this is the FOSDEM dev room, the Kotlin dev room at FOSDEM.
So, I'm going to assume that most people are familiar, at least with the use of Kotlin on the JVM, but Kotlin, of course, has multiple platforms that it can build to. So, there are primarily three groups. There's the JVM, which is Android and server-side stuff.
There's JavaScript, which includes web browsers and Node. And there's Kotlin Native, which includes iOS, native desktop, embedded systems, and things like that. And the Multiplatform framework is organized so that you have common code that can compile to more than one of these targets,
as well as platform-specific code, which can talk to the APIs of those platforms. There's actually some kind of intermediate stuff that surrounds the line between those as well, but we're not going to go into detail about that. So, what does this look like in code?
So, if all you have is kind of pure logic stuff, you can write that in common. So, common code has access to all the kind of platform-agnostic parts of the standard library, so things like Collections API, but it doesn't have any platform-specific stuff, so it can't do things like talk to a file system or sensors or things like that.
So, when you can't do something in common code, the Multiplatform framework provides these new keywords in the language called Expect and Actual. So, you would have a declaration in your common code with the Expect keyword,
and it can be a value, a function, a class, or anything else. And you can give it separate actual definitions on the different platforms. As I mentioned, most of my examples here are going to be using Android and iOS, but the same thing is true if you're using any of the other platforms as well.
So, Expect and Actual is a nice way to kind of quickly spin something up, but it has some limitations. So, there needs to be exactly one actual definition for every Expect definition. So, if you need to be able to switch out your platform definition in different scenarios,
you might need to do something else. So, instead, or in addition to Expect and Actual, you can also define interfaces in common and implement them in your platform code. So, you might have a logger interface, for example, that has an Android logger, an iOS logger, and an advantage here, because the limitation doesn't even need to come in Kotlin.
So, on your iOS side, if you're writing the rest of your application in Swift, you could implement your logger interface from Swift. The other advantage you get with something like this is you have the ability to define a test logger, or a test version of whatever application you have.
So, a quick overview of what the architecture of a multi-platform app looks like. So, the center of your application is going to be this multi-platform module. It has common code, and it can build, say, the Android code to an Android library,
it can build the iOS code to an iOS library, it has JavaScript code, it can build that to a JavaScript library, and there might be, like, the orange in the center is the common code, and the outside is the platform-specific stuff, which might have, say,
platform-specific implementations of certain things. These get consumed by the apps, which then also have the ability to consume any other platform-specific dependency that they have. So, that's the beauty and the flexibility of Kotlin multi-platform, is your shared code is just one library dependency among everything else.
It can be as much or as little as makes sense for you in your use case. But what happens when you also need dependencies from your common code? So, that's what we're going to be focused on when we talk about multi-platform libraries, is things that extend the APIs that are available to your common code,
such as, for example, multi-platform settings, which is the library that I maintain. It's available on GitHub at this URL, and it's a key-value store, so you can save and load simple data, giving things different keys as names.
And it operates on kind of a set of simple primitives. And it also has a couple bits of, like, Kotlin ICBs, like, operators and delegate functions,
just to make your common code nicer, depending on your code-style preference. And something about the Zina library that I haven't really emphasized much when I talked about it previously,
is it's very focused on platform interop. So, one could easily build a library like this by creating kind of a completely custom Kotlin-first implementation of everything, where you write some custom file format,
serialize it to disk, do it exactly the same on every platform. And that would work in your common code, but your platform-specific code would never really know anything about it. So, what I tried to do as I was building up a library, is make sure that it was using the same source of truth that you might be using in your platform-specific code.
So, for example, the core of the class is the settings interface, and I'm just kind of showing one function here as an example, so that you can actually read the text on the slide.
So, for example here, the Android settings wraps the shared preferences API, which is likely what you're using to do key-value storage in your Android code. So, if you have a key that you've saved in your common code, you can read it out from the platform-specific code,
and they will be synced up. And similarly, an iOS using user defaults, or JavaScript using local storage, and there's a number of other implementations as well, including a mock in-memory implementation.
So, there's mock settings that just wraps around a map, so you can use that in testing your application code that makes use of the library, without needing to use the actual runtime versions of things that are going to actually save data.
And in addition to the different implementations, there's syntax niceties, as I mentioned before. So, operators, you can get that bracket, get and set syntax, and delegates, so that you can define variables that, when you get and set them, are backed by your settings store.
And we'll talk a bit more about those later. So, with that kind of overview of what the library does, let's talk a bit about what I've been working on with it lately.
So, the first thing I want to talk about is some of the thoughts I've been having lately about some of the early API choices and things that I might have done slightly differently if I'd known then what I do now. So, the virtual version of the library, and this is kind of what things look like,
has getters that look like this, where there's a key that you pass in, obviously, and there's a default value, so that if that key is not present in the store, it'll return the default instead. And there's kind of two main ways that key value APIs in general do that sort of thing.
Handle missing keys. You can either pass a default value like that, or you can just make it nullable. So, later on I added this, I added kind of, or null, equivalents to all these different APIs.
But one kind of subtle thing that I wish I'd done differently is this equal zero here. So, the default value argument has itself a default value, which means if you just pass a key in, you still get the non-null version of the function, but when it's missing, you get zero instead of getting null.
So, it's like a slightly awkward API because I don't think there's a ton of actual use case in retrospect to where you want to just default to zero there without expecting it, without specifying it explicitly.
So, if I didn't have that, then these could both be named the same thing, right? You can either pass the default value and get the non-null, or you don't, you get the nullable. So, I've been thinking about doing this refactor, though it's a breaking change. So, there is an issue on GitHub if you're interested in giving feedback as to whether or not that's something that I should do.
But I wanted to highlight it as an example of something that I did early on without really giving a lot of thought to it, that has consequences down the line if you're worried about maintaining compatibility. Another early choice that I've been giving some extra thought to is around naming.
So, each platform tends to have a settings limitation that's named for that platform. So, the different Apple platforms, for example, iOS, macOS, tvOS, etc. I'll have a Apple settings that wraps user defaults.
But recently I also added a second limitation on these platforms that uses the keychain instead of using the user defaults API. And now this naming just feels kind of dissonant, right? Like, why is user defaults the thing that gets the Apple name instead of the keychain?
Am I saying that you should use one over the other? So, another refactor that I'm thinking about doing is changing all of the platform named implementations to name the API that they wrap around. And this has the advantage of making the interop a lot more clear.
But again, it's a breaking change. So, the next thing I want to talk about is some cool integrations I've been working on recently with some of the Next libraries. So, a long time pain point of Kotlin Native in particular has been binary compatibility.
Where every time a new Kotlin Native version came out, it broke compatibility with existing libraries. And so you had to recompile every library before you could update your application code. And that meant that it was pretty hard to justify having a library dependency within your own
library because you're adding this extra piece that you need to wait on before you can update. But with Kotlin 1.4, the compatibility story, they don't have explicit guarantees around binary compatibility yet on the native side.
But things have tended to be a lot more compatible. And so it's easier to publish integrations with the libraries and not have it block your update path. So that has enabled a couple cool things.
And the first one I want to talk about goes back to those delegate APIs that I mentioned earlier. So, the settings library provides functions for each of the types to use property allocates. So you can call this int extension function, pass it a key and a default value if you want, and there's nullable versions of these too.
So that you can use the property allocates syntax to have a variable in your code which is backed by your setting score on reads and writes. And that's nice for your primitives.
But something that I've always kind of like had in the back of my head that I thought would be nice to add was a way to add custom delegates. So maybe you have, say, a user class that has a first name and a last name. And you want to save it as a user instead of you have to define a key for each of its properties and save those separately.
I thought it'd be nice to have some kind of API to make that sort of thing easy to write. But I never, or for a while, I didn't really have a kind of a good way to do that until I had some conversations with a couple of JetBrains devs around the next serialization library.
So you might know the serialization library for its ability to serialize things to common formats like JSON or protobuf. But it also has APIs for custom serialization formats.
So they provide these classes called encoder and decoder, which you can use to kind of take. They basically kind of provide the glue between the kind of abstract serialized
form of a class and any arbitrary format that you might want to define. So we can use our settings store as that format. So essentially, you give it the settings and the kind of root key, and it's going to go through every member of the class.
And so if you have like if you have that user with first name and last name, it's going to say like it's going to save user first name and user last name as separate values for you. And the top level API is just another extension function.
And these are functions on settings to encode or decode. And then there's also a property I'll get one. So it looks a little bit more complex than our our original kind of conception.
The serialization API requires this case serializer argument, which is the thing that that tells the serialization kind of what. So it doesn't just kind of like infer the class automatically because you might have some kind of class hierarchy that needs to get serialized in a particular way, or you might want to kind of customize how it works.
So you have to pass a serializer explicitly and you have the option of passing this context, which is also part of how how the how like polynomial serialization can work.
But you could always kind of wrap these around your own extension function if you wanted to kind of get back to that original API style. So if you find this, like if in your application code, you define this, this my class style function, then you can get back to that that original syntax that I've been thinking about.
So that's pretty cool on the serialization side. The other library that I've spent some time thinking about integrations with is coroutines. So one of the things that this library provides is for for some platforms, certain types of limitations
are observable, which means they have this add listener function, which can you can pass it a callback. And that callback will get called every time every time the value at that key changes. So the obvious kind of coroutine extension to add is a just a flow version of that.
Right. Where instead of an arbitrary callback, you can get a flow and just subscribe to that flow. And that's actually like extremely straightforward. Right. So if you'd asked me a year ago, I would have thought that the only kind of coroutines extension that multiphone could ever need would be this.
But then the Android team came and added this new library called Datastore. So Datastore is intended as a shared preferences replacement. So as a key value storage library, obviously multiple settings is going to
want to have a Datastore based implementation in addition to the shared preferences implementation. But that ends up being kind of complicated. So Datastore is a completely flow based API. So you have the Datastore object and it can store any kind of data.
But the kind of shared preferences analog is this object called the preferences Datastore. And it has a data property on it, which is just the flow of the full preferences state every time it updates. So if you want to get like the value of a particular key, you strive for that flow, you map it based on that key and you get a flow of that value.
And if you want to write data, you set a function, you put in your edits fairly similar to like the shared preferences editor API that you have with your preferences.
So what's complicated here is everything in this is coroutine based. So like that edit function, the getters are all flows.
So that's all kind of hard to fit into a interface that is not coroutine aware, like settings. And so I've ended up doing to to have something that works with Datastore is added two different interfaces.
And we'll talk a bit about why. So there's a suspend settings interface, which looks exactly like settings, but all of the functions are suspend functions. And then there's a flow settings interface which extends to suspend settings and adds flow getters.
And then we can have our datastore settings, which wraps that store API in the flow settings interface, but could be downcast to a suspend settings as well. So why do we need both of these?
So remember, we have set it like in the base library, we have settings, we have observable settings, and you need observable settings to be able to set listeners and you need to be able to set listeners to be able to get flows. So if you want to be able to work with like a single one of these interfaces from your common code, you need to be able to pick one.
Right. So if you're if you're using flow, like your your common interface would have to be one of these coroutine aware interfaces. So you could on your other platforms, you would convert them to that.
So you can convert a setting, a simple settings instance to a suspend settings, you can convert a observable settings to a flow settings. And depending on whether you're so you can kind of share share which interface you're using, depending on whether all of your platforms are observable or not.
So as like a more concrete example, to maybe make this a little more clear. So you might have in your common code, like a expect file flow settings on Android, you use it out of store settings on iOS, you might take Apple settings and call that to flow settings extension.
But you wouldn't be able to add JavaScript here because the JavaScript says limitation, which is based around local storage, is not observable. But so sort of work around that you can use to spend settings instead, so you lose the
ability to have flows in your common code, but you gain the ability to hit every single platform. So in order to kind of have the flexibility to pick both of those scenarios, I ended up adding both interfaces. So that's a bunch of notes on things I've been working on recently.
What else is kind of still on the docket? So I was interested in adding more platforms and implementations. The major thing that's missing right now is desktop Linux. So there's there's a Windows registry implementation and there's the implementations work on Mac OS desktop.
So Linux is the only native desktop platform that doesn't have implementation yet. I'm very interested in hearing from the desktop Linux developer community about like what a good API to use there is.
I know that's a part of the Kotlin community that that sometimes feels a little bit underserved. So this is me kind of reaching out and saying, like, I want to give you guys something, but I don't know what the best way to do that is. So if you're someone in that community who has opinions, reach out and let me know. And of course, any any other limitations that people think would be useful, I'm interested in hearing about.
I don't have any others myself that I'm like are specifically on my radar, but that doesn't mean that more things can't be added. I also want to improve various bits of the API, like some of the not yet observable limitations could have listener support added.
I mean, there's various bits of API adjustment that I've been thinking about. And then at some point, this library's got to go 1.0, right? Right now, it's been in kind of ODOT releases for its entire life. And I kind of had this idea in my head at the beginning that it would go 1.0 after like multiplatform as a whole went 1.0, went stable.
And I'm not, I think, kind of absent any other like hard push, that's probably still the directory I would be on. But I'm starting to think more and more about, like, what would it look like
to to start calling the stable and completely 100% commit to all the API choices. So that's that's part of why I've been kind of thinking about what are some of the things that I might do differently, because if I do want to release a 1.0 version, that's the moment that everything needs to get finalized.
So what else is out there? I've talked a lot about my library, but obviously I'm not the only one. So JetBrains has their kind of core suite of things. So we talked about coroutines and serialization, because with the settings integrations,
you also have the Ktor client, which is your kind of like common HTTP client. And all of these are at post 1.0 releases now. So JetBrains is fully committed to them, their kind of core APIs for pretty much any reasonably scaled multi-platform application.
They also recently added a version of a datetime API. So definitely check that out if you haven't, because that's something that's been asking for for a long time. And then they've been working on, at kind of like lower levels of priority, IO and atomic APIs libraries.
Then, of course, there's community stuff. And I used to, when I had this slide, like list out not every, originally the very first time I had a slide like this, it was like, what is pretty much every community library that exists?
Or what are some of the major ones? And now I'm just going to give you a couple of links to community media and lists, and to the official mobile multi-platform docs, which has an ecosystem page with a bunch of libraries. The ecosystem has gotten pretty big, and that's pretty exciting.
There's a neat movement happening on some larger and larger scale libraries that you didn't see a year ago. So like Square, for example, has their OKO library, and they've been adding file system support to that.
And like other big things like that, the Apollo team has their multi-platform Apollo client, GraphQL client, which they've been talking about a lot recently. You're seeing kind of like larger organizations with larger scale multi-platform libraries enter the space,
which is a sign of how it's maturing. That doesn't mean that there's not still room for you to add your own contributions. The thing that attracted me, that kind of got me to write multi-platform settings originally, was the fact that there was this kind of like completely new wide-open ecosystem,
which is a really neat opportunity to make your open source mark, and to kind of like jump in and do something before anybody else does. And it's definitely harder to do that now than it was two, three years ago, but there's still plenty of room for more things to come in.
So I definitely encourage you, if you're interested in open source in general, to think about doing stuff in KMP, because it's pretty fun. So thanks for coming to my talk. I'm happy to answer questions either through the conference platforms,
or on Twitter or the Kotlin Lang Slack, you can find me at RussHWolf. And I'll also have the slides posted if you need to refer back to them. Thanks.