Slide 1

Slide 1 text

Over Kotlin How we've used Kotlin to build a Great Mobile Design App REBECCA FRANKS ANDROID ENGINEER, OVER ❤ @riggaroo riggaroo.co.za

Slide 2

Slide 2 text

Graphic design app for the every day person. Social media posters, invitations etc. Over madewithover.com

Slide 3

Slide 3 text

Over Featured a few times: Early Access New Apps New and Updated Apps

Slide 4

Slide 4 text

Over #2 / #3 Top Grossing in Art & Design

Slide 5

Slide 5 text

History Dev started in November/December 2017 I joined the team in March 2018

Slide 6

Slide 6 text

Our experience - Transitioning from Java backgrounds # Easy to learn Start slow and use more concepts as you Get comfortable Tooling has some room for improvement

Slide 7

Slide 7 text

Was it a good choice for us? Yes! Android is Kotlin first now Our services team writes Kotlin Developers don't want to write Java anymore

Slide 8

Slide 8 text

Features we love Lessons we’ve learnt (

Slide 9

Slide 9 text

Nullability❓

Slide 10

Slide 10 text

What is Nullability? Distinguishing between an object that can hold null and one that cannot: // can be null var user : User? // cannot be null var user : User

Slide 11

Slide 11 text

How do we use it? Explicitly mark things as null when required Hardly ever use !! Use let, if, etc to check. var user : User? user?.let { // user is now not null in this block }

Slide 12

Slide 12 text

Why we love it… - Forces us to cater for all scenarios - A great crash-free rate overall. - When we release new features, we don’t get NullPointerExceptions

Slide 13

Slide 13 text

.apply

Slide 14

Slide 14 text

.apply {} Brings the block with the variable into this scope + returns this value inline fun T.apply(block: T.() -> Unit): T val paint = Paint().apply { color = Color.RED textSize = 24 }

Slide 15

Slide 15 text

How we misused this - HUGE blocks with apply in use, hard to know that you are in that objects scope - Be careful with functions that change the this scope of code

Slide 16

Slide 16 text

What we do now: - Use apply for initialising a variable - not just for bringing the object into this scope - Don’t use it for large blocks of code that are unrelated to initialisation

Slide 17

Slide 17 text

Data Classes

Slide 18

Slide 18 text

What are Data classes? Class that holds data. - equals(), toString(), copy() automatically implemented data class TextLayer( val identifier: String = UUID.randomUUID().toString(), val opacity: Float = 1f, val text: String = "I'm a text layer" )

Slide 19

Slide 19 text

How we’ve used them… data class TextLayer( val identifier: String = UUID.randomUUID().toString(), val opacity: Float = 1f, val text: String = "I'm a text layer" ) val layer = TextLayer() val newTextLayer = layer.copy(text = "I've changed the text!")

Slide 20

Slide 20 text

How we’ve used them… Formed a fundamental part to our MVI flows.

Slide 21

Slide 21 text

Why we love them… • Less code than Java + less mess overall • No need to implement toString(), hashCode() etc. • No need for Builder pattern • Named arguments + Default Values

Slide 22

Slide 22 text

Extension Functions

Slide 23

Slide 23 text

What are Extension Functions? Extending a class with functionality without needing to edit the original class. Good for libraries who’s code you can’t edit. fun Canvas.center(): Point = Point(width / 2f, height / 2f) val centerPoint = canvas.center()

Slide 24

Slide 24 text

How we over used them… - Used them everywhere - Instead of creating new classes we created extension functions - - Both a blessing and a curse .

Slide 25

Slide 25 text

What we do now… • Still use them but not as much • Create new classes for our own functions • Use Extension functions on classes that aren't extendable, or for clear separation of concerns • Create private ones

Slide 26

Slide 26 text

We don’t do this ❌ fun String.toUserProperties() : UserProperties { return UserProperties(this.toUppercase()) } ⛔ We don’t use them for non related stuff. This method should not be part of the String API 2

Slide 27

Slide 27 text

We do this 3 fun String.toGraphemeCharsList(): List { // do something to get list of grapheme characters } ✅ This method can totally be part of the String API

Slide 28

Slide 28 text

Sealed Classes

Slide 29

Slide 29 text

What is a sealed class? • More powerful enums • Limited number of direct subclasses • Must all be defined in the same file sealed class AddLayerResult : EditorResult { data class Error(val exception: Exception) : AddLayerResult() data class Success(val session: ProjectSession) : AddLayerResult() }

Slide 30

Slide 30 text

How we’ve used it… - For state management with MVI (more on this) - Explicit when statements to handle all cases - Represent user actions - ie AddLayerAction, FontChangeAction etc

Slide 31

Slide 31 text

How we’ve used it… override fun reduce(state: EditorState, result: EditorResult): EditorState? { return when (result) { is AddLayerResult.Success -> { state.copy(session = result.session) } is AddLayerResult.Error -> { state.copy(navigation = Navigation.Error(result.exception)) } } }

Slide 32

Slide 32 text

Why we love them… • Makes state easier to reason about • Ensures handling of all possible cases - all states are mapped and must be handled • Less error prone code

Slide 33

Slide 33 text

State Management with Kotlin + MVI

Slide 34

Slide 34 text

Keeping track of how the UI should look on complex screens, can get quite tricky. - Loading? - Is there an error? - Currently selected tool? - Layers + properties of the project? Many interactions which would affect the state of what should be shown on screen. State Management

Slide 35

Slide 35 text

Traditional Approaches Used to using MVP or more recently MVVM to separate our screen logic from the UI. Let’s have a look at how we could use MVVM, and its potential issues.

Slide 36

Slide 36 text

class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData() val project = MutableLiveData() val error = MutableLiveData() fun createProject() { isLoading.value = true repository.createProject() .subscribe ({ project -> isLoading.value = false project.value = project }, { error -> isLoading.value = false error.value = error }) } }

Slide 37

Slide 37 text

class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData() val project = MutableLiveData() val error = MutableLiveData() fun createProject() { isLoading.value = true repository.createProject() .subscribe ({ project -> isLoading.value = false project.value = project }, { error -> isLoading.value = false error.value = error }) } }

Slide 38

Slide 38 text

class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData() val project = MutableLiveData() val error = MutableLiveData() fun createProject() { isLoading.value = true repository.createProject() .subscribe ({ project -> isLoading.value = false project.value = project }, { error -> isLoading.value = false error.value = error }) } } User Clicks "Create Project” it fails - error value is populated. User Clicks "Create Project" again Error value is still populated. Now we have a. loaded project + error screen showing at the same time

