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

November 18, 2022
Tweet

More Decks by Tech-Verse2022

Other Decks in Technology

Transcript

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

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

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

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

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

    write dynamic Android layouts with KotlinaDSL, by JetBrains
  9. Anko Layout linearLayout { id = R.id.container gravity = Gravity.CENTER

    backgroundColor = R.color.background themedTextView(R.style.CommonButtonStyle) { textResource = R.string.common_done } }
  10. 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() } } }
  11. 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() } } } }
  12. - Presentation Logic in UI code - More Flexible than

    XML - Fast Performance Write UI with Anko
  13. The Technical Debt As we decided to continue to self-maintain

    Anko Layout - Anko Layout is lack of UI Preview feature
  14. 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
  15. 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
  16. 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
  17. Migration is Inevitable What if…? User get Affected " Migration

    Side Effect ❌ Migration Started for Developer Needs Developer Experience x User Experience
  18. 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
  19. Migration Goal Keep in mind what the most important goal

    is For Maintainability Replace Anko
  20. 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
  21. 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
  22. 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
  23. Migration Plan Current Phase Refactor and recreate the same UI

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

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

    in older features - Massive Usage of Custom Android View - Fragment Based Navigation
  27. 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
  28. - Presentation Logic in UI code - More Flexible than

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

    XML - Fast Performance Write UI with Anko
  30. Refactor for Clean Architecture Data Layer UI Layer UI Elements

    StateHolder/ViewModel App Data UI State (Processed Data) UI Event
  31. 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 } } } }
  32. 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 } } } }
  33. 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)) } }
  34. 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)) } }
  35. 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)) } }
  36. TO BE Activity Fragment AnkoView Fragment Fragment AnkoView AnkoView ComposeView

    ComposeView ComposeView FeatureFlag FeatureFlag FeatureFlag
  37. - 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
  38. Custom Android Views - Utilized Interoperability API for fast migration

    - Large amount of custom Android Views are used (for image editing)
  39. 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 } ) }
  40. 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 } ) }
  41. 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 } ) }
  42. 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 } ) }
  43. 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?
  44. 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… $
  45. 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 }
  46. 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 }
  47. 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 }
  48. 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 } ) }
  49. @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… $
  50. View Event Does Not Align With Compose class TrimmingView {

    var baseMatrix = Matrix() override fun onSizeChanged() { super.onSizeChanged() resetMatrix() } }
  51. 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
  52. 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 } )
  53. 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() } }
  54. 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
  55. 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
  56. Android View Internal State Restoration @Composable fun SampleComposable( modifier: Modifier

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

    = Modifier, ) { val stateToPreserved = rememberSaveable { } } Will be restored when recreation
  58. 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) // … } };
  59. 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 } ) }
  60. Developer Experience x User Experience Migration is Inevitable What if…?

    User get Affected " Migration Side Effect ❌ Migration Started for Developer Needs
  61. Release Strategy - Outage monitoring - Fix issue or Quick

    rollback with feature flag - Release feature-by-feature
  62. The Technical Debt - diff Anko-Layout Android-Support-Library - Bottleneck for

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

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

    the migration, but we should keep user in mind We should…
  67. 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…
  68. 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…
  69. 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 $
  70. Migration Can Also Be … Fun! ' - Developers get

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

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

    between developers has become more proactive - Enjoy the Challenges - Developers get their hands on the newest technology