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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Features we love
    Lessons we’ve learnt (

    View Slide

  9. Nullability❓

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. .apply

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. Data Classes

    View Slide

  18. 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"
    )

    View Slide

  19. 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!")

    View Slide

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

    View Slide

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

    View Slide

  22. Extension Functions

    View Slide

  23. 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()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  28. Sealed Classes

    View Slide

  29. 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()
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. State Management
    with Kotlin + MVI

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. 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…

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  46. 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()
    }

    View Slide

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

    View Slide

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

    View Slide

  49. 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?

    View Slide

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

    View Slide

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

    View Slide

  52. Summary

    View Slide

  53. Summary

    Right tech
    Choice
    #
    Easy to learn

    Tooling has some room
    for improvement

    Sealed classes,
    Ext Funcs
    Higher Order Funcs

    Happy to
    write Kotlin

    View Slide

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

    View Slide

  55. Rebecca Franks
    @riggaroo
    Thank you!

    View Slide