Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Anko Layout linearLayout { }

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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() } } }

Slide 14

Slide 14 text

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() } } } }

Slide 15

Slide 15 text

Write UI with Anko - Fast Performance

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

However, in 2019…

Slide 19

Slide 19 text

The Team Decided to Keep Using it

Slide 20

Slide 20 text

The Team Decided to Keep Using it 2 Years Later…

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Migration is Inevitable

Slide 27

Slide 27 text

Migration is Inevitable Migration Started for Developer Needs

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Migration is Inevitable What if…? User get Affected " Migration Side Effect ❌ Migration Started for Developer Needs Developer Experience x User Experience

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Strategy for Continuous Migration Let the Migration Begins

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Migration Plan Release Remove Improve Implement

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Yes or No Fragments?

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Mixing of Layers

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Current Situation UI Anko Data Provider Data Provider . . .

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Current Situation UI Anko Data Provider Data Provider . . .

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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 } } } }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Feature Flag in UI Layer UI Anko Compose ViewModel FeatureFlag

Slide 54

Slide 54 text

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 } } } }

Slide 55

Slide 55 text

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)) } }

Slide 56

Slide 56 text

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)) } }

Slide 57

Slide 57 text

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)) } }

Slide 58

Slide 58 text

AS IS Activity Fragment AnkoView Fragment Fragment AnkoView AnkoView

Slide 59

Slide 59 text

TO BE Activity Fragment AnkoView Fragment Fragment AnkoView AnkoView ComposeView ComposeView ComposeView FeatureFlag FeatureFlag FeatureFlag

Slide 60

Slide 60 text

- 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

Slide 61

Slide 61 text

Custom AndroidViews

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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 } ) }

Slide 65

Slide 65 text

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 } ) }

Slide 66

Slide 66 text

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 } ) }

Slide 67

Slide 67 text

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 } ) }

Slide 68

Slide 68 text

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?

Slide 69

Slide 69 text

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… $

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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 }

Slide 72

Slide 72 text

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 }

Slide 73

Slide 73 text

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 }

Slide 74

Slide 74 text

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 } ) }

Slide 75

Slide 75 text

@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… $

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Anko Create View OnSizeChanged Reset Matrix Set Bitmap Set Base Matrix

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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 } )

Slide 81

Slide 81 text

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() } }

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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) // … } };

Slide 89

Slide 89 text

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 } ) }

Slide 90

Slide 90 text

What’s happening next? Release Strategy and Feature Work

Slide 91

Slide 91 text

Developer Experience x User Experience Migration is Inevitable What if…? User get Affected " Migration Side Effect ❌ Migration Started for Developer Needs

Slide 92

Slide 92 text

Release Strategy - Release feature-by-feature

Slide 93

Slide 93 text

Release Strategy - Outage monitoring - Release feature-by-feature

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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…

Slide 102

Slide 102 text

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…

Slide 103

Slide 103 text

Migration is Inevitable

Slide 104

Slide 104 text

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 $

Slide 105

Slide 105 text

Migration is Tough "

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Thank you