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

Over ❤️ Kotlin - How we've used Kotlin to build...

Rebecca Franks
September 28, 2019

Over ❤️ Kotlin - How we've used Kotlin to build a Mobile Design App

Over the past year and a half, I've worked primarily on a Kotlin codebase. We at Over, were lucky enough to get a chance to start a project from scratch and we chose Kotlin for many reasons. Our app has been featured multiple times on the Google Play Store and we have found ourselves facing some unique challenges with the product.

In this talk, I'll cover what my experience has been like working on a Kotlin codebase. I will cover some of the features in Kotlin we use the most, some features we eagerly over used and some of the mistakes we've made along the way.

Rebecca Franks

September 28, 2019
Tweet

More Decks by Rebecca Franks

Other Decks in Programming

Transcript

  1. Over Kotlin How we've used Kotlin to build a Great

    Mobile Design App REBECCA FRANKS ANDROID ENGINEER, OVER ❤ @riggaroo riggaroo.co.za
  2. Graphic design app for the every day person. Social media

    posters, invitations etc. Over madewithover.com
  3. 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
  4. 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
  5. 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
  6. 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 }
  7. 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
  8. .apply {} Brings the block with the variable into this

    scope + returns this value inline fun <T> T.apply(block: T.() -> Unit): T val paint = Paint().apply { color = Color.RED textSize = 24 }
  9. 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
  10. 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
  11. 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" )
  12. 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!")
  13. 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
  14. 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()
  15. How we over used them… - Used them everywhere -

    Instead of creating new classes we created extension functions - - Both a blessing and a curse .
  16. 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
  17. 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
  18. We do this 3 fun String.toGraphemeCharsList(): List<String> { // do

    something to get list of grapheme characters } ✅ This method can totally be part of the String API
  19. 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() }
  20. 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
  21. 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)) } } }
  22. 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
  23. 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
  24. 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.
  25. class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData<Boolean>() val project

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

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

    = MutableLiveData<Project>() val error = MutableLiveData<Throwable?>() 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
  28. class ProjectEditViewModel: ViewModel() { val isLoading = MutableLiveData<Boolean>() val project

    = MutableLiveData<Project>() val error = MutableLiveData<Throwable?>() 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…
  29. 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 }) } }
  30. 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 }) } }
  31. 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
  32. 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
  33. 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
  34. 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() }
  35. class EditorViewModel : ViewModel() { val state = MutableLiveData<EditorState>() fun

    onAction(editorAction: EditorAction) { // TODO take action and get result (processor) // TODO take result and current state - get new state (reducer) state.value = newState } }
  36. 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 } } }
  37. 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?
  38. 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
  39. What’s not so great about MVI? - A lot more

    boilerplate - Difficult for newer developers - Many different interpretations - Might be overkill in certain situations
  40. Summary ✅ Right tech Choice # Easy to learn Tooling

    has some room for improvement ❤ Sealed classes, Ext Funcs Higher Order Funcs Happy to write Kotlin
  41. “I never knew how much I loved Kotlin, until I

    had to write Java again.” - me, August 2019