Using the `kotlin-dsl` Gradle Plugin forces Kotlin 1.4 Compatibility 😱

I previously wrote an article “Sharing Gradle Configuration in Multi-Module Android Projects” about re-using Gradle configuration using the “apply” feature to reduce boilerplate and provide consistency. It’s super helpful and handy, but there is now a better way to do the same kind of thing using Gradle Convention Plugins.

Gradle Convention Plugins

There is a great post by Tony Robalik that goes into the benefits of Gradle Convention Plugins. I say it is “better” because it can be pre-compiled, written in Kotlin and tested in Kotlin. These convention plugins are most easily added in buildSrc, so I figured I’d start there when adding it to my existing project.

My journey writing a Gradle Convention Plugin and how I ran into Kotlin 1.4

So, in a large Android project I added the “kotlin-dsl” plugin to the buildSrc module and things blew up. I’m using the latest version of Gradle 7.4.2, yet it is telling me:

Language version 1.4 is deprecated and its support will be removed in a future version of Kotlin

I had specified Kotlin 1.6.10 everywhere! What was I doing wrong?

Nothing. It’s intentional. Gradle even calls out why on their site:

Gradle plugins written in Kotlin target Kotlin 1.4 for compatibility with Gradle and Kotlin DSL build scripts, even though the embedded Kotlin runtime is Kotlin 1.5.

Even though it is intentional, it wasn’t immediately clear to me, and to others, but I understand the thinking behind it now, and in this post show you how you can get around it if you need to.

Martin Bonnin has a great post talking about how you could get around this by doing some crazy things like creating shadow jars, but his answer of “Should I use this in production?” was “it depends”, and where possible, I try to not use complex workarounds (even though this post is a less complex workaround 😂).

Gradle 7.4.2 still targets Kotlin 1.4 with the kotlin-dsl plugin, even though 1.5.31 is embedded now in Gradle 7.4.2. Gradle plugin compatibility is very important when distributing plugins publicly, but if you are just going to use them in your team or organization, you may not want to support old version of Kotlin going back to 1.4.

In order to use the version of Kotlin available in the version of Gradle you have, you need to specify the version yourself to override the default.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.5"
            languageVersion = "1.5"

This will allow you to use Kotlin 1.5 syntax now when using the “kotlin-dsl” plugin!

One thing that is weird about this is the need for afterEvaulate. I’m not 100% sure why it was needed, but it’s how they specify it in Gradle source code, and it doesn’t work without adding afterEvaluate.

But… what about Kotlin 1.6?

If you really want to use Kotlin 1.6 though, fear not! Kotlin 1.6.10 is going to be available with Gradle 7.5 when it comes out. When it does, you should be able to use this configuration to use 1.6 compatibility:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.6"
            languageVersion = "1.6"

Kotlin/Gradle Version Table when using kotlin-dsl

Gradle VersionEmbedded Kotlin VersionDefault Api Version
7.5 (Unreleased as of April 14, 2022)

What do you recommend?

If you are only going to use this plugin internally, and everyone is using the same version of Kotlin everywhere, this seems pretty safe. If you are looking to open source something for public use, you may need to call out a minimum Gradle version required use your Plugin.

Should I put my Gradle Convention Plugin in buildSrc?

This is the first place you should try it out. You will have the same issue with any code you put in buildSrc though, where if any code changes, all code and tests in buildSrc have to be re-run which increases build times.

If you plan to use this a lot going forward, use an includeBuild to add in your Convention Plugins going forward so that you only re-compile when that code changes, and so you could publish a binary to avoid any compilation at all.


Thanks Martin Bonnin and Tony Robalik for reviewing the article. Also special thanks to Martin for helping me dig into the Gradle source to figure this out!

Kotlin Multiplatform: Building a “Fat” iOS Framework for iosArm64 and iosX64

