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

2a37bf1e025cc1523124774c760df91a?s=47 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.

2a37bf1e025cc1523124774c760df91a?s=128

Rebecca Franks

September 28, 2019
Tweet

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. Over Featured a few times: Early Access New Apps New

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

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

    in March 2018
  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
  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
  8. Features we love Lessons we’ve learnt (

  9. Nullability❓

  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
  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 }
  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
  13. .apply

  14. .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 }
  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
  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
  17. Data Classes

  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" )
  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!")
  20. How we’ve used them… Formed a fundamental part to our

    MVI flows.
  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
  22. Extension Functions

  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()
  24. How we over used them… - Used them everywhere -

    Instead of creating new classes we created extension functions - - Both a blessing and a curse .
  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
  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
  27. 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
  28. Sealed Classes

  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() }
  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
  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)) } } }
  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
  33. State Management with Kotlin + MVI

  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
  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.
  36. 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 }) } }
  37. 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 }) } }
  38. 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
  39. 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…
  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 }) } }
  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 }) } }
  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
  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
  44. MVI / Uni-directional Data Flow - Model everything around a

    Single State
  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
  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() }
  47. 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 } }
  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 } } }
  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?
  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
  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
  52. Summary

  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
  54. “I never knew how much I loved Kotlin, until I

    had to write Java again.” - me, August 2019
  55. Rebecca Franks @riggaroo Thank you!