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. None
  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
  3. Agenda - About LINE Sticker Maker - Tech Stack and

    Debt - Migration Plan - Obstacles and Resolution - Release Strategy
  4. About Me Po-Hao Chen (Po) - Android Engineer at LINE

    Fukuoka - Project - LINE Wallet - LINE Sticker Maker
  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
  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
  7. Tech Stack - Kotlin - Anko Layout - RxJava -

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

    Coroutine - Realm - Retrofit - OkHttp - Hilt !!
  9. What is Anko Layout A fast and type-safe way to

    write dynamic Android layouts with KotlinaDSL, by JetBrains
  10. Anko Layout linearLayout { }

  11. Anko Layout linearLayout { id = R.id.container gravity = Gravity.CENTER

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

    backgroundColor = R.color.background themedTextView(R.style.CommonButtonStyle) { textResource = R.string.common_done } }
  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() } } }
  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() } } } }
  15. Write UI with Anko - Fast Performance

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

    UI with Anko
  17. - Presentation Logic in UI code - More Flexible than

    XML - Fast Performance Write UI with Anko
  18. However, in 2019…

  19. The Team Decided to Keep Using it

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

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

    The Technical Debt
  22. The Technical Debt As we decided to continue to self-maintain

    Anko Layout - Anko Layout is lack of UI Preview feature
  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
  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
  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
  26. Migration is Inevitable

  27. Migration is Inevitable Migration Started for Developer Needs

  28. Migration is Inevitable What if…? Migration Started for Developer Needs

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

    Side Effect ❌ Migration Started for Developer Needs Developer Experience x User Experience
  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
  31. Strategy for Continuous Migration Let the Migration Begins

  32. Migration Goal Keep in mind what the most important goal

    is For Maintainability Replace Anko
  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
  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
  35. Migration Plan Release Remove Improve Implement

  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
  37. Migration Plan Current Phase Refactor and recreate the same UI

    in Compose After QA, enable Compose in release build Release Remove Improve Implement
  38. LINE Sticker Maker Code Structure Feature-based Multi-Module - Features are

    independent from each other :app :feature:a :feature:b :feature:c :core
  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
  40. Obstacles for Migration - Mixing Layer between data and UI

    in older features - Massive Usage of Custom Android View - Fragment Based Navigation
  41. Yes or No Fragments?

  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
  43. Mixing of Layers

  44. - Presentation Logic in UI code - More Flexible than

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

    XML - Fast Performance Write UI with Anko
  46. Current Situation UI Anko Data Provider Data Provider . .

    .
  47. Refactor for Clean Architecture Data Layer UI Layer UI Elements

    StateHolder/ViewModel App Data UI State (Processed Data) UI Event
  48. Current Situation UI Anko Data Provider Data Provider . .

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

    . ViewModel UI Implementation
  50. .uiState Isolated Ui Data class SampleViewModel() { val uiState: StateFlow<UiState>

    }
  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 } } } }
  52. Next Step: Migrate to Compose UI Anko ViewModel Compose Swap

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

  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 } } } }
  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)) } }
  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)) } }
  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)) } }
  58. AS IS Activity Fragment AnkoView Fragment Fragment AnkoView AnkoView

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

    ComposeView ComposeView FeatureFlag FeatureFlag FeatureFlag
  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
  61. Custom AndroidViews

  62. Custom Android Views - Utilized Interoperability API for fast migration

    - Large amount of custom Android Views are used (for image editing)
  63. Custom Android Views @Composable private fun TrimmingComposable( bitmap: Bitmap, baseMatrix:

    Matrix, modifier: Modifier = Modifier ) { }
  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 } ) }
  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 } ) }
  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 } ) }
  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 } ) }
  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?
  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… $
  70. View Event Does Not Align With Compose class TrimmingView {

    var baseMatrix = Matrix() };
  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 }
  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 }
  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 }
  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 } ) }
  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… $
  76. View Event Does Not Align With Compose class TrimmingView {

    var baseMatrix = Matrix() override fun onSizeChanged() { super.onSizeChanged() resetMatrix() } }
  77. Anko Create View OnSizeChanged Reset Matrix Set Bitmap Set Base

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

    Reset Matrix
  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
  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 } )
  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() } }
  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
  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
  84. Android View Internal State Restoration @Composable fun SampleComposable( modifier: Modifier

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

    = Modifier, ) { val stateToPreserved = rememberSaveable { } } Will be restored when recreation
  86. Android View Internal State Restoration class TrimmingView { var baseMatrix;=;Matrix();

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

    val transformMatrix = Matrix() };
  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) // … } };
  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 } ) }
  90. What’s happening next? Release Strategy and Feature Work

  91. Developer Experience x User Experience Migration is Inevitable What if…?

    User get Affected " Migration Side Effect ❌ Migration Started for Developer Needs
  92. Release Strategy - Release feature-by-feature

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

  94. Release Strategy - Outage monitoring - Fix issue or Quick

    rollback with feature flag - Release feature-by-feature
  95. Future Works Remove Anko completely Replace Fragment-based structure Release Remove

    Improve Implement
  96. The Technical Debt - diff Anko-Layout Android-Support-Library - Bottleneck for

    introducing new-comers - Anko Layout is lack of UI Preview feature
  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
  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
  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
  100. Migration is Inevitable - Developers are the main beneficiary of

    the migration, but we should keep user in mind We should…
  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…
  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…
  103. Migration is Inevitable

  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 $
  105. Migration is Tough "

  106. Migration Can Also Be … Fun! ' - Developers get

    their hands on the newest technology
  107. Migration Can Also Be … Fun! ' - Knowledge sharing

    between developers has become more proactive - Developers get their hands on the newest technology
  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
  109. Thank you