If you are building a Kotlin Multiplatform library which will be consumed by an existing iOS application, using a framework is a great way to do this. Frameworks are typically compiled for a specific target architecture which can then be run on an iPhone, iPad or iOS Simulator on your Intel Macbook.

If you are going use your Kotlin Multiplatform library as a framework in an existing app, you will want to provide a “Fat” framework which will contain both Arm64 and X64 architectures. This article contains the configuration I used to build the “Fat” framework. This is not a full build.gradle.kts file, but just the additional parts needed for a Kotlin Multiplatform project to build a “Fat” framework.

⚠️Use an XCFramework instead ⚠️

This “Fat” framework method no longer works with XCode 12+. Use an XCFramework instead.

I ended up with this Error Message in XCode 12.4: “Building for iOS Simulator, but the linked and embedded framework ‘my_library.framework’ was built for iOS + iOS Simulator.”

This Stack Overflow post shows you how to create an XCFramework from your two frameworks, and I’ll follow up with a blog post on how to do it with an XCFramework a bit later. This solutions ends up combining the two frameworks into a single XCFramework.

xcrun xcodebuild -create-xcframework \
    -framework /path/to/ios.framework \
    -framework /path/to/sim.framework \
    -output combined.xcframework

Custom Gradle Task to Build “Fat” framework

import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask

