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.

Avatar for Savvas Dalkitsis

Savvas Dalkitsis

October 06, 2017
Tweet

More Decks by Savvas Dalkitsis

Other Decks in Technology

Transcript

  1. @geeky_android Mandatory who-is-ASOS “Our mission is to become the world’s

    number-one online shopping destination for fashion-loving 20-somethings.”
  2. @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 . . . . . .
  3. @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; }
  4. @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 + '}'; } }
  5. @geeky_android data class Model( var name: String, var surname: String,

    var age: Int, var height: Int, var address: Address, var country: Country )
  6. @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 + '}'; } }
  7. @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 + '}'; } }
  8. @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); } }
  9. @geeky_android data class Model( var name: String, var surname: String,

    var age: Int, var height: Int, var address: Address, var country: Country )
  10. @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 )
  11. @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 }
  12. @geeky_android open class ReturnMethodPresenter<T: ReturnMethodView> : BasePresenter<T>() { 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) } } }
  13. @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 }
  14. @geeky_android val age = retrievePersonAge() ?: 0 private fun retrievePersonAge():

    Int? = … val age = retrievePersonAge() ?: throw IllegalStateException("Can I haz age?")
  15. @geeky_android val savvas = Person( name = "Savvas", age =

    confidential(), address = ditto(), email = "[email protected]", country = uk() )
  16. @geeky_android data class Person( val name: String?, val age: Int,

    val email: String?, val address: Address?, val country: Country? )
  17. @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? )
  18. @geeky_android val savvas = Person( name = "Savvas", age =

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

    confidential(), address = ditto(), country = uk() ) val olderSavvas = savvas.copy(age = 50)
  20. @geeky_android fun main(args: Array<String>) { 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<Person> = listOf(Person(age = 19), Person(age = 10), Person(age = 21))
  21. @geeky_android fun main(args: Array<String>) { Intent().navigateWith(someContext()) } fun Intent.navigateWith(context: Context)

    { if (context !is Activity) { this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(this) }
  22. @geeky_android fun main(args: Array<String>) { Intent().apply { action = "someAction"

    setClassName("my.awesome.package", ".MyAwesomeClass") }.navigateWith(someContext()) }
  23. @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<Unit> = BehaviorProcessor.create<Unit>() 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 ... }
  24. @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<Unit> = BehaviorProcessor.create<Unit>() init { debouncer.debounce(windowMillis, TimeUnit.MILLISECONDS) .subscribe { delegate.announceModelUpdated() } } override fun announceModelUpdated() { debouncer.onNext(Unit) } }