Slide 1

Slide 1 text

@geeky_android Migrating a mature codebase to Kotlin

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

@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 . . . . . .

Slide 5

Slide 5 text

@geeky_android A mature project 17,927 files

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

@geeky_android

Slide 9

Slide 9 text

@geeky_android Why do we ? !

Slide 10

Slide 10 text

@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; }

Slide 11

Slide 11 text

@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 + '}'; } }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

@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 + '}'; } }

Slide 14

Slide 14 text

@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 + '}'; } }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

@geeky_android How do I convince my ? "

Slide 19

Slide 19 text

@geeky_android A small feature

Slide 20

Slide 20 text

@geeky_android A small isolated feature

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

@geeky_android But WHERE do I begin?

Slide 24

Slide 24 text

@geeky_android

Slide 25

Slide 25 text

@geeky_android Then what?

Slide 26

Slide 26 text

@geeky_android . . . . . .

Slide 27

Slide 27 text

@geeky_android . . .

Slide 28

Slide 28 text

@geeky_android

Slide 29

Slide 29 text

@geeky_android Cmd+Shift+A is your friend

Slide 30

Slide 30 text

@geeky_android

Slide 31

Slide 31 text

@geeky_android

Slide 32

Slide 32 text

@geeky_android

Slide 33

Slide 33 text

@geeky_android

Slide 34

Slide 34 text

@geeky_android

Slide 35

Slide 35 text

@geeky_android

Slide 36

Slide 36 text

@geeky_android

Slide 37

Slide 37 text

@geeky_android

Slide 38

Slide 38 text

@geeky_android

Slide 39

Slide 39 text

@geeky_android

Slide 40

Slide 40 text

@geeky_android

Slide 41

Slide 41 text

@geeky_android

Slide 42

Slide 42 text

@geeky_android

Slide 43

Slide 43 text

@geeky_android

Slide 44

Slide 44 text

@geeky_android

Slide 45

Slide 45 text

@geeky_android

Slide 46

Slide 46 text

@geeky_android

Slide 47

Slide 47 text

@geeky_android

Slide 48

Slide 48 text

@geeky_android

Slide 49

Slide 49 text

@geeky_android

Slide 50

Slide 50 text

@geeky_android

Slide 51

Slide 51 text

@geeky_android

Slide 52

Slide 52 text

@geeky_android #

Slide 53

Slide 53 text

@geeky_android

Slide 54

Slide 54 text

@geeky_android

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

@geeky_android How are we doing?

Slide 60

Slide 60 text

@geeky_android Is it all ☁ and %?

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

@geeky_android

Slide 63

Slide 63 text

@geeky_android

Slide 64

Slide 64 text

@geeky_android

Slide 65

Slide 65 text

@geeky_android

Slide 66

Slide 66 text

@geeky_android • Build times • Switching branches

Slide 67

Slide 67 text

@geeky_android Show me some % % % %!!!

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

@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? )

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

@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 ... }

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

@geeky_android & Questions?