kotlin {
    // Set a name for your framework in a single place and reuse the variable
    val libName = "my_library"

    // Configure your Kotlin Multiplatform lib to generate iOS binaries
    // NOTE: This will only work on Macs
    ios {

    // You can choose your output directory
    val frameworkDestinationDir = buildDir.resolve("cocoapods/framework")

    tasks {

        // Custom task to build the DEBUG framework
        // ./gradlew universalFrameworkDebug
        register("universalFrameworkDebug", FatFrameworkTask::class) {
            baseName = libName
                iosArm64().binaries.getFramework(libName, "Debug"),
                iosX64().binaries.getFramework(libName, "Debug")
            destinationDir = frameworkDestinationDir
            group = libName
            description = "Create the debug framework for iOS"

        // Custom task to build the RELEASE framework
        // ./gradlew universalFrameworkRelease
        register("universalFrameworkRelease", FatFrameworkTask::class) {
            baseName = libName
                iosArm64().binaries.getFramework(libName, "Release"),
                iosX64().binaries.getFramework(libName, "Release")
            destinationDir = frameworkDestinationDir
            group = libName
            description = "Create the release framework for iOS"

Here are two custom gradle tasks that build a “Fat” framework for debug or release. In this I have it outputting to the build/cocoapods/framework directory, but you can configure that as you like.

Gradle Task for “Fat” iOS framework

  • Build a “Fat” debug version of the framework
    • ./gradlew universalFrameworkDebug
  • Build a “Fat” release version of the framework
    • ./gradlew universalFrameworkRelease

Importing the iOS Framework into XCode

I previously wrote a blog post about how to do this which has a companion video along with it. 👇

Thanks and Related Resources

I didn’t figure this all out myself. I just got it to work for me and extracted out the bare minimum you need to make this work. Thanks to Swapnil Patil for letting me know that “Fat” frameworks are possible. Thanks so much to Marco Gomiero for his post Introducing Kotlin Multiplatform in an existing project.

Run Custom Gradle Task After “build”

After running ./gradlew build on my Kotlin Multiplatform project, I wanted to copy the JavaScript build artifacts (.js & .html) to publish a demo where someone could test my library via a web browser. A custom Gradle task is is a great way to write your own behavior to accomplish this using the Gradle build system.

My Custom Gradle Task

I found the Gradle documentation for how to copy files, and was able to write this custom task "myTaskName" to do it. You can then just run ./gradlew myTaskName and it’ll run the task independently. The problem was that I didn’t know how to get it to always run after ./gradlew build ran.

/** Copies files from "build/distributions" to "demo" directory */
tasks.register<Copy>("myTaskName") {
    println("Copying Build Artifacts!!!")

TL;DR – Use finalizedBy()

  • finalizedBy – Runs my task AFTER “build”. ✅
    • tasks.named("build") { finalizedBy("myTaskName") }
  • dependsOn – Runs “build” BEFORE my task, if my task is executed explicitly.
    • tasks.named("myTaskName") { dependsOn("build") }

To give you more details and to go into my process of how I figured it out, here are a few options I tried while figuring out how to run this custom task after every execution of the “build” Gradle task.

1. finalizedBy ✅

You can use finalizedBy() to say what task you should run after a named task. I think this reads nicely because it calls out the task dependency separately from it’s declaration. This will appropriately run after the build task executes as I needed.

tasks.named("build") { finalizedBy("myTaskName") }

2. shouldRunAfter 🤔

Another possible way to do this is with shouldRunAfter() which can just be added inside the block where you register your custom task. This will run after the build task executes. HOWEVER: I had to call .get() after registering my task to get it to work in order to have this actually run, which just feels wrong… Someone feel free to explain why, but I’m guessing this is some sort of lazy initialization happening if I don’t call “.get()”. Because of this, I don’t like this solution personally.

tasks.register<Copy>("myTaskName") {
    shouldRunAfter("build") // Tells Gradle to execute after "build" task
    // My Custom Task Code

3. dependsOn() 🙃

You would think dependsOn() may work, but it’s the opposite behavior than what I wanted. It’s saying that “myTaskName” needs “build” to have run before it executes. It’s the opposite task dependency relationship from finalizedBy(). This wasn’t the behavior I wanted for this use case because I wanted it to copy the artifacts after every time a ./gradlew build was run, but could be useful depending on your use case.

tasks.named("myTaskName") { dependsOn("build") }

Wrap Up

None of this is rocket science, but if you try to Google for how to do it, it may take you a while to figure out how to do it. Using Option 1, finalizedBy() was the solution that worked for my use case! I hope this saved you an hour or more!

Intro to Kotlin Multiplatform JavaScript

One of the compilation targets of Kotlin Multiplatform is JavaScript. It’s pretty awesome that I can take the same Kotlin code and tools I write Android with, and write code that runs in a browser (or in Node.js).

Here is the video I’ve created to walk you through the creation of a Kotlin Multiplatform Project using the templates in Intellij IDEA, and what you can do to get your Kotlin running as JavaScript in the Browser!

Here are the code snippets mentioned in the video:

./gradlew build
 js(LEGACY) {
     browser {
         webpackTask {
             output.libraryTarget = "this" // Will add to window
 fun printHi() {
     println("Hello World Sam!")
 fun main(args: Array) {

The Best Way to Collect a Flow in Kotlin – launchIn

At some point you’ll need to collect (receive items) from a Flow (reactive stream) within a Kotlin Coroutine.  More than likely you will use a launch on a CoroutineScope, and then collect like this:

scope.launch {
    .onEach { println(it) }

This works great, but there is a better way for most use cases. It’s using a function called launchIn. What’s launchIn? It’s just shorthand to do what you did above. This is the equivalent logic as above, but using launchIn.

  .onEach { println(it) }

This is less code to write, but more importantly it’ll get you out of some hard to debug situations when collecting from Flows. The non obvious thing to understand is that collect() will block the coroutine until the flow has finished emitting. This behavior is sometimes desired, but for me it’s not in most cases.

In the example below, you’d think that both Flows are being collected at the same time, but flow1 is collected until the Flow finishes emitting, and then flow2 is collected until it is finished emitting.

scope.launch {
    .onEach { println(it) }
  // Will not run until flow1 finishes emitting
    .onEach { println(it) }

To collect both in parallel, you’d need to write this:

scope.launch {
    .collect { println(it) }
scope.launch {
    .collect { println(it) }

This is where launchIn comes to the rescue to make this reach much easier in my opinion. Here is the equivalent using launchIn:

  .onEach { println(it) }
  .onEach { println(it) }

I like launchIn because it’s less code to write, I don’t have to have indentation, and I just found it easier to understand.

In no way does this mean that the normal launch() and collect() aren’t great things to use, but for most use cases, I’d suggest considering using launchIn().

My MutableStateFlow Didn’t Emit!

I am in love with MutableStateFlow which is part of the Kotlin Coroutines Library. It makes managing state, and exposing it as a reactive Flow so easy.

You just create a MutableStateFlow(initialValue), and then you can later on just set myMutableStateFlow.value = newValue, and if it has changed, it’ll emit a new event as a reactive event on the Flow!

This is amazing, but in order to make this work the way you want it to, you HAVE to use Immutable Data Structures.

Why? Glad you asked, here we go.

How MutableStateFlow’s “value” Property Works

Just set mutableStateFlow.value=newValue and the value will be emitted! This Flow is a great way to keep track of state where you only want to emit when there is a change.

You can set your mutableStateFlow.value many times, but unless !oldValue.equals(newValue), then a new value won’t be emitted. That way subscribers don’t need to use distinctUntilChanged() to filter out duplicates.

Scenario where “My MutableStateFlow Didn’t Emit!

I’ve simplified a scenario I ran into to share how MutableStateFlow may not work the way you think you should. I banged my head on the wall trying to figure out what was happening, and hopefully this simplified example will save you a headache.

/** Class with Mutable Data */
data class SomePojo(var name: String = "placeholder")
val somePojo = SomePojo()
val mutableStateFlow = MutableStateFlow(somePojo)
println("INITIAL: ${mutableStateFlow.value}")

// Update the value = "Something Different"

// Print MutableStateFlow current value, but realize the value has changed because the data in that object has been mutated
println("CURRENT: ${mutableStateFlow.value}")

// Try to assign the new value to the MutableStateFlow, but they are already equal!  This means no emissions will occur.
mutableStateFlow.value = somePojo

// The result is the same, and no value was emitted.
println("UPDATED: ${mutableStateFlow.value}")

The output will be:

INITIAL: SomePojo(name=placeholder)
CURRENT: SomePojo(name=Something Different)
UPDATED: SomePojo(name=Something Different)

How I Went Wrong

  • My state model was “SomePojo” which had a mutable var “name” on it.
  • The “somePojo” variable was initially set as mutableStateFlow.value
  • I set”Something Different” to change the name.
  • I wanted to emit the new state, so set mutableStateFlow.value=somePojo.
  • My MutableStateFlow Didn’t Emit!
  • 🤔
  • The current value of mutableStateFlow.value was already set to myPojo, and I had modified the property “name” on it.
  • As seen above, MutableStateFlow.value will emit unless the oldValue.equals(newValue). Because I had modified the mutable property name, when the equals() comparison happened.
  • The value was already the same, and therefore no change occurred, and there was no emission.
  • Whoa… that’s confusing. Yes, but it makes sense now.


There are things you can do to avoid running into this problem. Use immutable data structures (classes with all val properties) with MutableStateFlow to avoid unexpected behavior.

If you mutate (change) the value of an object that’s currently the state of a mutable state flow, it won’t emit a new value when re-assigned because it only emits when the value has changed. In this case the mutable value has changed… but so has the underlying state, so when compared it looks as if nothing has changed.

Try for yourself with this Unit Test Gist:

Wrapping Mockito Mocks for Reusability

My general advice about Mockito is to try and avoid it when you can. Ryan Harter has an awesome companion post called Replacing Mocks which shows some potential pitfalls of using Mockito Mocks, as well as how to avoid using it by restructuring code. However, sometimes Mockito can be the right tool to use for mocking dependencies in a unit test when a code refactor isn’t possible due to constraints. I use Mockito-Kotlin which helps leverage Mockito in Kotlin code.

In this post I show how you can wrap a Mockito mock to avoid using verbose syntax (“whenever”, “verify”, etc.) all over the place. I call this wrapper around the Mockito mock a “Fake”.

// Fake Wrapper around Mockito Mock (See implementation below)
val fakeOven = FakeOven()

// Access and Use Mockito Mock
val oven : Oven = fakeOven.mock

// Clean API to Setup Mocks (using Wrapper)

// Clean API to Verify Mocks (using Wrapper)
fakeOven.thenOvenSetTo(temperatureFahrenheit = 350, timeMinutes = 30)


  • Single Responsibility – Mocking logic out of your test class.
  • Cleaner Tests – Avoid using “when”, “whenever” and “verify” all over the place.
  • Less Duplication – Can be reused across tests and for future tests you may write.

Example: Baking with an Oven

In this example we bake a Cake which requires an Oven. We need to mock the Oven.

NOTE: We could try various approaches for this example, but for the purpose of explaining this strategy, we’ll use the “Fake” Mockito wrapper.

/** Class that uses [Oven] */
class Dessert(val oven: Oven) {
    fun bakeCake(): OvenResult {
        return oven.start()
/** Class we will use Mockito to Mock */
class Oven {
    fun setTemperatureFahrenheit(tempF: Int) {
        // ...

    fun setTimeMinutes(minutes: Int) {
        // ...

    fun start(): OvenResult {
        // ...
/** Whether the Oven command was successful, or something happened */
sealed class OvenResult {
    object Success : OvenResult()
    data class Failure(val e: Exception) : OvenResult()

Original Test 🤷🏽‍♂️

import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import org.junit.Test

/** Test Dessert Baking */
class DessertTest {

    fun bakeCakeSuccess() {
        val oven: Oven = mock()
        val dessert = Dessert(oven)

        // Setup

        // Execute Code

        // Verification

FakeOven – Mockito Mock Wrapper 🤔

/** Wraps the Mockito mock for reuse */
class FakeOven {

    val mock: Oven = mock()

    fun givenOvenResult(ovenResult: OvenResult) {
        // Setup

    fun thenOvenSetTo(temperatureFahrenheit: Int, timeMinutes: Int) {
        // Verification

Updated Test – Using Fake Mockito Wrapper ✅

class DessertTestWithFake {

    fun bakeCakeSuccess() {
        val fakeOven = FakeOven()
        val dessert = Dessert(fakeOven.mock)

            temperatureFahrenheit = 350,
            timeMinutes = 30

We use “fakeOven.mock” to fulfill the “Oven” dependency, and control the behavior using the wrapper we have created.

val fakeOven = FakeOven()
val oven : Oven = fakeOven.mock


Isolate usage of Mockito, and avoid scattering it all over your tests. By using this pattern of wrapping a Mockito Mock in a fake, your tests that require Mockito are a little bit better.

SQLDelight 1.x Quick Start Guide for Android

SQLDelight is most well known as a Kotlin multiplatform database library. As an Android Developer, the most compelling reasons to use SQLDelight are:

  • Kotlin first
  • SQL first
  • Typesafe generated code
  • Unit tests don’t require an Android device

Documentation for how to use SQLDelight on just Android (without Kotlin mutliplatform) is lacking, so I wanted to create this guide to get people going fast on just Android.  This example is here to get you started, but does not reflect best coding practices.  I did this in order to make this quick start guide concise, and approachable.

1) Add Gradle Plugin to the buildscript classpath

In your project’s root build.gradle file, add the following dependency under buildscript dependencies.

classpath "com.squareup.sqldelight:gradle-plugin:1.3.0"

2) Apply the Gradle Plugin to your App or Module

You need to add the SQLDelight Gradle Plugin to your app or module because SQLDelight uses code generation from SQL files (*.sq), instead of code generation based on annotations (kapt).  This gives us incremental builds, but requires us to add a plugin to our root build.gradle file.  By doing code generation from SQL files, SQLDelight can generate code that works on any platform, because at the end of the day you are just executing SQL statements.

apply plugin: "com.squareup.sqldelight"

3) Add/Write the SQL (.SQ) File

You need to create a source folder for your SQL (*.sq) files at “src/main/sqldelight” folder, next to “java” or “kotlin” source folder. In this example, we’ll be representing a “item” in a shopping cart (Example from Shopping App) and the ItemInCartEntity.sq file in the following location:



    image TEXT NOT NULL,
    link TEXT

FROM itemInCart
ORDER BY label;

VALUES (?, ?, ?, ?);

FROM itemInCart
WHERE label = ?;


FROM itemInCart
WHERE label = ?;

4) Install the SQLDelight Android Studio Plugin

The Android Studio Plugin is not required, but is super helpful for syntax highlighting, code completion, warnings and navigation.

.sq file without the SQLDelight Android Studio Plugin

Adding the SQLDelight Android Studio Plugin

A .sq file after the SQLDelight Android Studio Plugin is Installed

5) Add the Dependency Used for in Memory Unit Tests

You need the JdbcSqliteDriver for Unit Tests. This allows you to run your database tests without Android device. This means blazing fast tests without the hassle of connecting a device!

Add the following Unit Test dependency:

testImplementation "com.squareup.sqldelight:sqlite-driver:1.3.0"

Note: You don’t have to write unit tests and could technically skip this and the next step, but do yourself the favor and write tests from the beginning.

6) Write Unit Tests

Now that everything is set up, you should write a unit test that runs on your computer to make sure it’s all working.  This is a huge benefit over the Room library that comes with Android Jetpack because we can completely decouple ourselves from knowing what Android is.  This allows us to verify our setup and to have a quick feedback loop to ensure everything is working.

Use the JdbcSqliteDriver for an in memory version of your database for unit tests to avoid state between tests. However, be sure to also call Database.Schema.create(sqlDriver) or your in memory tests will not work. In order to make sure I call it, I use an “apply” to make sure I do it along with creating an instance of the driver.

private val inMemorySqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).apply {

Here is the full ItemDatabaseTest.kt file containing the Unit Test.

package com.handstandsam.sqldelightquickstart

import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import org.junit.Assert.assertEquals
import org.junit.Test

class ItemDatabaseTest {

    private val inMemorySqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).apply {

    private val queries = Database(inMemorySqlDriver).itemInCartEntityQueries

    fun smokeTest() {
        val emptyItems: List = queries.selectAll().executeAsList()
        assertEquals(emptyItems.size, 0)

            label = "Pineapple",
            image = "https://localhost/pineapple.png",
            quantity = 5,
            link = null

        val items: List = queries.selectAll().executeAsList()
        assertEquals(items.size, 1)

        val pineappleItem = queries.selectByLabel("Pineapple").executeAsOneOrNull()
        assertEquals(pineappleItem?.image, "https://localhost/pineapple.png")
        assertEquals(pineappleItem?.quantity?.toInt(), 5)

7) Add the SQLDelight Android Driver Dependency

Now that we have the plugin working and unit tests passing, we need to integrate with our Android app. Add this “implementation” AndroidSqliteDriver dependency on SQLDelight.

implementation "com.squareup.sqldelight:android-driver:1.3.0"

If you only have an “app” module, then add it to that, but if you are in a multi-module project, I would highly suggest creating a “db” module (or similar) for this code.

8) Write your SQLDelight Android Code

In order to just show this working on Android, I pasted the code into the MainActivity, which is not what you should do, but it helps you validate that it’s actually working on Android.

You will need to use the AndroidSqliteDriver in order for SQLDelight to correctly write to the Android database. The JdbcSqliteDriver was helpful for allowing us to do in-memory unit testing, but it only keeps the database in memory, and would never save between app launches.

val androidSqlDriver = AndroidSqliteDriver(
    schema = Database.Schema,
    context = applicationContext,
    name = "items.db"

val queries = Database(androidSqlDriver).itemInCartEntityQueries

val itemsBefore: List = queries.selectAll().executeAsList()
Log.d("ItemDatabase", "Items Before: $itemsBefore")

for (i in 1..3) {
        label = "Item $i",
        image = "https://localhost/item$i.png",
        quantity = i.toLong(),
        link = null

val itemsAfter: List = queries.selectAll().executeAsList()
Log.d("ItemDatabase", "Items After: $itemsAfter")

9) Run The Code on Android

Hit the run button and filter Logcat so you can see that you have successfully added and retrieved data from SqlDelight on Android!

10) Peek at the “Magically” Generated Code

It’s cool to see where the plugin puts the code it generates in “build/sqldelight” and it may help you understand how SQLDelight works. The generated code is super easy to read since it uses Kotlin Data Classes, and the SQL code is just taken almost directly from your .sq file that you already wrote, but wrapped in a type-safe way.


These steps are all you need to get started with SQLDelight 1.x on Android.  Here is a pull request that contains all the changes mentioned in this post:  This article was written when version 1.1.4 was released, but has been updated to version 1.3.0.  Check out the SQLDelight change-log to see the latest released version.

Enjoy the beautiful generated Kotlin code which is generated from our .sq files, and enjoy validating your code via unit tests that can run without an Android device!

Related Links:

“It Depends” Is The Answer To Your Android Question

Android Questions:

  • Should I use Kotlin Multiplatform? “It Depends”
  • Should I use Kotlin Multiplatform for UI? “It Depends”
  • Should I use an Actor or StateFlow? “It Depends”
  • Should I use Mockito? “It Depends”
  • Should I put my Kotlin code in src/main/java? “It Depends”
  • Should I use Flutter? “It Depends”
  • Should I wrap a 3rd party library’s API? “It Depends”
  • Should I install the Android Q Beta? “It Depends”
  • Should I use Dagger? “It Depends”
  • Should I use Kotlin or Java? “It Depends”
  • Should I use React Native? “It Depends”
  • Should I use Dependency Injection? “It Depends”
  • Should I use OkHttp? “It Depends”
  • Should I use Multiple Activities or a Single Activity? “It Depends”
  • Should I upgrade to the latest Support Library? “It Depends”
  • Should I use a library that’s in Alpha? “It Depends”
  • Should I use Room or SqlDelight? “It Depends”
  • Should I write Espresso tests? “It Depends”
  • Should I bump my minSdk to 28? “It Depends”
  • Should I use MVP or MVVM or MVI or MVC? “It Depends”
  • Should I use RxJava or LiveData? “It Depends”
  • Should I learn Android or iOS? “It Depends”
  • Should I start a blog? “It Depends”
  • Should I start a side project? “It Depends”
  • … Insert Your Question Here  … “It Depends”

“It Depends” Is Technically Correct 💯% of the Time

While “It Depends” is technically correct since there is no absolute answer in software, it still doesn’t make it a good answer.  “Probably Should” is an answer well, but when you think about it, it’s still a variation of “It Depends”.

“It Depends” Is a Crappy Answer

Juhani brings up a great point about responsibility in his tweet above.  Opinions are great because they are shaped by experience.  As you gain experience, it’s important to share your opinions and discoveries because they will help provide insight and context into a topic.  That empowers the person searching for the answer to make a decision, because you can’t write software with “It Depends”, since it won’t compile. 😂

There Is No Perfect Answer

My goal of this post was to point out that there are tons of ways to do things, but based on your team, use case and target audience, there is no good 100% right answer for any topic.  I can strongly urge you to use Kotlin, but if you are building something that people are willing to pay millions of dollars from and they need Java, then Java is your right answer.  The important part is to keep listening to opinions and discoveries that are brought up, but know they are not a one-size-fits-all solution.

I’m happy to share my opinion and experiences with you on any topic if you reach out to me on Twitter @HandstandSam, and I’ll do my best not to answer with “It Depends”. 🙂