The apply scope function is a very useful Kotlin standard library method for grouping long variable configuration blocks, when no builder is available. It can be used when creating functions that return the instance of the object (in a builder like fashion), like in the following code:

class MyClass {
  fun func(): MyClass = apply {
    // add code here
  }
}

Moreover, it is especially convenient when using older APIs which do not provide a builder. For example:

class MyClass {
  fun func(person: Person, friendList: List<String>) {
    val bunde = Bundle().apply {
      putString("name", person.fullName
      putInt("age", person.age)
      putBoolean("isActive", person.isActive)
      putStringList("friends", ArrayList<String>(friendList))
    }
  }
}

Thus, it provides a means of creating and initialising a complex object in a val property of a class. In order words, you can do something like this:

class MyClass(person: Person, friendList: List<String>) {
  private val myBundle: Bundle = Bundle().apply {
      putString("name", person.fullName
      putInt("age", person.age)
      putBoolean("isActive", person.isActive)
      putStringList("friends", ArrayList<String>(friendList))
    }
}

However, often in development, when the requirements change and different (although not always) developers update the same piece of code, there is often little attention paid to minor code quality degradation. For example, consider updating the above example with code to add more filtering in the friendsList:

class MyClass(person: Person, friendList: List<String>) {
  private val myBundle: Bundle = Bundle().apply {
      putString("name", person.fullName
      putInt("age", person.age)
      putBoolean("isActive", person.isActive)
      putStringList(
        "friends", 
        friendList.filter { " " in it }
            .map {
                it.split(" ")
                    .zipWithNext()
                    .map { pair -> "${pair.second}, ${pair.first}" }
            }
            .flatten()
            .fold(ArrayList<String>()) { acc, item ->
                acc.add(item)
                acc
            }
      )
    }
}

Then someone might naively attempt to extract the friendsList filtering into an external method:

class MyClass(person: Person, friendList: List<String>) {
  private val myBundle: Bundle = Bundle().apply {
      putString("name", person.fullName
      putInt("age", person.age)
      putBoolean("isActive", person.isActive)
      filterFriendsList(friendsList)
    }

    private fun filterFriendsList(friendList: List<String>) {
        myBundle.putStringList(
          "friends", 
          friendList.filter { " " in it }
              .map {
                  it.split(" ")
                      .zipWithNext()
                      .map { pair -> "${pair.second}, ${pair.first}" }
              }
              .flatten()
              .fold(ArrayList<String>()) { acc, item ->
                  acc.add(item)
                  acc
              }
      )
    }
}

Now this method extraction has introduced a very simple NullPointerException bug, one which may surprise you somewhat when running the code. It is certainly non-obvious and easily missed, especially if you’ve been on top of the code for a while. Running the above snippet throws a NullPointerException in the following line:

myBundle.putStringList(

The variable myBundle is null. Although at first this may be confusing it has a very simple explanation. Let’s focus on this line here:

  private val myBundle: Bundle = Bundle().apply {

When a new MyClass object is created, the myBundle variable will have to be initialised. This initialisation is achieved by creating a new Bundle object, then running the apply block on the newly created object and then taking the result and assigning it to the myBundle variable. So the code of the filterFriendsList would work as it was written previously, because it was executing in the context of the newly created Bundle object, but cannot work as is without the myBundle instance having been initialised first.

There are multiple solutions to this problem. Do not forget that it was introduced by adding excessive logic in the variable initialisation, inside the apply block. Therefore the simplest solution is to extract all the initialisation logic in a separate function. For example:

class MyClass(person: Person, friendList: List<String>) {
  private val myBundle: Bundle = createMyBundle(person, friendList)

  private fun createMyBundle(person: Person, friendList: List<String>): Bundle {
      val bundle = Bundle()
      bundle.putString("name", person.fullName
      bundle.putInt("age", person.age)
      bundle.putBoolean("isActive", person.isActive)
      bundle.putStringList(
          "friends", 
          friendList.filter { " " in it }
              .map {
                  it.split(" ")
                      .zipWithNext()
                      .map { pair -> "${pair.second}, ${pair.first}" }
              }
              .flatten()
              .fold(ArrayList<String>()) { acc, item ->
                  acc.add(item)
                  acc
              }
      )
      return person
    }
}

Now all the code needed is in a single function, it is clear that it creates a new object instance, and it can be further refactored as needed in the future.