Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin 2 ½ Years Later

Kotlin 2 ½ Years Later

This is a "brief" story about how Cody Engel uses Kotlin along with some general tips/tricks/ideas that can be applied to your own projects.

Cody Engel

April 17, 2019
Tweet

More Decks by Cody Engel

Other Decks in Technology

Transcript

  1. data class Speaker(val name: String = "Cody") val speaker =

    Speaker() val stringTemplates = """ Wow this is cool. Thanks, ${speaker.name} """.trimIndent() What I’m Not Talking About
  2. data class Speaker(val name: String = "Cody") val speaker =

    Speaker() val stringTemplates = """ Wow this is cool. Thanks, ${speaker.name} """.trimIndent() What I’m Not Talking About
  3. fun conditionalExpressions(a: Int, b: Int): Int { if (a >

    b) { return a } else { return b } } What I’m Not Talking About
  4. fun conditionalExpressions(a: Int, b: Int): Int { return if (a

    > b) { a } else { b } } What I’m Not Talking About
  5. fun conditionalExpressions(a: Int, b: Int) = if (a > b)

    a else b What I’m Not Talking About
  6. fun typeCasting(any: Any) { when (any) { is String ->

    any.length is Map<*, *> -> any.entries is Int -> any + 16 } } What I’m Not Talking About
  7. Goodbye NPE if (nullDoesntExist != null) { } Kotlin greatly

    reduces the chance that you will inadvertently cause a NullPointerException in production. engel.dev
  8. if (nullDoesntExist != null) { // Why check null if

    it does // not exist in Kotlin? } Why Still Check For Null?
  9. package kotlin public open class KotlinNullPointerException : java.lang.NullPointerException { public

    constructor() { /* compiled code */ } public constructor(message: kotlin.String?) { /* compiled code */ } } Because NPEs Still Exist.
  10. val stringWithNothing: String? = null if (stringWithNothing != null) Which

    Do You Prefer? val stringWithNothing: String = “” stringWithNothing.isNotEmpty()
  11. val intWithNothing: Int? = null if (intWithNothing != null) Which

    Do You Prefer? val intWithNothing: Int = -1 if (intWithNothing != -1)
  12. val intWithNothing: Int? = null if (intWithNothing != null) Which

    Do You Prefer? val intWithNothing: Int = 0 if (intWithNothing != -1)
  13. val possiblyNull: String? = null val definitelyNotNull = possiblyNull?.let {

    println("Cool this wasn't null.") } ?: { println("This used to be null.") "" }()
  14. val possiblyNull: String? = null val definitelyNotNull = possiblyNull?.let {

    println("Cool this wasn't null.") } ?: { println("This used to be null.") "" }.invoke()
  15. Working With The NullPointerException data class LocalUser( val apiKey: String?

    = null, val username: String? = null, val userId: Long? = null )
  16. data class LocalUser( val apiKey: String? = null, val username:

    String? = null, val userId: Long? = null ) data class AuthenticationRecord( val apiKey: String, val username: String, val userId: Long )
  17. data class AuthenticationRecord( val apiKey: String, val username: String, val

    userId: Long ) val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey = localUser.apiKey, username = localUser.username, userId = localUser.userId )
  18. data class AuthenticationRecord( val apiKey: String, val username: String, val

    userId: Long ) val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey = localUser.apiKey, username = localUser.username, userId = localUser.userId ) Required: String, Found String?
  19. data class AuthenticationRecord( val apiKey: String, val username: String, val

    userId: Long ) val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey = localUser.apiKey, username = localUser.username, userId = localUser.userId ) Required: String, Found String? Required: String, Found String?
  20. data class AuthenticationRecord( val apiKey: String, val username: String, val

    userId: Long ) val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey = localUser.apiKey, username = localUser.username, userId = localUser.userId ) Required: String, Found String? Required: String, Found String? Required: String, Found String?
  21. val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey =

    localUser.apiKey ?: "", username = localUser.username ?: "", userId = localUser.userId ?: -1 ) val authRecord = AuthenticationRecord( apiKey = localUser.apiKey, username = localUser.username, userId = localUser.userId ) Required: String, Found String? Required: String, Found String? Required: String, Found String?
  22. val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey =

    localUser.apiKey!!, username = localUser.username!!, userId = localUser.userId!! ) val localUser = LocalUser() val authRecord = AuthenticationRecord( apiKey = localUser.apiKey ?: "", username = localUser.username ?: "", userId = localUser.userId ?: -1 )
  23. apiKey = localUser.apiKey!!, username = localUser.username!!, userId = localUser.userId!! )

    val authRecord = try { AuthenticationRecord( apiKey = localUser.apiKey!!, username = localUser.username!!, userId = localUser.userId!! ) } catch (exception: KotlinNullPointerException) { // Provide an error to the user because they // aren't authenticated. }
  24. // Provide an error to the user because they //

    aren't authenticated. } val authRecord = if ( localUser.apiKey != null && localUser.username != null && localUser.userId != null) { AuthenticationRecord( localUser.apiKey, localUser.username, localUser.userId ) } else { // Provide an error to the user because they // aren't authenticated. }
  25. } else { // Provide an error to the user

    because they // aren't authenticated. } val authRecord = try { AuthenticationRecord( apiKey = localUser.apiKey!!, username = localUser.username!!, userId = localUser.userId!! ) } catch (exception: KotlinNullPointerException) { // Provide an error to the user because they // aren't authenticated. }
  26. Data Classes data class Cat( val name: String = "Tina",

    val age: Int = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // true tina.hashCode() // some number val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) // older Sometimes classes exist for the sole purpose of containing data. This is where data classes shine. engel.dev
  27. class Cat( val name: String = "Tina", val age: Int

    = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // true tina.hashCode() // some number val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) // older Normal Boring Class.
  28. class Cat( val name: String = "Tina", val age: Int

    = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // false tina.hashCode() // some number val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) // older Normal Boring Class.
  29. class Cat( val name: String = "Tina", val age: Int

    = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // false tina.hashCode() // some number, but calculated differently val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) // older Normal Boring Class.
  30. class Cat( val name: String = "Tina", val age: Int

    = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // false tina.hashCode() // some number, but calculated differently val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) // older Normal Boring Class.
  31. class Cat( val name: String = "Tina", val age: Int

    = 4 ) val tina = Cat() tina.name // Tina tina.age // 4 tina.equals(Cat(name = "Tina")) // false tina.hashCode() // some number, but calculated differently val (name, age) = tina val tinaIsOlder = tina.copy(age = 5) Normal Boring Class.
  32. class Cat( val name: String = "Tina", val age: Int

    = 4, currentlyHungry: Boolean ) Normal Boring Class.
  33. data class Cat( val name: String = "Tina", val age:

    Int = 4, currentlyHungry: Boolean ) Exciting Data Class.
  34. data class Cat( val name: String = "Tina", val age:

    Int = 4, currentlyHungry: Boolean ) " Exciting Data Class.
  35. data class Cat( val name: String = "Tina", val age:

    Int = 4, currentlyHungry: Boolean ) " Exciting Data Class. Data class primary constructor must have only property (val/var) parameters
  36. data class Cat( val name: String = "Tina", val age:

    Int = 4 ) val shouldBeConsistent = tina.hashCode() Using Data Classes.
  37. data class Cat( val name: String = "Tina", val age:

    Int = 4 ) val shouldBeTrue = tina.equals(Cat(name = “Tina")) val shouldBeConsistent = tina.hashCode() Using Data Classes.
  38. data class Cat( val name: String = "Tina", val age:

    Int = 4 ) val tinaIsOlder = tina.copy(age = 5) val shouldBeTrue = tina.equals(Cat(name = “Tina")) val shouldBeConsistent = tina.hashCode() Using Data Classes.
  39. Sealed Classes sealed class DealCustomFieldValue( val fieldId: Long?, val value:

    String?, val title: String?, val order: Long?, val type: String?, val fieldValueId: Long? ) { data class StandardFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) data class NumberFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) data class CurrencyFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long?, val fieldCurrency: String?, val currencyPosition: String? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) } Used for representing restricted class hierarchies. They are like an extension on enum classes where sub- classes can have multiple instances available at a time. engel.dev
  40. sealed class DealCustomFieldValue( val fieldId: Long?, val value: String?, val

    title: String?, val order: Long?, val type: String?, val fieldValueId: Long? ) { data class StandardFieldValue(
  41. ) { data class StandardFieldValue( private val _fieldId: Long?, private

    val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long? ) : DealCustomFieldValue( _fieldId,
  42. private val _type: String?, private val _fieldValueId: Long? ) :

    DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId )
  43. data class CurrencyFieldValue( private val _fieldId: Long?, private val _value:

    String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long?, val fieldCurrency: String?, val currencyPosition: String? ) : DealCustomFieldValue(
  44. sealed class DealCustomFieldValue( val fieldId: Long?, val value: String?, val

    title: String?, val order: Long?, val type: String?, val fieldValueId: Long? ) { data class StandardFieldValue(
  45. sealed class DealCustomFieldValue { abstract val fieldId: Long? abstract val

    value: String? abstract val title: String? abstract val order: Long? abstract val type: String? abstract val fieldValueId: Long? data class StandardFieldValue( override val fieldId: Long?,
  46. data class StandardFieldValue( override val fieldId: Long?, override val value:

    String?, override val title: String?, override val order: Long?, override val type: String?, override val fieldValueId: Long? ) : DealCustomFieldValue() data class NumberFieldValue( override val fieldId: Long?,
  47. data class CurrencyFieldValue( override val fieldId: Long?, override val value:

    String?, override val title: String?, override val order: Long?, override val type: String?, override val fieldValueId: Long?, val fieldCurrency: String?, val currencyPosition: String? ) : DealCustomFieldValue()
  48. sealed class DealCustomFieldValue( val fieldId: Long?, val value: String?, val

    title: String?, val order: Long?, val type: String?, val fieldValueId: Long? ) { data class StandardFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) data class NumberFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) data class CurrencyFieldValue( private val _fieldId: Long?, private val _value: String?, private val _title: String?, private val _order: Long?, private val _type: String?, private val _fieldValueId: Long?, val fieldCurrency: String?, val currencyPosition: String? ) : DealCustomFieldValue( _fieldId, _value, _title, _order, _type, _fieldValueId ) } sealed class DealCustomFieldValue { abstract val fieldId: Long? abstract val value: String? abstract val title: String? abstract val order: Long? abstract val type: String? abstract val fieldValueId: Long? data class StandardFieldValue( override val fieldId: Long?, override val value: String?, override val title: String?, override val order: Long?, override val type: String?, override val fieldValueId: Long? ) : DealCustomFieldValue() data class NumberFieldValue( override val fieldId: Long?, override val value: String?, override val title: String?, override val order: Long?, override val type: String?, override val fieldValueId: Long? ) : DealCustomFieldValue() data class CurrencyFieldValue( override val fieldId: Long?, override val value: String?, override val title: String?, override val order: Long?, override val type: String?, override val fieldValueId: Long?, val fieldCurrency: String?, val currencyPosition: String? ) : DealCustomFieldValue() }
  49. fun mapCustomFieldResponse(field: DealCustomFieldValue): StandardFieldData { return field.run { when (this)

    { is DealCustomFieldValue.CurrencyFieldValue -> { createStandardField( title = title ?: "", body = currencyUtil.serverValueToDisplayValue( value?.toDouble(), currencyPosition, fieldCurrency ), leftIcon = R.drawable.ic_short_text ) } is DealCustomFieldValue.NumberFieldValue -> createStandardField( title = title ?: "", body = value.toDoubleFormatTwoDecimalPlaces(), leftIcon = R.drawable.ic_short_text ) is DealCustomFieldValue.StandardFieldValue -> createStandardField( title = title ?: “", body = value ?: “", leftIcon = R.drawable.ic_short_text ) } } }
  50. when (this) { is DealCustomFieldValue.CurrencyFieldValue -> {} is DealCustomFieldValue.NumberFieldValue ->

    {} is DealCustomFieldValue.StandardFieldValue -> {} else -> { /* not necessary */ } }
  51. when (this) { is DealCustomFieldValue.CurrencyFieldValue -> {} is DealCustomFieldValue.NumberFieldValue ->

    { createStandardField( title = title ?: "", body = value.toDoubleFormatTwoDecimalPlaces(), leftIcon = R.drawable.ic_short_text ) } is DealCustomFieldValue.StandardFieldValue -> {} }
  52. when (this) { is DealCustomFieldValue.CurrencyFieldValue -> {} is DealCustomFieldValue.NumberFieldValue ->

    { createStandardField( title = title ?: "", body = value.toDoubleFormatTwoDecimalPlaces(), leftIcon = R.drawable.ic_short_text ) } is DealCustomFieldValue.StandardFieldValue -> { createStandardField( title = title ?: “", body = value ?: “", leftIcon = R.drawable.ic_short_text ) } }
  53. when (this) { is DealCustomFieldValue.CurrencyFieldValue -> { createStandardField( title =

    title ?: "", body = currencyUtil.serverValueToDisplayValue( value?.toDouble(), currencyPosition, fieldCurrency ), leftIcon = R.drawable.ic_short_text ) } is DealCustomFieldValue.NumberFieldValue -> { createStandardField( title = title ?: "", body = value.toDoubleFormatTwoDecimalPlaces(), leftIcon = R.drawable.ic_short_text ) }
  54. Collections.kt val cats = listOf<Cat>() cats.map { it.age } //

    List of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Cats grouped by their age. cats.random() // Random Cat from the list. The Collections framework that ships with Kotlin makes working with collections a breeze. engel.dev
  55. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  56. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  57. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  58. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  59. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  60. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  61. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  62. val cats = listOf<Cat>() cats.map { it.age } // List

    of Ages. cats.filter { it.age > 4 } // List of Cats older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. Collections.kt Abbreviated.
  63. cats.filter { it.age > 4 } // List of Cats

    older than 4. cats.last { it.name == "Tina" } // Last Cat named Tina. cats.slice(5..10) // Cats within the range of 5 to 10. cats.takeLast(15) // The last 15 Cats in the list. cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. cats .filter { it.age > 4 } .random() // Random Cat, older than 4.
  64. // Random Cat, from the last 20 elements, older than

    4. cats .filter { it.age > 4 } .takeLast(20) .random() cats.groupBy { it.age } // Group Cats by their age. cats.random() // Random Cat from the list. cats .filter { it.age > 4 } .random() // Random Cat, older than 4.
  65. // Random Cat, from the last 20 elements, older than

    4. cats .filter { it.age > 4 } .takeLast(20) .random() // Random Cat, older than 4, from the last 20 elements. cats .takeLast(20) .filter { it.age > 4 } .random()
  66. // Random Cat, from the last 20 elements, older than

    4. cats .filter { it.age > 4 } .takeLast(20) .random() cats .takeLast(20) // New list of 20 cats. .filter { it.age > 4 } // New list containing only cats older than 4. .random()
  67. .takeLast(20) // New list of 20 cats. .filter { it.age

    > 4 } // New list containing only cats older than 4. .random() cats.reversed().asSequence() .take(20) .filter { it.age > 4 } .toList() .random()
  68. .takeLast(20) // New list of 20 cats. .filter { it.age

    > 4 } // New list containing only cats older than 4. .random() cats.reversed().asSequence() .take(20) .filter { it.age > 4 } .toList() .random()
  69. .takeLast(20) // New list of 20 cats. .filter { it.age

    > 4 } // New list containing only cats older than 4. .random() cats.reversed().asSequence() .take(20) .filter { it.age > 4 } .toList() .random()
  70. .takeLast(20) // New list of 20 cats. .filter { it.age

    > 4 } // New list containing only cats older than 4. .random() cats.reversed().asSequence() .take(20) .filter { it.age > 4 } .toList() .random()
  71. cats.reversed().asSequence() .take(20) .filter { it.age > 4 } .toList() .random()

    sealed class FieldValue<T> { abstract val value: T data class NumericField(override val value: Long) : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() }
  72. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  73. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  74. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  75. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  76. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  77. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  78. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  79. abstract val value: T data class NumericField(override val value: Long)

    : FieldValue<Long>() data class MoneyField(override val value: Double) : FieldValue<Double>() data class TextField(override val value: String) : FieldValue<String>() data class MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues }
  80. data class TextField(override val value: String) : FieldValue<String>() data class

    MultiSelect<T>(override val value: List<T>) : FieldValue<List<T>>() } val fieldValues = listOf<FieldValue>() val fieldGroup = mutableMapOf<KClass, MutableList<FieldValue>>() for (fieldValue in fieldValues) { val fieldGroupValues = fieldGroup[fieldValue::class] ?: mutableListOf() fieldGroupValues.add(fieldValue) fieldGroup[fieldValue::class] = fieldGroupValues } val fieldValues = listOf<FieldValue>() val fieldGroup = fieldValues.groupBy { it::class }
  81. Extensions editText.setTextIfChanged(“blah") view.verticalPositionInParent moshi.toJson(EmailBody("subject", “body")) activeCampaignAnalytics.markComplete() intent.conversationUri In Java extending

    functionality on concrete classes require static Util classes. Kotlin allows us to add functionality to concrete classes through extensions. engel.dev
  82. fun TextView.setTextIfChanged(text: CharSequence) { if (this.text.toString() != text) { setTextKeepState(text)

    } } editText.setTextIfChanged(“blah”) view.verticalPositionInParent moshi.toJson(EmailBody("subject", “body")) activeCampaignAnalytics.markComplete() intent.conversationUri
  83. editText.setTextIfChanged(“blah”) val View.verticalPositionInParent: Int get() { val offsetViewBounds = Rect()

    parentAsViewGroup .offsetDescendantRectToMyCoords( this, offsetViewBounds ) return offsetViewBounds.top } view.verticalPositionInParent moshi.toJson(EmailBody("subject", “body")) activeCampaignAnalytics.markComplete()
  84. .offsetDescendantRectToMyCoords( this, offsetViewBounds ) return offsetViewBounds.top } view.verticalPositionInParent inline fun

    <reified T> Moshi.toJson(value: T): String { val adapter: JsonAdapter<T> = adapter(T::class.java) return adapter.toJson(value) } moshi.toJson(EmailBody("subject", “body”)) activeCampaignAnalytics.markComplete() intent.conversationUri
  85. view.verticalPositionInParent inline fun <reified T> Moshi.toJson(value: T): String { val

    adapter: JsonAdapter<T> = adapter(T::class.java) return adapter.toJson(value) } moshi.toJson(EmailBody("subject", “body”)) fun ActiveCampaignAnalytics.markComplete() { sendEvent(completeConversationEvent) } activeCampaignAnalytics.markComplete() intent.conversationUri
  86. view.verticalPositionInParent inline fun <reified T> Moshi.toJson(value: T): String { val

    adapter: JsonAdapter<T> = adapter(T::class.java) return adapter.toJson(value) } moshi.toJson(EmailBody("subject", “body”)) fun ActiveCampaignAnalytics.markComplete() { sendEvent(completeConversationEvent) } activeCampaignAnalytics.markComplete() private val Intent.conversationUri: String get() = getStringExtra(EXTRA_CONVERSATION_URI) intent.conversationUri
  87. Class Delegation abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(),

    KeyboardHandler by KeyboardHandlerReal() { Delegation allows for code reuse in a manner that prefers composition over inheritance. engel.dev
  88. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() { override fun addDisposable(disposable: Disposable) {} override fun addDisposables(vararg disposables: Disposable) {} override fun clearDisposables() {} }
  89. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() { override fun addDisposable(disposable: Disposable) { compositeDisposable.add(disposable) } override fun addDisposables(vararg disposables: Disposable) { compositeDisposable.addAll(*disposables) } override fun clearDisposables() { compositeDisposable.clear() } }
  90. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() {} class DisposableHandlerReal : DisposableHandler { private val compositeDisposable = CompositeDisposable() override fun addDisposable(disposable: Disposable) { compositeDisposable.add(disposable) } override fun addDisposables(vararg disposables: Disposable) { compositeDisposable.addAll(*disposables) } override fun clearDisposables() { compositeDisposable.clear() } }
  91. abstract class AbstractViewModel : ViewModel(), DisposableHandler by DisposableHandlerReal() class DisposableHandlerReal

    : DisposableHandler { private val compositeDisposable = CompositeDisposable() override fun addDisposable(disposable: Disposable) { compositeDisposable.add(disposable) } override fun addDisposables(vararg disposables: Disposable) { compositeDisposable.addAll(*disposables) } override fun clearDisposables() { compositeDisposable.clear() } }
  92. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() { override fun showKeyboard(context: Context) {} override fun dismissKeyboard(context: Context, view: View) {} }
  93. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() { override fun showKeyboard(context: Context) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.toggleSoftInput( InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY ) } override fun dismissKeyboard(context: Context, view: View) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) } }
  94. abstract class AbstractActivity : AppCompatActivity(), DisposableHandler by DisposableHandlerReal(), KeyboardHandler by

    KeyboardHandlerReal() {} class KeyboardHandlerReal : KeyboardHandler { override fun showKeyboard(context: Context) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.toggleSoftInput( InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY ) } override fun dismissKeyboard(context: Context, view: View) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) } }
  95. class AbstractFragment : Fragment(), KeyboardHandler by KeyboardHandlerReal() {} class KeyboardHandlerReal

    : KeyboardHandler { override fun showKeyboard(context: Context) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.toggleSoftInput( InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY ) } override fun dismissKeyboard(context: Context, view: View) { val inputMethodManager = context .getSystemService(Context.INPUT_METHOD_SERVICE) inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) } }
  96. class Example { var property: String by Delegate() } class

    Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) }
  97. class Example { var property: String by Delegate() } class

    Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } } class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) }
  98. val lazyInitialization by lazy { """ This String Is Created

    When First Needed Then Cached And Reused In The Future """.trimIndent() } operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } }
  99. """.trimIndent() } /* * Copyright 2010-2018 JetBrains s.r.o. Use of

    this source code is governed by the Apache 2.0 license * that can be found in the license/LICENSE.txt file. */ @file:kotlin.jvm.JvmName("LazyKt") @file:kotlin.jvm.JvmMultifileClass package kotlin /** * Creates a new instance of the [Lazy] that uses the specified initialization function [initializer] * and the default thread-safety mode [LazyThreadSafetyMode.SYNCHRONIZED]. * * If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access. * * Note that the returned instance uses itself to synchronize on. Do not synchronize from external code on
  100. * If the initialization of a value throws an exception,

    it will attempt to reinitialize the value at next access. * * Note that the returned instance uses itself to synchronize on. Do not synchronize from external code on * the returned instance as it may cause accidental deadlock. Also this behavior can be changed in the future. */ public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) /** * Creates a new instance of the [Lazy] that uses the specified initialization function [initializer] * and thread-safety [mode]. * * If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access. * * Note that when the [LazyThreadSafetyMode.SYNCHRONIZED] mode is specified the returned instance uses itself * to synchronize on. Do not synchronize from external code on the returned instance as it may cause accidental deadlock.
  101. */ public actual fun <T> lazy(lock: Any?, initializer: () ->

    T): Lazy<T> = SynchronizedLazyImpl(initializer, lock) private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // final field is required to enable safe publication of constructed instance private val lock = lock ?: this override val value: T get() { val _v1 = _value if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } return synchronized(lock) { val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) {
  102. return newValue } } @Suppress("UNCHECKED_CAST") return _value as T }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) companion object { private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater( SafePublicationLazyImpl::class.java, Any::class.java, "_value" ) } }
  103. Higher Order Functions fun randomerNumber(randomNumber: () -> Long): () ->

    Long { return { randomNumber() * randomNumber() } } randomerNumber { Random.nextLong() }() Mostly used for passing a function into another function. However you can return a function from a function too. engel.dev
  104. val startTime = System.currentTimeMillis() for (i in 0..100000) { println("Cat:

    ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  105. val startTime = System.currentTimeMillis() for (i in 0..100000) { println("Cat:

    ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  106. val startTime = System.currentTimeMillis() for (i in 0..100000) { println("Cat:

    ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  107. val startTime = System.currentTimeMillis() for (i in 0..100000) { println("Cat:

    ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  108. val startTime = System.currentTimeMillis() for (i in 0..100000) { println("Cat:

    ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  109. fun benchmark(blockOfCode: () -> Unit): Long { val startTime =

    System.currentTimeMillis() blockOfCode.invoke() val endTime = System.currentTimeMillis() return endTime - startTime } println("Cat: ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  110. fun benchmark(blockOfCode: () -> Unit): Long { val startTime =

    System.currentTimeMillis() blockOfCode.invoke() val endTime = System.currentTimeMillis() return endTime - startTime } println("Cat: ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  111. fun benchmark(blockOfCode: () -> Unit): Long { val startTime =

    System.currentTimeMillis() blockOfCode.invoke() val endTime = System.currentTimeMillis() return endTime - startTime } println("Cat: ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  112. fun benchmark(blockOfCode: () -> Unit): Long { val startTime =

    System.currentTimeMillis() blockOfCode.invoke() val endTime = System.currentTimeMillis() return endTime - startTime } println("Cat: ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  113. fun benchmark(blockOfCode: () -> Unit): Long { val startTime =

    System.currentTimeMillis() blockOfCode() val endTime = System.currentTimeMillis() return endTime - startTime } println("Cat: ${Cat()}") } val endTime = System.currentTimeMillis() val runtime = endTime - startTime
  114. benchmark({ for (i in 0..100000) { println("Cat: ${Cat()}") } }

    val endTime = System.currentTimeMi return endTime - startTime }
  115. benchmark { for (i in 0..100000) { println("Cat: ${Cat()}") }

    } val endTime = System.currentTimeMi return endTime - startTime }
  116. interface MutableListViewModel<T> : ListViewModel<T> { var dataSetChanges: ((change: DataSetChange) ->

    Unit)? fun addItem(item: T) fun moveItem(item: T, newPosition: Int) fun removeItem(item: T) }
  117. interface MutableListViewModel<T> : ListViewModel<T> { var dataSetChanges: ((change: DataSetChange) ->

    Unit)? fun addItem(item: T) fun moveItem(item: T, newPosition: Int) fun removeItem(item: T) }
  118. class MutableListViewModelDelegate<T>( initialItems: List<T> = emptyList() ) : MutableListViewModel<T> {

    private val items = mutableListOf<T>() .apply { addAll(initialItems) } override var dataSetChanges: ((change: DataSetChange) -> Unit)? = null override val itemCount: Int = items.size override fun get(position: Int): T = items[position] override fun addItem(item: T) { items.add(item) dataSetChanges?.invoke(DataSetChange.Inserted(items.size, 1)) } override fun moveItem(item: T, newPosition: Int) { val from = items.indexOf(item) items.removeAt(from) items.add(newPosition, item) dataSetChanges?.invoke(DataSetChange.Moved(from, newPosition)) } override fun removeItem(item: T) { val index = items.indexOf(item) items.removeAt(index) dataSetChanges?.invoke(DataSetChange.Removed(index, 1)) } }
  119. class MutableListViewModelDelegate<T>( initialItems: List<T> = emptyList() ) : MutableListViewModel<T> {

    private val items = mutableListOf<T>() .apply { addAll(initialItems) } override var dataSetChanges: ((change: DataSetChange) -> Unit)? = null override val itemCount: Int = items.size override fun get(position: Int): T = items[position] override fun addItem(item: T) { items.add(item) dataSetChanges?.invoke(DataSetChange.Inserted(items.size, 1)) } override fun moveItem(item: T, newPosition: Int) { val from = items.indexOf(item) items.removeAt(from) items.add(newPosition, item) dataSetChanges?.invoke(DataSetChange.Moved(from, newPosition)) } override fun removeItem(item: T) { val index = items.indexOf(item) items.removeAt(index) dataSetChanges?.invoke(DataSetChange.Removed(index, 1)) } }
  120. class MutableListViewModelDelegate<T>( initialItems: List<T> = emptyList() ) : MutableListViewModel<T> {

    override var dataSetChanges: ((change: DataSetChange) -> Unit)? = null override fun addItem(item: T) { dataSetChanges?.invoke(DataSetChange.Inserted(items.size, 1)) } override fun moveItem(item: T, newPosition: Int) { dataSetChanges?.invoke(DataSetChange.Moved(from, newPosition)) } override fun removeItem(item: T) { dataSetChanges?.invoke(DataSetChange.Removed(index, 1)) } }
  121. sealed class DataSetChange { object All : DataSetChange() data class

    Changed(val positionStart: Int, val itemCount: Int = 1) : DataSetChange() data class Inserted(val positionStart: Int, val itemCount: Int = 1) : DataSetChange() data class Moved(val from: Int, val to: Int) : DataSetChange() data class Removed(val positionStart: Int, val itemCount: Int = 1) : DataSetChange() }
  122. abstract class ListViewModelAdapter<T, VH : ListViewModelAdapter.ViewHolder<T>> : RecyclerView.Adapter<VH>() { protected

    abstract val listViewModel: ListViewModel<T> val dataSetChanges: (change: DataSetChange) -> Unit = { when (it) { is DataSetChange.Inserted -> notifyItemRangeInserted(it.positionStart, it.itemCount) is DataSetChange.Moved -> notifyItemMoved(it.from, it.to) is DataSetChange.Removed -> notifyItemRangeRemoved(it.positionStart, it.itemCount) is DataSetChange.Changed -> notifyItemRangeChanged(it.positionStart, it.itemCount) is DataSetChange.All -> notifyDataSetChanged() } } override fun getItemCount(): Int = listViewModel.itemCount override fun onBindViewHolder(holder: VH, position: Int) { holder.bind(listViewModel[position]) } }
  123. abstract class ListViewModelAdapter<T, VH : ListViewModelAdapter.ViewHolder<T>> : RecyclerView.Adapter<VH>() { protected

    abstract val listViewModel: ListViewModel<T> val dataSetChanges: (change: DataSetChange) -> Unit = { when (it) { is DataSetChange.Inserted -> notifyItemRangeInserted(it.positionStart, it.itemCount) is DataSetChange.Moved -> notifyItemMoved(it.from, it.to) is DataSetChange.Removed -> notifyItemRangeRemoved(it.positionStart, it.itemCount) is DataSetChange.Changed -> notifyItemRangeChanged(it.positionStart, it.itemCount) is DataSetChange.All -> notifyDataSetChanged() } } override fun getItemCount(): Int = listViewModel.itemCount override fun onBindViewHolder(holder: VH, position: Int) { holder.bind(listViewModel[position]) } }
  124. abstract class ListViewModelAdapter<T, VH : ListViewModelAdapter.ViewHolder<T>> : RecyclerView.Adapter<VH>() { val

    dataSetChanges: (change: DataSetChange) -> Unit = { when (it) { is DataSetChange.Inserted -> notifyItemRangeInserted(it.positionStart, it.itemCount) is DataSetChange.Moved -> notifyItemMoved(it.from, it.to) is DataSetChange.Removed -> notifyItemRangeRemoved(it.positionStart, it.itemCount) is DataSetChange.Changed -> notifyItemRangeChanged(it.positionStart, it.itemCount) is DataSetChange.All -> notifyDataSetChanged() } } }
  125. Unit Testing If you don’t want to ship Kotlin to

    production, then start with your CI/CD process. class LoginViewModelTest : ViewModelTest() { @MockK private lateinit var authenticationRepository: Authenti @MockK private lateinit var stringLoader: StringLoader @MockK private lateinit var telemetry: Telemetry @RelaxedMockK private lateinit var analytics: ActiveCampaignAnalytics private lateinit var subject: LoginViewModel private val viewStateAssertion = ViewStateAssertion<Log private val accountTextChanges by lazy { EventObservabl private val usernameTextChanges by lazy { EventObservab private val passwordTextChanges by lazy { EventObservab private val domainTextChanges by lazy { EventObservable private val loginClicks by lazy { EventObservable<Any>( private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString( private val invalidRequestErrorMessage by lazy { random private val noPermissionsErrorMessage by lazy { randomS engel.dev
  126. class LoginViewModelTest : ViewModelTest() { @MockK private lateinit var authenticationRepository:

    AuthenticationRepository @MockK private lateinit var stringLoader: StringLoader @MockK private lateinit var telemetry: Telemetry @RelaxedMockK private lateinit var analytics: ActiveCampaignAnalytics private lateinit var subject: LoginViewModel private val viewStateAssertion = ViewStateAssertion<LoginViewModel.State>() private val accountTextChanges by lazy { EventObservable<CharSequence>() } private val usernameTextChanges by lazy { EventObservable<CharSequence>() } private val passwordTextChanges by lazy { EventObservable<CharSequence>() } private val domainTextChanges by lazy { EventObservable<CharSequence>() } private val loginClicks by lazy { EventObservable<Any>() } private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString() } private val invalidRequestErrorMessage by lazy { randomString() } private val noPermissionsErrorMessage by lazy { randomString() } private val accountExpiredErrorMessage by lazy { randomString() } private val trace by lazy { mockk<Trace>() }
  127. ss LoginViewModelTest : ViewModelTest() { @MockK private lateinit var authenticationRepository:

    AuthenticationRepository @MockK private lateinit var stringLoader: StringLoader @MockK private lateinit var telemetry: Telemetry @RelaxedMockK private lateinit var analytics: ActiveCampaignAnalytics private lateinit var subject: LoginViewModel private val viewStateAssertion = ViewStateAssertion<LoginViewModel.State>() private val accountTextChanges by lazy { EventObservable<CharSequence>() } private val usernameTextChanges by lazy { EventObservable<CharSequence>() } private val passwordTextChanges by lazy { EventObservable<CharSequence>() } private val domainTextChanges by lazy { EventObservable<CharSequence>() } private val loginClicks by lazy { EventObservable<Any>() } private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString() } private val invalidRequestErrorMessage by lazy { randomString() } private val noPermissionsErrorMessage by lazy { randomString() } private val accountExpiredErrorMessage by lazy { randomString() } private val trace by lazy { mockk<Trace>() } MockKAnnotations.init(this)
  128. private val viewStateAssertion = ViewStateAssertion<LoginViewModel.State>() private val accountTextChanges by lazy

    { EventObservable<CharSequence>() } private val usernameTextChanges by lazy { EventObservable<CharSequence>() } private val passwordTextChanges by lazy { EventObservable<CharSequence>() } private val domainTextChanges by lazy { EventObservable<CharSequence>() } private val loginClicks by lazy { EventObservable<Any>() } private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString() } private val invalidRequestErrorMessage by lazy { randomString() } private val noPermissionsErrorMessage by lazy { randomString() } private val accountExpiredErrorMessage by lazy { randomString() } private val trace by lazy { mockk<Trace>() } private fun prepareTelemetry() { every { telemetry.startTrace(any()) } returns trace every { trace.putAttribute(any(), any()) } just Runs every { telemetry.endTrace(trace) } just Runs }
  129. private val viewStateAssertion = ViewStateAssertion<LoginViewModel.State>() private val accountTextChanges by lazy

    { EventObservable<CharSequence>() } private val usernameTextChanges by lazy { EventObservable<CharSequence>() } private val passwordTextChanges by lazy { EventObservable<CharSequence>() } private val domainTextChanges by lazy { EventObservable<CharSequence>() } private val loginClicks by lazy { EventObservable<Any>() } private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString() } private val invalidRequestErrorMessage by lazy { randomString() } private val noPermissionsErrorMessage by lazy { randomString() } private val accountExpiredErrorMessage by lazy { randomString() } private val trace by lazy { mockk<Trace>() } private fun prepareTelemetry() { every { telemetry.startTrace(any()) } returns trace every { trace.putAttribute(any(), any()) } just Runs every { telemetry.endTrace(trace) } just Runs }
  130. private val viewStateAssertion = ViewStateAssertion<LoginViewModel.State>() private val accountTextChanges by lazy

    { EventObservable<CharSequence>() } private val usernameTextChanges by lazy { EventObservable<CharSequence>() } private val passwordTextChanges by lazy { EventObservable<CharSequence>() } private val domainTextChanges by lazy { EventObservable<CharSequence>() } private val loginClicks by lazy { EventObservable<Any>() } private val textChange by lazy { randomString() } private val errorMessage by lazy { randomString() } private val genericErrorMessage by lazy { randomString() } private val invalidRequestErrorMessage by lazy { randomString() } private val noPermissionsErrorMessage by lazy { randomString() } private val accountExpiredErrorMessage by lazy { randomString() } private val trace by lazy { mockk<Trace>() } private fun prepareTelemetry() { every { telemetry.startTrace(any()) } returns trace every { trace.putAttribute(any(), any()) } just Runs every { telemetry.endTrace(trace) } just Runs }
  131. = ViewStateAssertion<LoginViewModel.State>() by lazy { EventObservable<CharSequence>() } s by lazy

    { EventObservable<CharSequence>() } s by lazy { EventObservable<CharSequence>() } by lazy { EventObservable<CharSequence>() } y { EventObservable<Any>() } { randomString() } zy { randomString() } e by lazy { randomString() } rMessage by lazy { randomString() } Message by lazy { randomString() } rMessage by lazy { randomString() } ckk<Trace>() } { ce(any()) } returns trace (any(), any()) } just Runs (trace) } just Runs /** * Generates a random string in the form of a [UUID]. */ fun randomString() = UUID.randomUUID().toString()
  132. @Test fun `initial state should have loginSuccessful set to false`()

    { viewStateAssertion.assertFirstViewState { assertThat(it.loginSuccessful).isFalse() } } @Test fun `initial state should have loginEnabled set to false`() { viewStateAssertion.assertFirstViewState { assertThat(it.loginEnabled).isFalse() } } @Test fun `initial state should have loginForm set to empty values`() { viewStateAssertion.assertFirstViewState { assertThat(it.loginForm).isEqualTo(LoginViewModel.LoginForm()) } }
  133. @Test fun `initial state should have loginSuccessful set to false`()

    { viewStateAssertion.assertFirstViewState { assertThat(it.loginSuccessful).isFalse() } } @Test fun `initial state should have loginEnabled set to false`() { viewStateAssertion.assertFirstViewState { assertThat(it.loginEnabled).isFalse() } } @Test fun `initial state should have loginForm set to empty values`() { viewStateAssertion.assertFirstViewState { assertThat(it.loginForm).isEqualTo(LoginViewModel.LoginForm()) } }
  134. statefulModel.state.removeObserver(currentObserver!!) } currentObserver = null } fun assertLastViewState(assertion: (actualState: S)

    -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.last()) } } fun assertFirstViewState(assertion: (actualState: S) -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.first()) } } fun assertViewStateSeries(assertion: (actualStates: List<S>) -> Unit) { assertion(viewStates) } }
  135. statefulModel.state.removeObserver(currentObserver!!) } currentObserver = null } fun assertLastViewState(assertion: (actualState: S)

    -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.last()) } } fun assertFirstViewState(assertion: (actualState: S) -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.first()) } } fun assertViewStateSeries(assertion: (actualStates: List<S>) -> Unit) { assertion(viewStates) } }
  136. class ViewStateAssertion<S : ViewState> { private val viewStates = mutableListOf<S>()

    private var currentObserver: Observer<S>? = null private lateinit var statefulModel: StatefulModel<S> val numberOfStates: Int get() = viewStates.size fun start(statefulModel: StatefulModel<S>) { this.statefulModel = statefulModel currentObserver = Observer { viewStates.add(it!!) } try { viewStates.clear() } catch (ex: Exception) { Assert.fail("Unable to complete ViewStateAssertion.start(). The followin } this.statefulModel.state.observeForever(currentObserver!!) }
  137. @Test fun `accountTextChanges should update the loginForm with new account

    value`() { prepareForFormInteractions() accountTextChanges.updateValue(textChange) viewStateAssertion.assertLastViewState { assertThat(it.loginForm.account).isEqualTo(textChange) } } @Test fun `usernameTextChanges should update the loginForm with new username value`() { prepareForFormInteractions() usernameTextChanges.updateValue(textChange) viewStateAssertion.assertLastViewState { assertThat(it.loginForm.username).isEqualTo(textChange) } }
  138. } this.statefulModel.state.observeForever(currentObserver!!) } fun end() { if (currentObserver != null)

    { statefulModel.state.removeObserver(currentObserver!!) } currentObserver = null } fun assertLastViewState(assertion: (actualState: S) -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.last()) } } fun assertFirstViewState(assertion: (actualState: S) -> Unit) { assertViewStateSeries { actualStates -> assertion(actualStates.first()) } }
  139. @Test fun `accountTextChanges should update the loginForm with new account

    value`() { prepareForFormInteractions() accountTextChanges.updateValue(textChange) viewStateAssertion.assertLastViewState { assertThat(it.loginForm.account).isEqualTo(textChange) } } @Test fun `usernameTextChanges should update the loginForm with new username value`() { prepareForFormInteractions() usernameTextChanges.updateValue(textChange) viewStateAssertion.assertLastViewState { assertThat(it.loginForm.username).isEqualTo(textChange) } }