Slide 39

Slide 39 text

class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData() val project = MutableLiveData() val error = MutableLiveData() fun createProject() { isLoading.value = true repository.createProject() .subscribe ({ project -> isLoading.value = false error.value = null project.value = project }, { error -> isLoading.value = false error.value = error }) } } // No problem! We will just reset this value on load success…

Slide 40

Slide 40 text

class ProjectFragment: Fragment() { fun observeViewModelChanges() { viewModel.isLoading.observe(this, Observer { // show loading or hide it }) viewModel.project.observe(this, Observer { // show project }) viewModel.error.observe(this, Observer { // show error or hide it }) } }

Slide 41

Slide 41 text

class ProjectFragment: Fragment() { fun observeViewModelChanges() { viewModel.isLoading.observe(this, Observer { // show loading or hide it }) viewModel.project.observe(this, Observer { // show project }) viewModel.error.observe(this, Observer { // show error or hide it }) } }

Slide 42

Slide 42 text

class ProjectFragment: Fragment() { fun observeViewModelChanges() { viewModel.isLoading.observe(this, Observer { // show loading or hide it }) viewModel.project.observe(this, Observer { // show project }) viewModel.error.observe(this, Observer { // show error or hide it }) } } What is the overall state of the UI? Loading could be shown at the same time as an error or the project

Slide 43

Slide 43 text

It’s hard to know at a single point in time, how the UI should look. No single snapshot to be able to recreate the UI from. Are we handling all cases? Problems with MVP/MVVM

Slide 44

Slide 44 text

MVI / Uni-directional Data Flow - Model everything around a Single State

Slide 45

Slide 45 text

MVI / Uni-directional Data Flow View ViewModel Processor Reducer Emits Actions Sends State for view to render Sends Actions Sends Result State + Result = New State

Slide 46

Slide 46 text

sealed class EditorAction { data class LoadProjectAction(val identifier: String): EditorAction() data class AddLayerAction(val stuff: String) : EditorAction() // more actions here... } sealed class EditorState { object Loading: EditorState() data class Error(val throwable: Throwable): EditorState() data class Initial(val project: Project): EditorState() }

Slide 47

Slide 47 text

class EditorViewModel : ViewModel() { val state = MutableLiveData() fun onAction(editorAction: EditorAction) { // TODO take action and get result (processor) // TODO take result and current state - get new state (reducer) state.value = newState } }

Slide 48

Slide 48 text

class MainActivity : Fragment() { fun setupViewModel() { editorViewModel.state.observe(this, Observer { state -> render(state) }) editorViewModel.onAction(EditorAction.LoadProjectAction("384")) } private fun render(editorState: EditorState){ when (editorState){ is EditorState.Initial -> // show project etc is EditorState.Error -> // show error, hide project EditorState.Loading -> // show loading, hide other things } } }

Slide 49

Slide 49 text

More on MVI Lots of frameworks and great posts about it already - MvRx (Airbnb) - https://github.com/airbnb/MvRx - Mosby - https://github.com/sockeqwe/mosby - Mobius (Spotify) - https://github.com/spotify/mobius - Roxie (WW) - https://github.com/ww-tech/roxie Roll your own?

Slide 50

Slide 50 text

Why do we love it? ❤ - Much easier to reason about state - Less error prone when new features are added - Leverages data classes .copy() + sealed classes heavily - Can’t imagine our editor without this state management

Slide 51

Slide 51 text

What’s not so great about MVI? - A lot more boilerplate - Difficult for newer developers - Many different interpretations - Might be overkill in certain situations

Slide 52

Slide 52 text

Summary

Slide 53

Slide 53 text

Summary ✅ Right tech Choice # Easy to learn Tooling has some room for improvement ❤ Sealed classes, Ext Funcs Higher Order Funcs Happy to write Kotlin

Slide 54

Slide 54 text

“I never knew how much I loved Kotlin, until I had to write Java again.” - me, August 2019

Slide 55

Slide 55 text

Rebecca Franks @riggaroo Thank you!