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

The Journey From Legacy Code to Idiomatic Kotlin

Seetha
November 25, 2019

The Journey From Legacy Code to Idiomatic Kotlin

We are often faced with large legacy codebases in Java that need to be converted to Kotlin.

Sure, you can let Android Studio do the conversion, but then you end up with a bunch of !!s and inefficient Kotlin code. So where do you begin?

This talk walks you through key steps in converting your Java codebase to efficient, idiomatic Kotlin. After working with multiple clients on legacy projects, I have learned lessons on doing this conversion efficiently, and ensuring that you are taking full advantage of Kotlin. We will walk through the process, along with tips on major patterns that generally require attention. After all, shouldn't every developer's goal be to leave the codebase better than you found it?

Seetha

November 25, 2019
Tweet

More Decks by Seetha

Other Decks in Technology

Transcript

  1. The Journey From Legacy
    Code to Idiomatic Kotlin
    Seetha Annamraju
    @seetha_a

    View full-size slide

  2. Every Android project
    written in Java is now a
    legacy project.

    View full-size slide

  3. ◂ What is idiomatic Kotlin?
    ◂ Convert common patterns into idiomatic
    Kotlin
    ◂ Ideas and suggestions
    Outline

    View full-size slide

  4. ◂ You know basic Kotlin syntax.
    ◂ You believe code should be left better
    than you found it.
    Assumptions

    View full-size slide

  5. Step 1:
    Take a step back and
    look at your project.

    View full-size slide

  6. ◂ How much time do I have?
    ◂ What are the priority items?
    ◂ Are there components that need fixes?
    Ask yourself:

    View full-size slide

  7. Step 2:
    Start converting.

    View full-size slide

  8. ◂ Rewrite the file in Kotlin from scratch.
    Option 1: Rewrite

    View full-size slide

  9. ◂ Rewrite the file in Kotlin from scratch.
    ◂ Works well for large files or files with
    many dependencies.
    Option 1: Rewrite

    View full-size slide

  10. ◂ "Convert Java file
    to Kotlin file”
    Option 2: Pull off the bandaid

    View full-size slide

  11. ◂ 1. Compiler errors

    View full-size slide

  12. ◂ 1. Compiler errors
    ◂ 2. Warnings and suggestions

    View full-size slide

  13. ◂ 1. Compiler errors
    ◂ 2. Warnings and suggestions
    ◂ 3. Idiomatic Kotlin stuff

    View full-size slide

  14. Step 3:
    Ensure Idiomatic code.

    View full-size slide

  15. ◂ Null Safety
    ◂ Immutability
    ◂ Being expressive and concise
    ◂ Easy Interoperability
    What is idiomatic Kotlin?

    View full-size slide

  16. Item #1: Null Safety
    Getting rid of !!s

    View full-size slide

  17. ◂ ? —“maybe it’s null?”
    ◂ !! —“this is definitely NOT null!!”
    Null Safety

    View full-size slide

  18. fun glDrawFrame(viewProjectionMatrix: FloatArray, eyeType: Int) {
    // ...
    if (frameAvailable.compareAndSet(true, false)) {
    displayTexture!!.updateTexImage()
    checkGlError()
    }
    displayMesh!!.glDraw(viewProjectionMatrix, eyeType)
    if (videoUiView != null) {
    canvasQuad!!.glDraw(viewProjectionMatrix, videoUiView.alpha)
    }
    reticle.glDraw(viewProjectionMatrix, controllerOrientationMatrix)
    }
    fun glShutdown() {
    if (displayMesh != null) {
    displayMesh!!.glShutdown()
    }
    // ...
    }
    @BinderThread
    @Synchronized
    fun setControllerOrientation(currentOrientation: Orientation) {
    this.controllerOrientation = currentOrientation
    controllerOrientation!!.toRotationMatrix(controllerOrientationMatrix)
    }
    @MainThread
    fun handleClick() {
    // ...
    val clickTarget = CanvasQuad.translateClick(controllerOrientation!!)
    // ...
    // The actual processing of the synthetic event needs to happen in the UI thread.
    uiHandler!!.post {
    // ...
    }
    }
    Code Sample

    View full-size slide

  19. fun glDrawFrame(viewProjectionMatrix: FloatArray, eyeType: Int) {
    // ...
    if (frameAvailable.compareAndSet(true, false)) {
    displayTexture!!.updateTexImage()
    checkGlError()
    }
    displayMesh!!.glDraw(viewProjectionMatrix, eyeType)
    if (videoUiView != null) {
    canvasQuad!!.glDraw(viewProjectionMatrix, videoUiView.alpha)
    }
    reticle.glDraw(viewProjectionMatrix, controllerOrientationMatrix)
    }
    fun glShutdown() {
    if (displayMesh != null) {
    displayMesh!!.glShutdown()
    }
    // ...
    }
    @BinderThread
    @Synchronized
    fun setControllerOrientation(currentOrientation: Orientation) {
    this.controllerOrientation = currentOrientation
    controllerOrientation!!.toRotationMatrix(controllerOrientationMatrix)
    }
    @MainThread
    fun handleClick() {
    // ...
    val clickTarget = CanvasQuad.translateClick(controllerOrientation!!)
    // ...
    // The actual processing of the synthetic event needs to happen in the UI thread.
    uiHandler!!.post {
    // ...
    }
    }
    Code Sample
    !!
    !!
    !!
    !!
    !!
    !!
    !!

    View full-size slide

  20. Safe-call operator (?)
    fun destroy() {
    mediaPlayer?.stop()
    mediaPlayer?.release()
    mediaPlayer = null
    isDestroyed = true
    }

    View full-size slide

  21. Safe-call operator (?)
    fun destroy() {
    mediaPlayer?.stop()
    mediaPlayer?.release()
    mediaPlayer = null
    isDestroyed = true
    }

    View full-size slide

  22. Use Elvis operator
    val result = name.toString ?: "Unknown"

    View full-size slide

  23. Can you use lateinit var?
    @Inject
    lateinit var viewModelFactory:ViewModelProvider.Factory

    View full-size slide

  24. Item #2: Immutability
    By lazy, local captures

    View full-size slide

  25. Can you use by lazy?
    private MediaLoader mediaLoader;
    public void initialize(){
    mediaLoader = new
    MediaLoader(getContext());
    ...
    }

    View full-size slide

  26. Can you use by lazy?
    private val mediaLoader: MediaLoader by lazy { MediaLoader(context) }
    private MediaLoader mediaLoader;
    public void initialize(){
    mediaLoader = new
    MediaLoader(getContext());
    ...
    }

    View full-size slide

  27. Local captures of values

    View full-size slide

  28. private var mediaPlayer: MediaPlayer? = null
    if (mediaPlayer != null && sceneRenderer != null) {
    displaySurface = sceneRenderer?.createDisplay(
    mediaPlayer!!.videoWidth,
    mediaPlayer!!.videoHeight, mesh)
    mediaPlayer!!.apply {
    setSurface(displaySurface)
    isLooping = true
    start()
    }
    }
    Local captures of values

    View full-size slide

  29. private var mediaPlayer: MediaPlayer? = null
    if (mediaPlayer != null && sceneRenderer != null) {
    displaySurface = sceneRenderer?.createDisplay(
    mediaPlayer!!.videoWidth,
    mediaPlayer!!.videoHeight, mesh)
    mediaPlayer!!.apply {
    setSurface(displaySurface)
    isLooping = true
    start()
    }
    }
    Local captures of values

    View full-size slide

  30. Local captures of values
    val player = mediaPlayer
    val renderer = sceneRenderer
    if (player != null && renderer != null) {
    displaySurface = renderer.createDisplay(
    player.videoWidth, player.videoHeight, mesh
    )
    with(player) {
    setSurface(displaySurface)
    isLooping = true
    start()
    }
    }

    View full-size slide

  31. Local captures of values
    val player = mediaPlayer
    val renderer = sceneRenderer
    if (player != null && renderer != null) {
    displaySurface = renderer.createDisplay(
    player.videoWidth, player.videoHeight, mesh
    )
    with(player) {
    setSurface(displaySurface)
    isLooping = true
    start()
    }
    }

    View full-size slide

  32. Item #3: Being more concise and expressive
    Using Kotlin’s Standard Library

    View full-size slide

  33. Over 70% of your time is spent
    reading code, rather than writing
    code.
    -Unknown

    View full-size slide

  34. Java if-statements
    if (detailResponse != null
    && detailResponse.getStudent() != null
    && !Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses())
    && detailResponse.getStudent()
    .getRegisteredClasses().get(0) != null
    && !Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses()
    .get(0).getSubjectAssignments())
    &&
    detailResponse.getStudent().getRegisteredClasses().get(0)
    .getSubjectAssignments().get(0) != null){
    //do something
    }

    View full-size slide

  35. try{
    detailResponse.getStudent().getRegisteredClasses()
    .get(0).getSubjectAssignments().get(0)
    } catch (NullPointerException e){}
    Java shorter statement

    View full-size slide

  36. Kotlin’s Let
    detailResponse.getStudent?.getRegisteredClasses()?
    .get(0)?.getSubjectAssignments()?.get(0)?.let {
    //do something
    }

    View full-size slide

  37. if (detailResponse != null
    && detailResponse.getStudent() != null
    && !
    Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses())
    && detailResponse.getStudent()
    .getRegisteredClasses().get(0) != null
    && !
    Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses()
    .get(0).getSubjectAssignments())
    &&
    detailResponse.getStudent().getRegisteredClasses()
    .get(0)
    .getSubjectAssignments().get(0) != null){
    //do something
    }
    Kotlin’s Let

    View full-size slide

  38. if (detailResponse != null
    && detailResponse.getStudent() != null
    && !
    Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses())
    && detailResponse.getStudent()
    .getRegisteredClasses().get(0) != null
    && !
    Helper.isListEmpty(detailResponse.getStudent()
    .getRegisteredClasses()
    .get(0).getSubjectAssignments())
    &&
    detailResponse.getStudent().getRegisteredClasses()
    .get(0)
    .getSubjectAssignments().get(0) != null){
    //do something
    }
    Kotlin’s Let
    detailResponse.getStudent?.getRegisteredClasses()?
    .get(0)?.getSubjectAssignments()?.get(0)?.let {
    //do something
    }

    View full-size slide

  39. Apply
    mediaPlayer = (MediaPlayer()).apply {
    setDisplay(surfaceHolder)
    setDataSource(context, uri)
    prepare()
    setOnPreparedListener(preparedListener)
    }

    View full-size slide

  40. Apply
    mediaPlayer = (MediaPlayer()).apply {
    setDisplay(surfaceHolder)
    setDataSource(context, uri)
    prepare()
    setOnPreparedListener(preparedListener)
    }

    View full-size slide

  41. With
    fun getCourse(syllabus: Syllabus): Course {
    return with(syllabus) {
    Course(title, professorName, syllabus)
    }
    }

    View full-size slide

  42. With
    fun getCourse(syllabus: Syllabus): Course {
    return with(syllabus) {
    Course(title, professorName, syllabus)
    }
    }

    View full-size slide

  43. Transforming Collections

    View full-size slide

  44. Transforming Collections
    Students -> StudentViewItems

    View full-size slide

  45. Transforming Collections
    val studentsViewItems = studentsList
    .mapIndexed { index, student ->
    Pair(student.major, StudentViewItem(student.name,
    student.imgURL))
    }
    .filter { (studentMajor, _) ->
    studentMajor == COMPUTER_SCIENCE
    }.map { (_, studentViewItem) ->
    studentViewItem
    }

    View full-size slide

  46. Transforming Collections
    val studentsViewItems = studentsList
    .mapIndexed { index, student ->
    Pair(student.major, StudentViewItem(student.name,
    student.imgURL))
    }
    .filter { (studentMajor, _) ->
    studentMajor == COMPUTER_SCIENCE
    }.map { (_, studentViewItem) ->
    studentViewItem
    }

    View full-size slide

  47. Transforming Collections
    val studentsViewItems = studentsList
    .mapIndexed { index, student ->
    Pair(student.major, StudentViewItem(student.name,
    student.imgURL))
    }
    .filter { (studentMajor, _) ->
    studentMajor == COMPUTER_SCIENCE
    }.map { (_, studentViewItem) ->
    studentViewItem
    }

    View full-size slide

  48. Transforming Collections
    val studentsViewItems = studentsList
    .mapIndexed { index, student ->
    Pair(student.major, StudentViewItem(student.name,
    student.imgURL))
    }
    .filter { (studentMajor, _) ->
    studentMajor == COMPUTER_SCIENCE
    }.map { (_, studentViewItem) ->
    studentViewItem
    }

    View full-size slide

  49. Item #4: Easy interop
    Models and Utility Classes

    View full-size slide

  50. Annotate your Java APIs
    @NonNull
    @Nullable

    View full-size slide

  51. public class User {
    private String firstName;
    private String lastName;
    public User(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
    public String getFirstName() {
    return firstName;
    }
    public void setFirstName(String firstName) {
    this.firstName = firstName;
    }
    public String getLastName() {
    return lastName;
    }
    public void setLastName(String lastName) {
    this.lastName = lastName;
    }
    }
    Convert your Java Objects

    View full-size slide

  52. public class User {
    private String firstName;
    private String lastName;
    public User(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
    public String getFirstName() {
    return firstName;
    }
    public void setFirstName(String firstName) {
    this.firstName = firstName;
    }
    public String getLastName() {
    return lastName;
    }
    public void setLastName(String lastName) {
    this.lastName = lastName;
    }
    }
    Convert your Java Objects
    data class User(var firstName: String?, var lastName: String?)

    View full-size slide

  53. Extensions for utility classes
    public class ExtraStringUtils {
    public ExtraStringUtils(){}
    public static String toOppositeCase(String str)
    {
    // ...
    }
    }

    View full-size slide

  54. StringExt.kt
    @JvmMultifileClass
    @file:JvmName("ExtraStringUtils")
    package com.example.androidtest
    // ...
    fun String.toOppositeCase(): String {
    // ...
    }

    View full-size slide

  55. FAQ
    Common Questions

    View full-size slide

  56. How do I do a large
    refactoring into Kotlin?

    View full-size slide

  57. ◂ Commit #1: Convert file to Kotlin
    Refactoring to Kotlin

    View full-size slide

  58. ◂ Commit #1: Convert file to Kotlin
    ◂ Commit #2: Fix compiler errors
    Refactoring to Kotlin

    View full-size slide

  59. ◂ Commit #1: Convert file to Kotlin
    ◂ Commit #2: Fix compiler errors
    ◂ Commit #3: Fix warnings
    Refactoring to Kotlin

    View full-size slide

  60. ◂ Commit #1: Convert file to Kotlin
    ◂ Commit #2: Fix compiler errors
    ◂ Commit #3: Fix warnings
    ◂ Commit #4: Idiomatic changes
    Refactoring to Kotlin

    View full-size slide

  61. How do I ensure
    idiomatic code?

    View full-size slide

  62. ◂ Code reviews
    ◂ Learn from each other
    ◂ https://kotlinlang.org/docs/reference/
    idioms.html
    Ensuring Idiomatic Kotlin

    View full-size slide

  63. What’s next?

    View full-size slide

  64. ◂ Lambdas
    ◂ Sealed classes
    ◂ Standard library functions
    What’s next?

    View full-size slide

  65. ◂ Step back and take a look at the project.
    ◂ Look for areas to apply null safety,
    immutability, readability, better interop
    ◂ Keep your team on the same page.
    Recap

    View full-size slide

  66. What challenges do you
    face?
    @seetha_a / seetha.io

    View full-size slide