{"id":21111,"date":"2019-08-06T09:49:56","date_gmt":"2019-08-06T07:49:56","guid":{"rendered":"https:\/\/www.inovex.de\/blog\/?p=16635"},"modified":"2022-11-24T10:36:20","modified_gmt":"2022-11-24T09:36:20","slug":"kotlin-multiplatform-for-clean-architecture","status":"publish","type":"post","link":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/","title":{"rendered":"Kotlin Multiplatform for Clean Architecture"},"content":{"rendered":"<p>As mobile developers, we often rewrite the same logic in another language, maintaining two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, which enables us to write the same code for the JVM as well as LLVM!\u00a0<!--more--><\/p>\n<p>&nbsp;<\/p>\n<p>The project is getting more and more traction in the community right now. It is also heavily supported by JetBrains and Google, which probably means that this is more than yet another hyped cross-platform framework.<\/p>\n<p>Following the <em>write once, compile everywhere<\/em> pattern, Kotlin multiplatform code is regular Kotlin code compiled to JVM bytecode for Android and to LLVM bytecode for iOS. The concept of compiling a shared library to native code for both mobile platforms is not new. For instance, it is possible to write a shared library in C++ and integrate it in Android via JNI and iOS via Swift\/C interoperability. However, Kotlin multiplatform takes this idea a step further and handles (most of) the difficult parts of the integration. And after all, Kotlin is a great language and we think its use should not be limited to the JVM!<\/p>\n<p>For us mobile developers at inovex, several questions arise:<\/p>\n<ol>\n<li>Is it ready for production yet?<\/li>\n<li>Can we use Kotlin multiplatform to build apps following clean architecture guidelines?<\/li>\n<li>How much code can be shared between platforms? In other words, how much development time (=money) can be scrapped by using Kotlin MP?<\/li>\n<\/ol>\n<p>It is hard to fully answer these questions without releasing multiple apps into production and evaluating the results, i.e., feeling the pain of Kotlin multiplatform being still beta and in active development. Before we take that path, we could build a minimal example to evaluate whether Kotlin multiplatform is feasible for the next Android and iOS project at inovex.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_79_2 counter-hierarchy ez-toc-counter ez-toc-custom ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\"><p class=\"ez-toc-title\" style=\"cursor:inherit\"><\/p>\n<\/div><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Goal\" >Goal<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Scaffolding\" >Scaffolding<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Persistence\" >Persistence<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#HTTP-client\" >HTTP client<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Repositories\" >Repositories<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#View-models\" >View models<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Tests\" >Tests<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#Verdict\" >Verdict<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"Goal\"><\/span>Goal<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our primary goal is to build a minimal sample app which uses the MVVM pattern with the components persistence layer (i.e., database), repositories, view models and a HTTP-client to sync data. All components should be kept in the shared codebase, only the code for the UI should be platform-specific.<\/p>\n<p>In terms of functionality, the app loads a list of todos (read: strings) from a <a href=\"http:\/\/jsonplaceholder.typicode.com\/\">public lorem-ipsum JSON-API<\/a> and displays them in a list. The todos are persisted in the database.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-16637\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/screenshot_cpsample_ios.png\" alt=\"\" width=\"269\" height=\"547\" \/> <img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-16636\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/screenshot_cpsample_android.png\" alt=\"\" width=\"268\" height=\"546\" \/><\/p>\n<p>The following part of this post covers the example in detail, but you can also <a href=\"#verdict\">skip to the end<\/a> if you just want to know our opinion on Kotlin MP.<\/p>\n<p>You can find the final code on GitHub:\u00a0<a href=\"https:\/\/github.com\/inovex\/kotlin-multiplatform-sample\">https:\/\/github.com\/inovex\/kotlin-multiplatform-sample<\/a><\/p>\n<h2><span class=\"ez-toc-section\" id=\"Scaffolding\"><\/span>Scaffolding<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>For our first Hello world app we followed the <a href=\"https:\/\/kotlinlang.org\/docs\/tutorials\/native\/mpp-ios-android.html\">official tutorial from JetBrains<\/a>. The tutorial is pretty concise, the only hiccup was that Gradle source set names are case-sensitive and we first used <em>iosMain<\/em> instead of <em>iOSMain<\/em>.<\/p>\n<pre class=\"lang:default decode:true \">sourceSets {\r\n\r\n   commonMain {\r\n\r\n       \/\/ ...\r\n\r\n   }\r\n\r\n   androidMain {\r\n\r\n       \/\/ ...\r\n\r\n   }\r\n\r\n   iOSMain { \/\/ this ain't camelCase! :(\r\n\r\n       \/\/ ...\r\n\r\n   }\r\n\r\n}\r\n\r\n<\/pre>\n<p>As our primary IDE we use Android Studio 3.5 beta. iOS builds are tested with Xcode, of course. IDE support for Kotlin multiplatform in Android Studio is not complete at the moment and the builds take very long. However, Google is working on better IDE support, as we heard on the <a href=\"https:\/\/www.youtube.com\/watch?v=Td0C-fT5SxU&amp;list=PLnYRVL0Cw1FT5LO1r3QWku_VyY1H1et7w\">Conference for Kotliners<\/a> this year. Let\u2019s hope that build times can be improved, too.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Persistence\"><\/span>Persistence<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>For a shared database, we use the SQLite wrapper <a href=\"https:\/\/github.com\/square\/sqldelight\">SQLDelight<\/a>. The framework uses a database scheme followed by queries for data access written in plain SQLite\u00a0 to generate all necessary classes like data access objects and such. The documentation on GitHub is a bit outdated. We recommend using the latest version of SQLDelight and refer to <a href=\"https:\/\/github.com\/inovex\/kotlin-multiplatform-sample\/blob\/master\/SharedCode\/src\/commonMain\/kotlin\/common.kt\">our sample code<\/a> for the details.<\/p>\n<pre class=\"lang:mysql decode:true\">CREATE TABLE todo(\r\n\r\n   id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n\r\n   title TEXT NOT NULL,\r\n\r\n   completed INTEGER\r\n\r\n   );\r\n\r\nselectAll:\r\n\r\nSELECT *\r\n\r\nFROM todo;\r\n\r\ninsert:\r\n\r\nINSERT INTO todo(title, completed)\r\n\r\nVALUES (?, ?);\r\n\r\ndeleteAll:\r\n\r\nDELETE FROM todo;\r\n\r\n<\/pre>\n<p>In the Gradle file, the database is configured as follows:<\/p>\n<pre class=\"lang:default decode:true \">sqldelight {\r\n\r\n   Database {\r\n\r\n       packageName = \"com.inovex.cpsample.shared\"\r\n\r\n   }\r\n\r\n}<\/pre>\n<p>The SQLDelight Gradle plugin will generate a Kotlin class called <em>Database<\/em> (if no other name is specified). To add an entry to the <em>Todo<\/em> table, all we need is a simple method call:<\/p>\n<pre class=\"lang:default decode:true\">fun addTodo(title: String, completed: Boolean) {\r\n\r\n   val completedNum = if (completed) 1L else 0L\r\n\r\n   database.databaseQueries.insert(title, completedNum)\r\n\r\n}<\/pre>\n<p>Note that booleans are not supported, thus we have to convert the flag to an SQLite <em>INTEGER<\/em>, which is handled as <em>Long<\/em> in Kotlin.<\/p>\n<p><em>SELECT<\/em> queries can also be monitored for changes, which helps us to emulate a simplified version of Android <em>LiveData<\/em> in our view model later. We keep track of registered observers using unique numerical IDs.<\/p>\n<pre class=\"lang:default decode:true\">fun observeTodos(id: Int, onChangeCallback: (List&lt;Todo&gt;) -&gt; Unit) {\r\n\r\n   if (observers.containsKey(id)) {\r\n\r\n       throw RuntimeException(\"Already observing with id $id\")\r\n\r\n   } else {\r\n\r\n       val listener = object : Query.Listener {\r\n\r\n           override fun queryResultsChanged() {\r\n\r\n               onChangeCallback(database.databaseQueries.selectAll().executeAsList())\r\n\r\n           }\r\n\r\n       }\r\n\r\n       observers[id] = listener\r\n\r\n       database.databaseQueries.selectAll().addListener(listener)\r\n\r\n   }\r\n\r\n}\r\n\r\nfun stopObservingTodos(id: Int) {\r\n\r\n   observers[id]?.let {\r\n\r\n       database.databaseQueries.selectAll().removeListener(it)\r\n\r\n   }\r\n\r\n}<\/pre>\n<p>To make SQLDelight actually work on both platforms, it needs a platform-specific database driver. We use the <em>actual\/expect<\/em> pattern for this task. For iOS, this works out of the box, for Android it does not because we need the famous Android <em>Context<\/em> to initialize the driver.<\/p>\n<pre class=\"lang:default decode:true \">\/\/ in comonMain source set\r\n\r\nexpect fun getDriver(): SqlDriver?\r\n\r\n\/\/ implementation in iOSMain source set\r\n\r\nactual fun getDriver(): SqlDriver? {\r\n\r\n   return NativeSqliteDriver(Database.Schema, \"main.db\")\r\n\r\n}\r\n\r\n\/\/ no implementation in androidMain source set\r\n\r\nactual fun getDriver(): SqlDriver? {\r\n\r\n   return null\r\n\r\n}\r\n\r\n\/\/ in Android activity (can also be done in Application class):\r\n\r\nval driver = AndroidSqliteDriver(Database.Schema, this.applicationContext, \"main.db\")\r\n\r\nCommonDatabase.driver = driver<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"HTTP-client\"><\/span>HTTP client<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As Android devs, we are happy to use <a href=\"https:\/\/square.github.io\/retrofit\/\">OkHTTP and Retrofit<\/a> to build API clients. With Kotlin\u00a0multiplatform, we cannot use these Java libraries. Fortunately, the new <a href=\"https:\/\/ktor.io\/\">Ktor framework<\/a> provides an HTTP client ready to be used in Kotlin multiplatform projects. Integration using Gradle dependencies is described in the<a href=\"https:\/\/ktor.io\/clients\/http-client\/multiplatform.html\"> official documentation<\/a>.<\/p>\n<p>As a replacement for <a href=\"https:\/\/github.com\/google\/gson\">Gson<\/a>, we use <a href=\"https:\/\/github.com\/Kotlin\/kotlinx.serialization\">Kotlinx-Serialization<\/a> to convert JSON strings to Kotlin classes and vice-versa.<\/p>\n<p>With these libraries and the wonderful SQLDelight database interface our API client is pretty small:<\/p>\n<pre class=\"lang:default decode:true\">internal class Api(private val repo: TodoRepository) {\r\n\r\n   private val client = HttpClient()\r\n\r\n   val baseUrl = \"https:\/\/jsonplaceholder.typicode.com\/\"\r\n\r\n   @Serializable\r\n\r\n   data class ApiTodo(val title: String, val completed: Boolean)\r\n\r\n   fun getTodos(): Job {\r\n\r\n       val url = baseUrl + \"todos\/\"\r\n\r\n       return GlobalScope.launch(applicationDispatcher) {\r\n\r\n           val result = client.call {\r\n\r\n               url(url)\r\n\r\n           }.response.readText()\r\n\r\n           val apiTodos = Json.nonstrict.parse(ApiTodo.serializer().list, result)\r\n\r\n           CommonDatabase.database.transaction {\r\n\r\n               apiTodos.forEach {\r\n\r\n                   repo.addTodo(it.title, it.completed)\r\n\r\n               }\r\n\r\n           }\r\n\r\n       }\r\n\r\n   }\r\n\r\n}<\/pre>\n<p>Did you notice the <em>applicationDispatcher<\/em> co-routine scope? Correct, we can use Kotlin co-routines on iOS and Android! Initialization is a bit of a bummer, though. On Android, it is as simple as<\/p>\n<pre class=\"\">actual val mainDispatcher = Dispatchers.Main as CoroutineDispatcher\r\n\r\nactual val backgroundDispatcher = Dispatchers.Default<\/pre>\n<p>On iOS, we copied the code from<a href=\"https:\/\/github.com\/Kotlin\/kotlinx.coroutines\/issues\/470#issuecomment-414635811\"> this issue&#8217;s comment<\/a> to use iOS\u2019s <em>dispatch_async<\/em> on the main queue for our co-routine dispatcher. Kotlin Native\u2014the LLVM compiler used in Kotlin multiplatform\u2014does not yet support co-routines on background threads, using a background queue will cause crashes instead. There is an <a href=\"https:\/\/github.com\/Kotlin\/kotlinx.coroutines\/issues\/462\">open issue in the Kotlinx repo<\/a> and we hope there will be a fix soon, because we do not want to run network calls on the main thread in production!<\/p>\n<pre class=\"lang:default decode:true\">actual val mainDispatcher = object : CoroutineDispatcher() {\r\n\r\n    override fun dispatch(context: CoroutineContext, block: Runnable) {\r\n\r\n        dispatch_async(dispatch_get_main_queue()) {\r\n\r\n            block.run()\r\n\r\n        }\r\n\r\n    }\r\n\r\n}\r\n\r\nactual val backgroundDispatcher = mainDispatcher \/\/ :'(\r\n\r\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Repositories\"><\/span>Repositories<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <em>TodoRepository<\/em> serves as a proxy class for the database. As a wrapper around the generated <em>Database<\/em> class we use a singleton <em>CommonDatabase<\/em>.<\/p>\n<pre class=\"lang:default decode:true\">@ThreadLocal\r\n\r\nobject CommonDatabase {\r\n\r\n   var driver: SqlDriver? = getDriver()\r\n\r\n   val database: Database by lazy {\r\n\r\n       Database(driver!!)\r\n\r\n   }\r\n\r\n   private val observers: MutableMap&lt;Int, Query.Listener&gt; = mutableMapOf()\r\n\r\n   fun addTodo(title: String, completed: Boolean) {\r\n\r\n       val completedNum = if (completed) 1 else 0\r\n\r\n       database.databaseQueries.insert(title, completedNum.toLong())\r\n\r\n   }\r\n\r\n  \/\/ [...]\r\n\r\n}<\/pre>\n<p><span id=\"freezing-behavior\">Because of Kotlin Native\u2019s<\/span> <a href=\"https:\/\/kotlinlang.org\/docs\/native-concurrency.html#object-transfer-and-freezing\">object freezing behavior<\/a>, singletons do not work very well with multi-threaded access on iOS. To prevent crashes on iOS, we use the @ThreadLocal annotation, which creates one copy of the object on each thread. It works for this example, but handle with care!<\/p>\n<p>When you get started with Kotlin multiplatform, prepare to run into such issues from time to time. Most things work without problems on Android because of the underlying JVM, but there might be some quirks when the same code is compiled to native LLVM bytecode.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"View-models\"><\/span>View models<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>To complete our architecture stack, we introduce a view model for the list of todos.<\/p>\n<pre class=\"lang:default decode:true\">class TodoViewModel(private val repo: TodoRepository = TodoRepository()) {\r\n\r\n   private val id: Int = idCounter.getAndIncrement()\r\n\r\n   private val api by lazy { Api(repo) }\r\n\r\n   fun observeTodos(onChangeCallback: (List&lt;Todo&gt;) -&gt; Unit) {\r\n\r\n       repo.observeTodos(id) {\r\n\r\n           GlobalScope.launch(mainDispatcher) {\r\n\r\n               onChangeCallback(it)\r\n\r\n           }\r\n\r\n       }\r\n\r\n   }\r\n\r\n   fun clearTodos() {\r\n\r\n       repo.deleteAll()\r\n\r\n   }\r\n\r\n  fun triggerSync() {\r\n\r\n       api.getTodos()\r\n\r\n   }\r\n\r\n   fun onDestroy() {\r\n\r\n       repo.stopObservingTodos(id)\r\n\r\n   }\r\n\r\n}<\/pre>\n<p>The view model is our interface towards the platform-specific implementation. From Swift, it can be used as follows:<\/p>\n<pre class=\"lang:default decode:true\">var todos: [String]?\r\n\r\nlet viewModel = TodoViewModel(repo: TodoRepository())\r\n\r\noverride func viewDidLoad() {\r\n\r\n    super.viewDidLoad()\r\n\r\n    viewModel.observeTodos(onChangeCallback: { (list: [Todo]) -&gt; KotlinUnit in\r\n\r\n        self.todos = list.map { (t) -&gt; String in return t.title }\r\n\r\n        self.tableView.reloadData()\r\n\r\n        return KotlinUnit()\r\n\r\n    })\r\n\r\n    syncData(self)\r\n\r\n}<\/pre>\n<p>Since our callback is expected to return the Kotlin type Unit, we have to return an instance of <em>KotlinUnit<\/em> in Swift.<\/p>\n<p>On Android, observing the data works similar:<\/p>\n<pre class=\"lang:default decode:true\">viewModel.observeTodos { todos -&gt;\r\n\r\n   if (adapter == null) {\r\n\r\n       adapter = ArrayAdapter&lt;String&gt;(this, android.R.layout.simple_list_item_1, todos.map { it.title })\r\n\r\n       todoList.adapter = adapter\r\n\r\n   } else {\r\n\r\n       adapter!!.apply {\r\n\r\n           clear()\r\n\r\n           todos.forEach { insert(it.title, count) }\r\n\r\n           notifyDataSetChanged()\r\n\r\n       }\r\n\r\n   }\r\n\r\n   Toast.makeText(this, \"Updated TODO list\", Toast.LENGTH_SHORT).show()\r\n\r\n}\r\n\r\noverride fun onDestroy() {\r\n\r\n   viewModel.onDestroy()\r\n\r\n   super.onDestroy()\r\n\r\n}\r\n\r\n<\/pre>\n<p>Please note that advanced architecture features like saved-state View Models and <em>LiveData<\/em> are not available. For smaller apps, we think it is feasible to implement by hand, for larger apps, <a href=\"https:\/\/medium.com\/@kpgalligan\/touchlab-square-collaborating-on-kotlin-multiplatform-eb6aa7eb4a4a\">we will have to wait<\/a> for advanced libraries aimed at Kotlin multiplatform.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Tests\"><\/span>Tests<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We thought about keeping the Unit tests in the shared codebase. The problem is that no JVM and JUnit framework can be used here. Thus we decided to put the Unit test for the <em>TodoViewModel<\/em> class into the Android JUnit test folder.<\/p>\n<pre class=\"lang:default decode:true\">class TodoViewModelTest {\r\n\r\n   private val mockedRepo = mockk&lt;TodoRepository&gt;(relaxed = true)\r\n\r\n   private val viewModel  = TodoViewModel(mockedRepo)\r\n\r\n   private val mainThreadSurrogate = newSingleThreadContext(\"UI thread\")\r\n\r\n   @Before\r\n\r\n   fun init() {\r\n\r\n       Dispatchers.setMain(mainThreadSurrogate)\r\n\r\n   }\r\n\r\n   @Test\r\n\r\n   fun testObserveTodos() {\r\n\r\n       val latch = CountDownLatch(1)\r\n\r\n       var resultList: List&lt;Todo&gt;? = null\r\n\r\n       val mockedTodo = mockk&lt;Todo&gt;()\r\n\r\n       every { mockedRepo.observeTodos(any(), any()) } answers {\r\n\r\n           secondArg&lt;(List&lt;Todo&gt;) -&gt; Unit&gt;().invoke(listOf(mockedTodo))\r\n\r\n       }\r\n\r\n       viewModel.observeTodos {\r\n\r\n           resultList = it\r\n\r\n           latch.countDown()\r\n\r\n       }\r\n\r\n       latch.await(500L, TimeUnit.MILLISECONDS)\r\n\r\n       assert(resultList != null)\r\n\r\n   }\r\n\r\n   \/\/ [...]\r\n\r\n}\r\n\r\n<\/pre>\n<p>The advantage for us Android developers is that we have all the tools available we know from regular Android apps, like <a href=\"https:\/\/mockk.io\/\">Mockk<\/a> for example. Mocking is also a lot easier on the JVM than on native platforms. A small disadvantage is that the common code is not unit-tested on iOS. Of course, we might naively assume that the behavior is the same as on the JVM, but experience in this first project taught us that this is not true. For instance, consider the object freezing issues <a href=\"#freezing-behavior\">mentioned above<\/a>.<b> <\/b><\/p>\n<h2 id=\"verdict\"><span class=\"ez-toc-section\" id=\"Verdict\"><\/span>Verdict<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our minimal sample app enables us to answer the three questions from the beginning:<\/p>\n<ol>\n<li><b>Production readiness<\/b>: For small projects, Kotlin\u00a0multiplatform\u00a0feels manageable. For larger projects, we would suggest to wait until IDE support in Android Studio and\/or Xcode has improved.Stack traces for crashes are human readable on both the JVM and LLVM, so crash monitoring on production is possible.A major showstopper at the moment\u00a0 is the lack of support for co-routines on a background thread\u2014<a href=\"https:\/\/github.com\/Kotlin\/kotlinx.coroutines\/issues\/462\">JetBrains is working on it, though.<\/a>\n<p>We did not test Integration with CI\/CD yet but we believe it can be achieved in the usual ways known from single-platform Android and iOS app projects.<\/li>\n<li><b>Building apps with clean architecture? <\/b>Yes, it is possible to some extent. Check out the example code and judge for yourself.<\/li>\n<li><b>Code sharing between platforms?<\/b> This was better than expected\u201455% of total LOC was in the shared code base.<\/li>\n<\/ol>\n<table>\n<tbody>\n<tr>\n<td>Platform<\/td>\n<td>LOC (including blanks)<\/td>\n<td>Percentage<\/td>\n<\/tr>\n<tr>\n<td>Shared (including tests)<\/td>\n<td>212<\/td>\n<td>55%<\/td>\n<\/tr>\n<tr>\n<td>Android (without XML-resources)<\/td>\n<td>83<\/td>\n<td>22%<\/td>\n<\/tr>\n<tr>\n<td>iOS<\/td>\n<td>91<\/td>\n<td>23%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16713 size-full\" style=\"padding-top: 50px; padding-bottom: 50px;\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution.png\" alt=\"Kotlin multiplatform code base pie chart\" width=\"1599\" height=\"908\" srcset=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution.png 1599w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-300x170.png 300w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-1024x581.png 1024w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-768x436.png 768w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-1536x872.png 1536w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-400x227.png 400w, https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/07\/kotlin-multiplatform-code-distribution-360x204.png 360w\" sizes=\"auto, (max-width: 1599px) 100vw, 1599px\" \/><\/a><\/p>\n<p>Based on the aforementioned results we strongly believe that Kotlin\u00a0multiplatform\u00a0is a technology worth pursuing. Of course, we are very interested in <b><i>your <\/i><\/b>opinion on this topic, so please drop a comment below.<\/p>\n<p>Lastly, a friendly note to any sales person reading this: We know you will do the math, scrapping 33% off the budget on the next project because 50% of the code can be shared between Android and iOS. Please don\u2019t be too optimistic\u2014cross-platform code is harder to write than single-platform code, even in Kotlin ? But we still think that sharing code will save some time in the end and also make maintenance of the code base for both platforms less of an effort.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As mobile developers, we often rewrite the same logic in another language, maintaining two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, which enables us to write the same code for the JVM as well as LLVM!\u00a0<\/p>\n","protected":false},"author":121,"featured_media":16748,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"ep_exclude_from_search":false,"footnotes":""},"tags":[510],"service":[420],"coauthors":[{"id":121,"display_name":"Jan Freymann","user_nicename":"jfreymann"}],"class_list":["post-21111","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","tag-apps-2","service-apps"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Kotlin Multiplatform for Clean Architecture - inovex GmbH<\/title>\n<meta name=\"description\" content=\"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\" \/>\n<meta property=\"og:locale\" content=\"de_DE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Kotlin Multiplatform for Clean Architecture - inovex GmbH\" \/>\n<meta property=\"og:description\" content=\"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\" \/>\n<meta property=\"og:site_name\" content=\"inovex GmbH\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/inovexde\" \/>\n<meta property=\"article:published_time\" content=\"2019-08-06T07:49:56+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2022-11-24T09:36:20+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1920\" \/>\n\t<meta property=\"og:image:height\" content=\"1080\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Jan Freymann\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero-1024x576.png\" \/>\n<meta name=\"twitter:creator\" content=\"@inovexgmbh\" \/>\n<meta name=\"twitter:site\" content=\"@inovexgmbh\" \/>\n<meta name=\"twitter:label1\" content=\"Verfasst von\" \/>\n\t<meta name=\"twitter:data1\" content=\"Jan Freymann\" \/>\n\t<meta name=\"twitter:label2\" content=\"Gesch\u00e4tzte Lesezeit\" \/>\n\t<meta name=\"twitter:data2\" content=\"11\u00a0Minuten\" \/>\n\t<meta name=\"twitter:label3\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data3\" content=\"Jan Freymann\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\"},\"author\":{\"name\":\"Jan Freymann\",\"@id\":\"https:\/\/www.inovex.de\/de\/#\/schema\/person\/6d82b20cb6437c22ca95caf5f798c7ec\"},\"headline\":\"Kotlin Multiplatform for Clean Architecture\",\"datePublished\":\"2019-08-06T07:49:56+00:00\",\"dateModified\":\"2022-11-24T09:36:20+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\"},\"wordCount\":1583,\"commentCount\":4,\"publisher\":{\"@id\":\"https:\/\/www.inovex.de\/de\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png\",\"keywords\":[\"Apps\"],\"articleSection\":[\"Applications\",\"English Content\",\"General\"],\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\",\"url\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\",\"name\":\"Kotlin Multiplatform for Clean Architecture - inovex GmbH\",\"isPartOf\":{\"@id\":\"https:\/\/www.inovex.de\/de\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png\",\"datePublished\":\"2019-08-06T07:49:56+00:00\",\"dateModified\":\"2022-11-24T09:36:20+00:00\",\"description\":\"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!\",\"breadcrumb\":{\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#breadcrumb\"},\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage\",\"url\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png\",\"contentUrl\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png\",\"width\":1920,\"height\":1080,\"caption\":\"Kotlin between an iPhone and a Pixel Phone\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.inovex.de\/de\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Kotlin Multiplatform for Clean Architecture\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.inovex.de\/de\/#website\",\"url\":\"https:\/\/www.inovex.de\/de\/\",\"name\":\"inovex GmbH\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/www.inovex.de\/de\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.inovex.de\/de\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"de\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.inovex.de\/de\/#organization\",\"name\":\"inovex GmbH\",\"url\":\"https:\/\/www.inovex.de\/de\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png\",\"contentUrl\":\"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png\",\"width\":1921,\"height\":1081,\"caption\":\"inovex GmbH\"},\"image\":{\"@id\":\"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/inovexde\",\"https:\/\/x.com\/inovexgmbh\",\"https:\/\/www.instagram.com\/inovexlife\/\",\"https:\/\/www.linkedin.com\/company\/inovex\",\"https:\/\/www.youtube.com\/channel\/UC7r66GT14hROB_RQsQBAQUQ\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.inovex.de\/de\/#\/schema\/person\/6d82b20cb6437c22ca95caf5f798c7ec\",\"name\":\"Jan Freymann\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\/\/www.inovex.de\/de\/#\/schema\/person\/image\/465a462143393c283b1eaad2f452250c\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/98a984b90a9e35cb97a02b24463e20689c670c1b909ef73b027608759611acf2?s=96&d=retro&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/98a984b90a9e35cb97a02b24463e20689c670c1b909ef73b027608759611acf2?s=96&d=retro&r=g\",\"caption\":\"Jan Freymann\"},\"url\":\"https:\/\/www.inovex.de\/de\/blog\/author\/jfreymann\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Kotlin Multiplatform for Clean Architecture - inovex GmbH","description":"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/","og_locale":"de_DE","og_type":"article","og_title":"Kotlin Multiplatform for Clean Architecture - inovex GmbH","og_description":"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!","og_url":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/","og_site_name":"inovex GmbH","article_publisher":"https:\/\/www.facebook.com\/inovexde","article_published_time":"2019-08-06T07:49:56+00:00","article_modified_time":"2022-11-24T09:36:20+00:00","og_image":[{"width":1920,"height":1080,"url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png","type":"image\/png"}],"author":"Jan Freymann","twitter_card":"summary_large_image","twitter_image":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero-1024x576.png","twitter_creator":"@inovexgmbh","twitter_site":"@inovexgmbh","twitter_misc":{"Verfasst von":"Jan Freymann","Gesch\u00e4tzte Lesezeit":"11\u00a0Minuten","Written by":"Jan Freymann"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#article","isPartOf":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/"},"author":{"name":"Jan Freymann","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/person\/6d82b20cb6437c22ca95caf5f798c7ec"},"headline":"Kotlin Multiplatform for Clean Architecture","datePublished":"2019-08-06T07:49:56+00:00","dateModified":"2022-11-24T09:36:20+00:00","mainEntityOfPage":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/"},"wordCount":1583,"commentCount":4,"publisher":{"@id":"https:\/\/www.inovex.de\/de\/#organization"},"image":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage"},"thumbnailUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png","keywords":["Apps"],"articleSection":["Applications","English Content","General"],"inLanguage":"de","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/","url":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/","name":"Kotlin Multiplatform for Clean Architecture - inovex GmbH","isPartOf":{"@id":"https:\/\/www.inovex.de\/de\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage"},"image":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage"},"thumbnailUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png","datePublished":"2019-08-06T07:49:56+00:00","dateModified":"2022-11-24T09:36:20+00:00","description":"As mobile developers, we often rewrite the same logic in a different language, having to maintain two similar codebases for Android and iOS. Wouldn\u2019t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, enabling us to write the same code for the JVM as well as LLVM!","breadcrumb":{"@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#breadcrumb"},"inLanguage":"de","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/"]}]},{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#primaryimage","url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png","contentUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2019\/08\/Kotlin-Multiplatform-hero.png","width":1920,"height":1080,"caption":"Kotlin between an iPhone and a Pixel Phone"},{"@type":"BreadcrumbList","@id":"https:\/\/www.inovex.de\/de\/blog\/kotlin-multiplatform-for-clean-architecture\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.inovex.de\/de\/"},{"@type":"ListItem","position":2,"name":"Kotlin Multiplatform for Clean Architecture"}]},{"@type":"WebSite","@id":"https:\/\/www.inovex.de\/de\/#website","url":"https:\/\/www.inovex.de\/de\/","name":"inovex GmbH","description":"","publisher":{"@id":"https:\/\/www.inovex.de\/de\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.inovex.de\/de\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"de"},{"@type":"Organization","@id":"https:\/\/www.inovex.de\/de\/#organization","name":"inovex GmbH","url":"https:\/\/www.inovex.de\/de\/","logo":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/","url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png","contentUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png","width":1921,"height":1081,"caption":"inovex GmbH"},"image":{"@id":"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/inovexde","https:\/\/x.com\/inovexgmbh","https:\/\/www.instagram.com\/inovexlife\/","https:\/\/www.linkedin.com\/company\/inovex","https:\/\/www.youtube.com\/channel\/UC7r66GT14hROB_RQsQBAQUQ"]},{"@type":"Person","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/person\/6d82b20cb6437c22ca95caf5f798c7ec","name":"Jan Freymann","image":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/person\/image\/465a462143393c283b1eaad2f452250c","url":"https:\/\/secure.gravatar.com\/avatar\/98a984b90a9e35cb97a02b24463e20689c670c1b909ef73b027608759611acf2?s=96&d=retro&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/98a984b90a9e35cb97a02b24463e20689c670c1b909ef73b027608759611acf2?s=96&d=retro&r=g","caption":"Jan Freymann"},"url":"https:\/\/www.inovex.de\/de\/blog\/author\/jfreymann\/"}]}},"_links":{"self":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts\/21111","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/users\/121"}],"replies":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/comments?post=21111"}],"version-history":[{"count":3,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts\/21111\/revisions"}],"predecessor-version":[{"id":39555,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts\/21111\/revisions\/39555"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/media\/16748"}],"wp:attachment":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/media?parent=21111"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/tags?post=21111"},{"taxonomy":"service","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/service?post=21111"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/coauthors?post=21111"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}