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

Anko to Jetpack Compose, LINE Sticker Maker's Technical Debt Resolution

Anko to Jetpack Compose, LINE Sticker Maker's Technical Debt Resolution

Po-Hao Chen (Po) (LINE Fukuoka / Development C Team / Android Developer)

https://tech-verse.me/ja/sessions/16
https://tech-verse.me/en/sessions/16
https://tech-verse.me/ko/sessions/16

Tech-Verse2022
PRO

November 18, 2022
Tweet

More Decks by Tech-Verse2022

Other Decks in Technology

Transcript

  1. View Slide

  2. Takeaway of This Session
    - Why the migration is inevitable
    - Migration plan plays a big role to protect user experience
    - Our learning when migrating from Anko to Jetpack Compose

    View Slide

  3. Agenda
    - About LINE Sticker Maker
    - Tech Stack and Debt
    - Migration Plan
    - Obstacles and Resolution
    - Release Strategy

    View Slide

  4. About Me
    Po-Hao Chen (Po)
    - Android Engineer at LINE Fukuoka
    - Project
    - LINE Wallet
    - LINE Sticker Maker

    View Slide

  5. LINE Sticker Maker
    - Creating LINE Stickers on
    smartphone with simple edit flow
    - Create for scratch, existing image or
    LINE Avatar
    - Various image editing tools
    - Concluding the flow of creating sticker
    to requesting for sale all on one
    device/app

    View Slide

  6. LINE Sticker Maker History
    2021.05
    Rebranding to LINE
    Sticker Maker
    2018.10
    Image Auto
    Trimming Feature
    with OpenCV
    2021.12
    LINE Avatar
    Sticker Creation
    2018.11
    Sticker Campaign
    Frame Feature
    2017.06
    Service Launched
    as LINE Creators
    Studio

    View Slide

  7. Tech Stack
    - Kotlin
    - Anko Layout
    - RxJava
    - Coroutine
    - Realm
    - Retrofit
    - OkHttp
    - Hilt

    View Slide

  8. Tech Stack
    - Kotlin;
    - Anko Layout
    - RxJava
    - Coroutine
    - Realm
    - Retrofit
    - OkHttp
    - Hilt
    !!

    View Slide

  9. What is Anko Layout
    A fast and type-safe way to write dynamic Android layouts with KotlinaDSL, by JetBrains

    View Slide

  10. Anko Layout
    linearLayout {
    }

    View Slide

  11. Anko Layout
    linearLayout {
    id = R.id.container
    gravity = Gravity.CENTER
    backgroundColor = R.color.background
    }

    View Slide

  12. Anko Layout
    linearLayout {
    id = R.id.container
    gravity = Gravity.CENTER
    backgroundColor = R.color.background
    themedTextView(R.style.CommonButtonStyle) {
    textResource = R.string.common_done
    }
    }

    View Slide

  13. Anko Layout
    linearLayout {
    id = R.id.container
    gravity = Gravity.CENTER
    backgroundColor = R.color.background
    themedTextView(R.style.CommonButtonStyle) {
    textResource = R.string.common_done
    onClick {
    actionA()
    }
    }
    }

    View Slide

  14. Anko Layout
    linearLayout {
    id = R.id.container
    gravity = Gravity.CENTER
    backgroundColor = R.color.background
    themedTextView(R.style.CommonButtonStyle) {
    textResource = R.string.common_done
    onClick {
    if (isFirstVisit) {
    actionA()
    } else {
    actionB()
    }
    }
    }
    }

    View Slide

  15. Write UI with Anko
    - Fast Performance

    View Slide

  16. - Presentation Logic in UI code
    - Fast Performance
    Write UI with Anko

    View Slide

  17. - Presentation Logic in UI code
    - More Flexible than XML
    - Fast Performance
    Write UI with Anko

    View Slide

  18. However, in 2019…

    View Slide

  19. The Team Decided to Keep Using it

    View Slide

  20. The Team Decided to Keep Using it
    2 Years Later…

    View Slide

  21. The Team Decided to Keep Using it
    2 Years Later…
    The Technical Debt

    View Slide

  22. The Technical Debt
    As we decided to continue to self-maintain Anko Layout
    - Anko Layout is lack of UI Preview feature

    View Slide

  23. The Technical Debt
    As we decided to continue to self-maintain Anko Layout
    - diff Anko-Layout Android-Support-Library
    - Anko Layout is lack of UI Preview feature

    View Slide

  24. The Technical Debt
    As we decided to continue to self-maintain Anko Layout
    - diff Anko-Layout Android-Support-Library
    - Bottleneck for introducing new-comers
    - Anko Layout is lack of UI Preview feature

    View Slide

  25. To Jetpack Compose
    The Declarative UI Framework for Android
    - Migration was considered soon after
    the deprecation of Anko Layout
    - Jetpack Compose stable-release in
    July 2021
    - The Team proceed to migrate in
    March 2022

    View Slide

  26. Migration is Inevitable

    View Slide

  27. Migration is Inevitable
    Migration
    Started for
    Developer Needs

    View Slide

  28. Migration is Inevitable
    What if…?
    Migration
    Started for
    Developer Needs
    User get
    Affected "
    Migration Side Effect

    View Slide

  29. Migration is Inevitable
    What if…?
    User get
    Affected "
    Migration Side Effect

    Migration
    Started for
    Developer Needs
    Developer Experience x User Experience

    View Slide

  30. Developer Experience x User Experience
    Migration is Inevitable
    What if…?
    User get
    Affected "
    Migration Side Effect

    Migration
    Started for
    Developer Needs
    A Proper Migration Plan is Important

    View Slide

  31. Strategy for Continuous Migration
    Let the Migration
    Begins

    View Slide

  32. Migration Goal
    Keep in mind what the most important goal is
    For Maintainability
    Replace
    Anko

    View Slide

  33. Migration Goal
    Keep in mind what the most important goal is
    Refactor
    Legacy Code
    For Maintainability
    1st Priority
    Replace
    Anko
    UI
    Performance
    Keep minimal Avoid over-tuning

    View Slide

  34. Migration Goal
    Keep in mind what the most important goal is
    Refactor
    Legacy Code
    For Maintainability
    1st Priority
    Replace
    Anko
    UI
    Performance
    Keep minimal Avoid over-tuning
    Keep in Sync, Aiming for the Same Goal

    View Slide

  35. Migration Plan
    Release Remove Improve
    Implement

    View Slide

  36. Migration Plan
    Refactor and
    recreate the same
    UI in Compose
    After QA, enable
    Compose in release
    build
    Release Remove Improve
    Implement
    Remove Anko
    completely
    Replace
    Fragment-based
    structure

    View Slide

  37. Migration Plan
    Current Phase
    Refactor and
    recreate the same
    UI in Compose
    After QA, enable
    Compose in release
    build
    Release Remove Improve
    Implement

    View Slide

  38. LINE Sticker Maker Code Structure
    Feature-based Multi-Module
    - Features are independent from
    each other
    :app
    :feature:a :feature:b :feature:c
    :core

    View Slide

  39. LINE Sticker Maker Code Structure
    Feature-based Multi-Module
    - Features are independent from
    each other
    - Create a `common-ui` module with
    no other dependency for common
    UI components
    :app
    :feature:a :feature:b :feature:c
    :core :common-ui

    View Slide

  40. Obstacles for Migration
    - Mixing Layer between data and UI in older features
    - Massive Usage of Custom Android View
    - Fragment Based Navigation

    View Slide

  41. Yes or No Fragments?

    View Slide

  42. Keep Most of the Fragments
    For now
    - Migrate to Navigation Compose at one go is risky
    - Speedy iteration of screen-based migration and quality assurance
    - Have the Screen Composable ready for future improvement

    View Slide

  43. Mixing of Layers

    View Slide

  44. - Presentation Logic in UI code
    - More Flexible than XML
    - Fast Performance
    Write UI with Anko

    View Slide

  45. - Presentation Logic in UI code
    - More Flexible than XML
    - Fast Performance
    Write UI with Anko

    View Slide

  46. Current Situation
    UI
    Anko
    Data Provider
    Data Provider
    .
    .
    .

    View Slide

  47. Refactor for Clean Architecture
    Data Layer
    UI Layer
    UI Elements
    StateHolder/ViewModel
    App Data
    UI State (Processed Data) UI Event

    View Slide

  48. Current Situation
    UI
    Anko
    Data Provider
    Data Provider
    .
    .
    .

    View Slide

  49. Refactor Result
    UI
    Anko
    Data Provider
    Data Provider
    .
    .
    . ViewModel
    UI Implementation

    View Slide

  50. .uiState
    Isolated Ui Data
    class SampleViewModel() {
    val uiState: StateFlow
    }

    View Slide

  51. Isolated Ui Data
    class SampleScreenFragment : Fragment() {
    override fun onCreateView(): View =
    SampleScreenUi().createView(AnkoContext.create(requireContext(), this))
    .also {
    viewLifecycleOwner.lifecycleScope.launch {
    sampleViewModel
    .uiState
    .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
    .collect {
    // update UI
    }
    }
    }
    }

    View Slide

  52. Next Step: Migrate to Compose
    UI
    Anko
    ViewModel
    Compose
    Swap UI Implementation

    View Slide

  53. Feature Flag in UI Layer
    UI
    Anko
    Compose
    ViewModel FeatureFlag

    View Slide

  54. Same Data, Different UI Implementation
    class SampleScreenFragment : Fragment() {
    override fun onCreateView(): View =
    SampleScreenUi().createView(AnkoContext.create(requireContext(), this))
    .also {
    viewLifecycleOwner.lifecycleScope.launch {
    sampleViewModel
    .uiState
    .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
    .collect {
    // update UI
    }
    }
    }
    }

    View Slide

  55. Same Data, Different UI Implementation
    class SampleScreenFragment : Fragment() {
    override fun onCreateView(): View = if (FeatureFlag.FEATURE_SAMPLE_USE_COMPOSE) {
    } else {
    SampleScreenUi().createView(AnkoContext.create(requireContext(), this))
    }
    }

    View Slide

  56. Same Data, Different UI Implementation
    class SampleScreenFragment : Fragment() {
    override fun onCreateView(): View = if (FeatureFlag.FEATURE_SAMPLE_USE_COMPOSE) {
    // Compose View Creation
    ComposeView(requireContext()).apply {
    // ...
    }
    }
    } else {
    // Anko View Creation
    SampleScreenUi().createView(AnkoContext.create(requireContext(), this))
    }
    }

    View Slide

  57. Same Data, Different UI Implementation
    class SampleScreenFragment : Fragment() {
    override fun onCreateView(): View = if (FeatureFlag.FEATURE_SAMPLE_USE_COMPOSE) {
    // Compose View Creation
    ComposeView(requireContext()).apply {
    // ...
    setContent {
    val uiState by sampleViewModel
    .uiState
    .collectAsLifecycleAwareState(SampleViewModel.UiState())
    }
    }
    } else {
    // Anko View Creation
    SampleScreenUi().createView(AnkoContext.create(requireContext(), this))
    }
    }

    View Slide

  58. AS IS
    Activity
    Fragment
    AnkoView
    Fragment Fragment
    AnkoView AnkoView

    View Slide

  59. TO BE
    Activity
    Fragment
    AnkoView
    Fragment Fragment
    AnkoView AnkoView
    ComposeView ComposeView ComposeView
    FeatureFlag
    FeatureFlag FeatureFlag

    View Slide

  60. - With Clear Layering, switching UI at
    runtime become possible
    - Allow Developers to easily validate
    implementation
    - Simplify QA process without several
    build configs
    Runtime UI switching

    View Slide

  61. Custom AndroidViews

    View Slide

  62. Custom Android Views
    - Utilized Interoperability API for fast migration
    - Large amount of custom Android Views are used (for image editing)

    View Slide

  63. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    }

    View Slide

  64. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  65. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  66. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  67. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  68. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }
    Simple?

    View Slide

  69. Custom Android Views
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }
    Actually Not So Simple… $

    View Slide

  70. View Event Does Not Align With Compose
    class TrimmingView {
    var baseMatrix = Matrix()
    };

    View Slide

  71. View Event Does Not Align With Compose
    class TrimmingView {
    var baseMatrix = Matrix()
    };
    private fun loadImageAndBaseMatrixFromViewModel() {
    ui.trimmingView.imageBitmap = editImageViewModel.loadImage()
    ui.trimmingView.baseMatrix = editImageViewModel.baseMatrix
    }

    View Slide

  72. View Event Does Not Align With Compose
    class TrimmingView {
    var baseMatrix = Matrix()
    };
    private fun loadImageAndBaseMatrixFromViewModel() {
    ui.trimmingView.imageBitmap = editImageViewModel.loadImage()
    ui.trimmingView.baseMatrix = editImageViewModel.baseMatrix
    }

    View Slide

  73. View Event Does Not Align With Compose
    class TrimmingView {
    var baseMatrix = Matrix()
    };
    private fun loadImageAndBaseMatrixFromViewModel() {
    ui.trimmingView.imageBitmap = editImageViewModel.loadImage()
    ui.trimmingView.baseMatrix = editImageViewModel.baseMatrix
    }

    View Slide

  74. View Event Does Not Align With Compose
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  75. @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }
    View Event Does Not Align With Compose
    Not Working As Expected… $

    View Slide

  76. View Event Does Not Align With Compose
    class TrimmingView {
    var baseMatrix = Matrix()
    override fun onSizeChanged() {
    super.onSizeChanged()
    resetMatrix()
    }
    }

    View Slide

  77. Anko
    Create View
    OnSizeChanged Reset Matrix
    Set Bitmap
    Set Base Matrix

    View Slide

  78. Anko Compose
    Create View
    Set Bitmap
    Set Base Matrix
    OnSizeChanged Reset Matrix

    View Slide

  79. Anko Compose
    Create View
    Set Bitmap
    Set Base Matrix
    OnSizeChanged Reset Matrix
    Create View
    Data Binding
    Create View
    OnSizeChanged Reset Matrix
    Set Bitmap
    Set Base Matrix
    Update Block

    View Slide

  80. View Event Does Not Align With Compose
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context).apply {
    onSizeChangeListener =
    TrimmingView.OnSizeChangeListener {
    trimmingView.baseMatrix = baseMatrix
    }
    }
    },
    update = { view ->
    view.bitmap = bitmap
    }
    )

    View Slide

  81. View Event Does Not Align With Compose
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context).apply {
    onSizeChangeListener =
    TrimmingView.OnSizeChangeListener {
    trimmingView.baseMatrix = baseMatrix
    }
    }
    },
    update = { view ->
    view.bitmap = bitmap
    }
    )
    class TrimmingView {
    override fun onSizeChanged() {
    super.onSizeChanged()
    resetMatrix()
    onSizeChangeListener.onSizeChanged()
    }
    }

    View Slide

  82. View Event Does Not Align With Compose
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context).apply {
    onSizeChangeListener =
    TrimmingView.OnSizeChangeListener {
    trimmingView.baseMatrix = baseMatrix
    }
    }
    },
    update = { view ->
    view.bitmap = bitmap
    }
    )
    class TrimmingView {
    override fun onSizeChanged() {
    super.onSizeChanged()
    resetMatrix()
    onSizeChangeListener.onSizeChanged()
    }
    }
    Create View
    Set Bitmap
    Set Base Matrix
    OnSizeChanged Reset Matrix

    View Slide

  83. View Event Does Not Align With Compose
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context).apply {
    onSizeChangeListener =
    TrimmingView.OnSizeChangeListener {
    trimmingView.baseMatrix = baseMatrix
    }
    }
    },
    update = { view ->
    view.bitmap = bitmap
    }
    )
    class TrimmingView {
    override fun onSizeChanged() {
    super.onSizeChanged()
    resetMatrix()
    onSizeChangeListener.onSizeChanged()
    }
    }
    Create View
    Set Bitmap
    Set Base Matrix
    OnSizeChanged Reset Matrix

    View Slide

  84. Android View Internal State Restoration
    @Composable
    fun SampleComposable(
    modifier: Modifier = Modifier,
    ) {
    val stateToPreserved = rememberSaveable {
    }
    }

    View Slide

  85. Android View Internal State Restoration
    @Composable
    fun SampleComposable(
    modifier: Modifier = Modifier,
    ) {
    val stateToPreserved = rememberSaveable {
    }
    }
    Will be restored when recreation

    View Slide

  86. Android View Internal State Restoration
    class TrimmingView {
    var baseMatrix;=;Matrix();
    };

    View Slide

  87. Android View Internal State Restoration
    class TrimmingView {
    var baseMatrix;=;Matrix();
    val transformMatrix = Matrix()
    };

    View Slide

  88. Android View Internal State Restoration
    class TrimmingView {
    val transformMatrix = Matrix()
    override fun onSaveInstanceState(): Parcelable {
    val parentState: Parcelable? = super.onSaveInstanceState()
    return bundleOf(
    KEY_TRANSFORM_MATRIX to transformMatrix.getValues(),
    KEY_PARENT_STATE to parentState
    )
    }
    override fun onRestoreInstanceState(state: Parcelable?) {
    // …
    val transform = (state as Bundle).getFloatArray(KEY_TRANSFORM_MATRIX)
    transformMatrix.setValues(transform)
    // …
    }
    };

    View Slide

  89. Android View Internal State Restoration
    @Composable
    private fun TrimmingComposable(
    bitmap: Bitmap,
    baseMatrix: Matrix,
    modifier: Modifier = Modifier
    ) {
    AndroidView(
    modifier = modifier,
    factory = {
    TrimmingView(context)
    .apply {
    id = R.id.trimming_view
    // …
    }
    },
    update = { view ->
    view.bitmap = bitmap
    view.baseMatrix = baseMatrix
    }
    )
    }

    View Slide

  90. What’s happening next?
    Release Strategy and
    Feature Work

    View Slide

  91. Developer Experience x User Experience
    Migration is Inevitable
    What if…?
    User get
    Affected "
    Migration Side Effect

    Migration
    Started for
    Developer Needs

    View Slide

  92. Release Strategy
    - Release feature-by-feature

    View Slide

  93. Release Strategy
    - Outage monitoring
    - Release feature-by-feature

    View Slide

  94. Release Strategy
    - Outage monitoring
    - Fix issue or Quick rollback with feature flag
    - Release feature-by-feature

    View Slide

  95. Future Works
    Remove Anko
    completely
    Replace
    Fragment-based
    structure
    Release Remove Improve
    Implement

    View Slide

  96. The Technical Debt
    - diff Anko-Layout Android-Support-Library
    - Bottleneck for introducing new-comers
    - Anko Layout is lack of UI Preview feature

    View Slide

  97. The Technical Debt
    - diff Anko-Layout Android-Support-Library
    - Bottleneck for introducing new-comers
    - Anko Layout is lack of UI Preview feature
    - Compose Preview

    View Slide

  98. The Technical Debt
    - diff Anko-Layout Android-Support-Library
    - Jetpack Library Support
    - Bottleneck for introducing new-comers
    - Anko Layout is lack of UI Preview feature
    - Compose Preview

    View Slide

  99. The Technical Debt
    - diff Anko-Layout Android-Support-Library
    - Jetpack Library Support
    - Bottleneck for introducing new-comers
    - Spotlighted topic in Android community
    - Anko Layout is lack of UI Preview feature
    - Compose Preview

    View Slide

  100. Migration is Inevitable
    - Developers are the main beneficiary of the migration, but we should keep
    user in mind
    We should…

    View Slide

  101. Migration is Inevitable
    - Plan from the beginning and plan for the worst case
    - Developers are the main beneficiary of the migration, but we should keep
    user in mind
    We should…

    View Slide

  102. Migration is Inevitable
    - Plan from the beginning and plan for the worst case
    - Focus on the goal as a team
    - Developers are the main beneficiary of the migration, but we should keep
    user in mind
    We should…

    View Slide

  103. Migration is Inevitable

    View Slide

  104. Migration is Tough "
    - Tidying up Fragment class with 1000+ lines of code %
    - Facing bug with no previous case study &
    - Trying to understand mysterious legacy code $

    View Slide

  105. Migration is Tough "

    View Slide

  106. Migration Can Also Be … Fun! '
    - Developers get their hands on the newest technology

    View Slide

  107. Migration Can Also Be … Fun! '
    - Knowledge sharing between developers has become more proactive
    - Developers get their hands on the newest technology

    View Slide

  108. Migration Can Also Be … Fun! '
    - Knowledge sharing between developers has become more proactive
    - Enjoy the Challenges
    - Developers get their hands on the newest technology

    View Slide

  109. Thank you

    View Slide