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 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) } }