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

Migrating a mature code base to Kotlin

Migrating a mature code base to Kotlin

In this presentation we will talk about the practicalities of slowly migrating a mature Android app to Kotlin.

The ASOS Android app is about 4 years old. During this time, a lot of legacy code has been accumulated, and many different technologies used.

We will present our approach of integrating Kotlin, the lessons we learned and a few Kotlin features we love and can no longer do without.

Savvas Dalkitsis

October 06, 2017
Tweet

More Decks by Savvas Dalkitsis

Other Decks in Technology

Transcript

  1. @geeky_android
    Migrating a mature
    codebase to Kotlin

    View Slide

  2. @geeky_android
    Mandatory who-am-i
    • Savvas Dalkitsis
    • Principal Software Engineer for Apps @

    View Slide

  3. @geeky_android
    Mandatory who-is-ASOS
    “Our mission is to become the world’s number-one
    online shopping destination for fashion-loving
    20-somethings.”

    View Slide

  4. @geeky_android
    Team structure
    Director of technology – Web and Apps
    Apps team
    Web team
    Platform lead Platform lead Principal Architect
    ADM
    ADM
    ADM
    BA
    BA
    BA
    Android team
    Team 1
    Dev
    Dev
    QA
    QA
    . . .
    . . .
    Team 2
    Dev
    Dev
    QA
    QA
    . . .
    . . .
    Team 3
    Dev
    Dev
    QA
    QA
    . . .
    . . .
    Principal Engineer
    iOS team
    Team 1
    Dev
    Dev
    QA
    QA
    . . .
    . . .
    Team 2
    Dev
    Dev
    QA
    QA
    . . .
    . . .
    Team 3
    Dev
    Dev
    QA
    QA
    . . .
    . . .

    View Slide

  5. @geeky_android
    A mature project
    17,927 files

    View Slide

  6. @geeky_android
    Mobile is important to us
    • 75%+ traffic from mobile (web and apps)
    • 60%+ revenue

    View Slide

  7. @geeky_android
    • Butterknife
    • RxJava2
    • OkHttp
    • Retrofit
    • Espresso
    • Kotlin?

    View Slide

  8. @geeky_android

    View Slide

  9. @geeky_android
    Why do we ?
    !

    View Slide

  10. @geeky_android
    class Model {
    private String name;
    private String surname;
    private int age;
    private int height;
    private Address address;
    private Country country;
    public String getName() {
    return name;
    }
    public String getSurname() {
    return surname;
    }
    public int getAge() {
    return age;
    }
    public int getHeight() {
    return height;
    }
    public Address getAddress() {
    return address;
    }

    View Slide

  11. @geeky_android
    if (height != model.height) return false;
    if (name != null ? !name.equals(model.name) : model.name != null) return false;
    if (surname != null ? !surname.equals(model.surname) : model.surname != null) return false;
    if (address != null ? !address.equals(model.address) : model.address != null) return false;
    return country != null ? country.equals(model.country) : model.country == null;
    }
    @Override
    public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (surname != null ? surname.hashCode() : 0);
    result = 31 * result + age;
    result = 31 * result + height;
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (country != null ? country.hashCode() : 0);
    return result;
    }
    @Override
    public String toString() {
    return "Model{" +
    "name='" + name + '\'' +
    ", surname='" + surname + '\'' +
    ", age=" + age +
    ", height=" + height +
    ", address=" + address +
    ", country=" + country +
    '}';
    }
    }

    View Slide

  12. @geeky_android
    data class Model(
    var name: String,
    var surname: String,
    var age: Int,
    var height: Int,
    var address: Address,
    var country: Country
    )

    View Slide

  13. @geeky_android
    class Model {
    private String name;
    private String surname;
    private int age;
    private int height;
    private Address address;
    private Country country;
    public String getName() {
    return name;
    }
    public String getSurname() {
    return surname;
    }
    public int getAge() {
    return age;
    }
    public int getHeight() {
    return height;
    }
    public Address getAddress() {
    return address;
    }
    public Country getCountry() {
    return country;
    }
    public void setName(String name) {
    this.name = name;
    }
    public void setSurname(String surname) {
    this.surname = surname;
    }
    public void setAge(int age) {
    this.age = age;
    }
    public void setHeight(int height) {
    this.height = height;
    }
    public void setAddress(Address address) {
    this.address = address;
    }
    public void setCountry(Country country) {
    this.country = country;
    }
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Model model = (Model) o;
    if (age != model.age) return false;
    if (height != model.height) return false;
    if (name != null ? !name.equals(model.name) : model.name != null) return false;
    if (surname != null ? !surname.equals(model.surname) : model.surname != null) return false;
    if (address != null ? !address.equals(model.address) : model.address != null) return false;
    return country != null ? country.equals(model.country) : model.country == null;
    }
    @Override
    public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (surname != null ? surname.hashCode() : 0);
    result = 31 * result + age;
    result = 31 * result + height;
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (country != null ? country.hashCode() : 0);
    return result;
    }
    @Override
    public String toString() {
    return "Model{" +
    "name='" + name + '\'' +
    ", surname='" + surname + '\'' +
    ", age=" + age +
    ", height=" + height +
    ", address=" + address +
    ", country=" + country +
    '}';
    }
    }

    View Slide

  14. @geeky_android
    class Model {
    private String name;
    private String surname;
    private int age;
    private int height;
    private Address address;
    private Country country;
    public Model(Builder builder) {
    name = builder.name;
    surname = builder.surname;
    age = builder.age;
    height = builder.height;
    address = builder.address;
    country = builder.country;
    }
    public String getName() {
    return name;
    }
    public String getSurname() {
    return surname;
    }
    public int getAge() {
    return age;
    }
    public int getHeight() {
    return height;
    }
    public Address getAddress() {
    return address;
    }
    public Country getCountry() {
    return country;
    }
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Model model = (Model) o;
    if (age != model.age) return false;
    if (height != model.height) return false;
    if (name != null ? !name.equals(model.name) : model.name != null) return false;
    if (surname != null ? !surname.equals(model.surname) : model.surname != null) return false;
    if (address != null ? !address.equals(model.address) : model.address != null) return false;
    return country != null ? country.equals(model.country) : model.country == null;
    }
    @Override
    public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (surname != null ? surname.hashCode() : 0);
    result = 31 * result + age;
    result = 31 * result + height;
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (country != null ? country.hashCode() : 0);
    return result;
    }
    @Override
    public String toString() {
    return "Model{" +
    "name='" + name + '\'' +
    ", surname='" + surname + '\'' +
    ", age=" + age +
    ", height=" + height +
    ", address=" + address +
    ", country=" + country +
    '}';
    }
    }

    View Slide

  15. @geeky_android
    class Model {
    private String name;
    private String surname;
    private int age;
    private int height;
    private Address address;
    private Country country;
    public Model(Builder builder) {
    name = builder.name;
    surname = builder.surname;
    age = builder.age;
    height = builder.height;
    address = builder.address;
    country = builder.country;
    }
    public String getName() {
    return name;
    }
    public String getSurname() {
    return surname;
    }
    public int getAge() {
    return age;
    }
    public int getHeight() {
    return height;
    }
    public Address getAddress() {
    return address;
    }
    public Country getCountry() {
    return country;
    }
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Model model = (Model) o;
    if (age != model.age) return false;
    if (height != model.height) return false;
    if (name != null ? !name.equals(model.name) : model.name != null) return false;
    if (surname != null ? !surname.equals(model.surname) : model.surname != null) return false;
    if (address != null ? !address.equals(model.address) : model.address != null) return false;
    return country != null ? country.equals(model.country) : model.country == null;
    }
    @Override
    public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (surname != null ? surname.hashCode() : 0);
    result = 31 * result + age;
    result = 31 * result + height;
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (country != null ? country.hashCode() : 0);
    return result;
    }
    @Override
    public String toString() {
    return "Model{" +
    "name='" + name + '\'' +
    ", surname='" + surname + '\'' +
    ", age=" + age +
    ", height=" + height +
    ", address=" + address +
    ", country=" + country +
    '}';
    }
    }
    public static class Builder {
    private String name;
    private String surname;
    private int age;
    private int height;
    private Address address;
    private Country country;
    public static Builder model() {
    return new Builder();
    }
    public Builder withName(String name) {
    this.name = name;
    return this;
    }
    public Builder withSurname(String surname) {
    this.surname = surname;
    return this;
    }
    public Builder withAge(int age) {
    this.age = age;
    return this;
    }
    public Builder withHeight(int height) {
    this.height = height;
    return this;
    }
    public Builder withAddress(Address address) {
    this.address = address;
    return this;
    }
    public Builder withCountry(Country country) {
    this.country = country;
    return this;
    }
    public Model build() {
    return new Model(this);
    }
    }

    View Slide

  16. @geeky_android
    data class Model(
    var name: String,
    var surname: String,
    var age: Int,
    var height: Int,
    var address: Address,
    var country: Country
    )

    View Slide

  17. @geeky_android
    data class Model(
    var name: String,
    var surname: String,
    var age: Int,
    var height: Int,
    var address: Address,
    var country: Country
    )
    data class Model(
    val name: String,
    val surname: String,
    val age: Int,
    val height: Int,
    val address: Address,
    val country: Country
    )

    View Slide

  18. @geeky_android
    How do I convince my ?
    "

    View Slide

  19. @geeky_android
    A small feature

    View Slide

  20. @geeky_android
    A small isolated feature

    View Slide

  21. @geeky_android
    ¯\_(ϑ)_/¯
    If things don’t work out

    View Slide

  22. @geeky_android
    Where do I begin?
    https://kotlinlang.org/docs/tutorials/koans.html

    View Slide

  23. @geeky_android
    But WHERE do I begin?

    View Slide

  24. @geeky_android

    View Slide

  25. @geeky_android
    Then what?

    View Slide

  26. @geeky_android
    . . .
    . . .

    View Slide

  27. @geeky_android
    . . .

    View Slide

  28. @geeky_android

    View Slide

  29. @geeky_android
    Cmd+Shift+A is your friend

    View Slide

  30. @geeky_android

    View Slide

  31. @geeky_android

    View Slide

  32. @geeky_android

    View Slide

  33. @geeky_android

    View Slide

  34. @geeky_android

    View Slide

  35. @geeky_android

    View Slide

  36. @geeky_android

    View Slide

  37. @geeky_android

    View Slide

  38. @geeky_android

    View Slide

  39. @geeky_android

    View Slide

  40. @geeky_android

    View Slide

  41. @geeky_android

    View Slide

  42. @geeky_android

    View Slide

  43. @geeky_android

    View Slide

  44. @geeky_android

    View Slide

  45. @geeky_android

    View Slide

  46. @geeky_android

    View Slide

  47. @geeky_android

    View Slide

  48. @geeky_android

    View Slide

  49. @geeky_android

    View Slide

  50. @geeky_android

    View Slide

  51. @geeky_android

    View Slide

  52. @geeky_android
    #

    View Slide

  53. @geeky_android

    View Slide

  54. @geeky_android

    View Slide

  55. @geeky_android
    class ReturnListActivity: ToolbarFragmentActivity() {
    companion object {
    @JvmStatic fun newIntent(activity: Activity)= Intent(activity, ReturnListActivity::class.java)
    }
    override fun getFragment() = ReturnListFragment()
    override fun getToolbarTitle(): String? = getString(R.string.my_returns_header)
    override fun getDisplayHomeAsUpEnabled(): Boolean = true
    }

    View Slide

  56. @geeky_android
    open class ReturnMethodPresenter : BasePresenter() {
    private lateinit var orderDetails: OrderDetails
    fun bindView(view: T, orderDetails: OrderDetails) {
    super.setView(view)
    this.orderDetails = orderDetails
    }
    fun onDropOffClicked() {
    orderDetails.deliveryDetails.address?.let {
    val customerBasicInfo = CustomerBasicInfo(it.firstName,
    it.lastName, it.telephoneMobile, it.emailAddress)
    val searchData = DropOffSearchData(it.postalCode, it.countryCode,
    orderDetails.currencyCode, customerBasicInfo)
    view.launchDropOffPointSearch(searchData)
    }
    }
    }

    View Slide

  57. View Slide

  58. View Slide

  59. @geeky_android
    How are we doing?

    View Slide

  60. @geeky_android
    Is it all ☁ and %?

    View Slide

  61. @geeky_android
    @PaperParcel
    data class SocialConnection(val isConnected: Boolean = false,
    val email: String? = null,
    val nickname: String? = null,
    var firstName: String? = null,
    var lastName: String? = null,
    val isLoggedIn: Boolean = false) : Parcelable {
    companion object {
    @JvmField val CREATOR = PaperParcelSocialConnection.CREATOR
    }
    override fun writeToParcel(dest: Parcel, flags: Int) {
    PaperParcelSocialConnection.writeToParcel(this, dest, flags)
    }
    override fun describeContents() = 0
    }

    View Slide

  62. @geeky_android

    View Slide

  63. @geeky_android

    View Slide

  64. @geeky_android

    View Slide

  65. @geeky_android

    View Slide

  66. @geeky_android
    • Build times
    • Switching branches

    View Slide

  67. @geeky_android
    Show me some % % % %!!!

    View Slide

  68. @geeky_android
    val age = retrievePersonAge() ?: 0
    private fun retrievePersonAge(): Int? = …
    val age = retrievePersonAge() ?: throw IllegalStateException("Can I haz age?")

    View Slide

  69. @geeky_android
    val savvas = Person(
    name = "Savvas",
    age = confidential(),
    address = ditto(),
    email = "[email protected]",
    country = uk()
    )

    View Slide

  70. @geeky_android
    data class Person(
    val name: String?,
    val age: Int,
    val email: String?,
    val address: Address?,
    val country: Country?
    )

    View Slide

  71. @geeky_android
    data class Person(
    val name: String?,
    val age: Int,
    val email: String?,
    val address: Address?,
    val country: Country?
    )
    data class Person(
    val name: String?,
    val age: Int,
    val email: String? = null,
    val address: Address?,
    val country: Country?
    )

    View Slide

  72. @geeky_android
    val savvas = Person(
    name = "Savvas",
    age = confidential(),
    address = ditto(),
    country = uk()
    )

    View Slide

  73. @geeky_android
    val savvas = Person(
    name = "Savvas",
    age = confidential(),
    address = ditto(),
    country = uk()
    )
    val olderSavvas = savvas.copy(age = 50)

    View Slide

  74. @geeky_android
    checkout().bagStockReservation?.outOfStockItems.orEmpty()
    .filter { (it.price?.discount?.value ?: 0.0) > 10 }

    View Slide

  75. @geeky_android
    fun main(args: Array) {
    people().orEmpty().filter { it.age > 18 }
    }
    private fun people(): List? = null

    View Slide

  76. @geeky_android
    fun main(args: Array) {
    println(people()
    .map { it.age }
    .filter { it > 18 }
    .average())
    println(people()
    .filter { it.age > 18 }
    .sumBy { it.age })
    println(people()
    .map { it.age }
    .filter { it > 18 }
    .reduce { total, current -> total + current })
    // prints:
    // 20
    // 40
    // 40
    }
    private fun people(): List = listOf(Person(age = 19), Person(age = 10), Person(age = 21))

    View Slide

  77. @geeky_android
    fun main(args: Array) {
    Intent().navigateWith(someContext())
    }
    fun Intent.navigateWith(context: Context) {
    if (context !is Activity) {
    this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }
    context.startActivity(this)
    }

    View Slide

  78. @geeky_android
    fun main(args: Array) {
    Intent().apply {
    action = "someAction"
    setClassName("my.awesome.package", ".MyAwesomeClass")
    }.navigateWith(someContext())
    }

    View Slide

  79. @geeky_android
    class Model
    interface ModelUseCase {
    fun announceModelUpdated()
    fun saveModel(model: Model)
    // many more methods ...
    }

    View Slide

  80. @geeky_android
    class Model
    interface ModelUseCase {
    fun announceModelUpdated()
    fun saveModel(model: Model)
    // many more methods ...
    }
    class DebounceModelUseCase(windowMillis: Long, val delegate: ModelUseCase): ModelUseCase {
    private val debouncer: BehaviorProcessor = BehaviorProcessor.create()
    init {
    debouncer.debounce(windowMillis, TimeUnit.MILLISECONDS)
    .subscribe { delegate.announceModelUpdated() }
    }
    override fun announceModelUpdated() {
    debouncer.onNext(Unit)
    }
    override fun saveModel(model: Model) {
    delegate.saveModel(model)
    }
    // many more methods ...
    }

    View Slide

  81. @geeky_android
    class Model
    interface ModelUseCase {
    fun announceModelUpdated()
    fun saveModel(model: Model)
    // many more methods ...
    }
    class DebounceModelUseCase(windowMillis: Long, delegate: ModelUseCase): ModelUseCase by delegate {
    private val debouncer: BehaviorProcessor = BehaviorProcessor.create()
    init {
    debouncer.debounce(windowMillis, TimeUnit.MILLISECONDS)
    .subscribe { delegate.announceModelUpdated() }
    }
    override fun announceModelUpdated() {
    debouncer.onNext(Unit)
    }
    }

    View Slide

  82. @geeky_android
    and much much more
    https://kotlinlang.org/docs/tutorials/koans.html

    View Slide

  83. @geeky_android
    &
    Questions?

    View Slide