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

Retour d'expérience sur la modernisation technique d'une Top App

Retour d'expérience sur la modernisation technique d'une Top App

Toute application logicielle, lorsqu'elle atteint une certaine taille, se retrouve remplie d'anti patterns de code smell et de technologies obsolètes. Cette présentation a pour but de comprendre les problèmes les plus fréquemment rencontrés au sein des applications Android, d'en évaluer les enjeux et dee les résoudre grâce à des technologies, librairies et moyens plus modernes. Cependant, la résolution de ces problèmes et l'intégration de nouvelles solutions peuvent être délicats lorsque l'application impact des millions d'utilisateurs. Chez Leboncoin, nous vivons cette modernisation technique au quotidien et en avons fait un enjeu majeur pour notre équipe mobile. Nous allons vous présenter notre retour d'expérience ainsi que les stratégies adoptées au cours de la modernisation de notre application sur ces dernières années

C0e29dade6ec33081689ac135a269b45?s=128

Cristian Garrido

April 24, 2019
Tweet

Transcript

  1. Retour d’expérience sur la modernisation technique d’une Top App

  2. Cristian Garrido Eliott Lujan cristian.garrido@schibsted.com @crgarridos eliott.lujan@schibsted.com

  3. ~1 000 Employees 14 Android developers 17M+ Downloads 180 000+

    LoC (55% / 45% ) 1st French marketplace
  4. The App • Hard to maintain • Slow feature delivery

    • GodActivities • Outdated patterns • No global architecture • “Unfixable” bugs • Long build times
  5. Actions • Conventions • Modern patterns • Modern technologies •

    Modern architecture
  6. GodActivity

  7. GodActivity (often known as MainActivity) • Context • Lifecycle •

    Navigation • Fragments • Deeplink • Presentation • Network Requests • Persistence • ...
  8. public class MainActivity extends BaseActivity implements NavigationListener, AdSelectionListener, AdDetailListener, RegionSelectionListener,

    DismissFragmentRequestListener, FragmentListener, LoadingListener, AdvancedSearchSelectionListener, SubmitSelectedFeaturesForPaymentListener, MessageDialogInvocationListener, AdvancedSearchLaunchListener, ErrorFragmentListener, SavedSearchFragmentListener, NavigationAdditionnalDataListener, AdInsertionListener, InfosSelectionListener, PasswordCheckListener, LostPasswordListener, AdInsertionDataListener, LoginListener, LoginPageListener, BusinessService.RestartListener, TwoPaneLayoutRequestListener, ListingLoadingListener, PaymentPageListener, RatePopupListener, CheckCitiesDatabaseListener
  9. Composition over inheritance

  10. class BaseActivity : AppCompatActivity() { ... } class InjectedActivity :

    BaseActivity() { ... } class DrawerActivity : InjectedActivity() { ... } class MainActivity : DrawerActivity() { ... }
  11. class MainActivity : BaseActivity() { private lateinit var injector: Injector

    private lateinit var drawerDelegate: DrawerDelegate override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } }
  12. class MainActivity : BaseActivity() { private lateinit var injector: Injector

    private lateinit var drawerDelegate: DrawerDelegate override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injector.inject(this) drawerDelegate.setup(drawerLayout) } }
  13. class MainActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) injector.inject(this) drawerDelegate.setup(drawerLayout) dualPaneDelegate.setup(this) navigationController.setup() permissionHandler.setup(this) presenter.attach(this) ... } }
  14. Android Lifecycle Library

  15. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) lifecycle.addObserver(drawerDelegate) } }
  16. class DrawerDelegate : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun setup() { ...

    } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun release() { ... } }
  17. MVP Pattern • Big step for us • Mature pattern

    • Stateful implementation
  18. MVP Pattern • App in background • Configuration changes •

    Boilerplate
  19. MVVM + LiveData

  20. MVVM + LiveData • App in background • Configuration changes

    • Boilerplate
  21. MVVM + LiveData • App in background • Configuration changes

    • Boilerplate
  22. MVVM + LiveData • App in background • Configuration changes

    • Boilerplate
  23. MVVM + LiveData • App in background • Configuration changes

    • Boilerplate • Process restoration
  24. MVVM + LiveData https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate • App in background • Configuration

    changes • Boilerplate • Process restoration
  25. Sealed Classes

  26. class ViewModelExample : ViewModel() { val results = LiveData<Result>() sealed

    class Result { object Loading : Result() object Success : Result() class Failure(val message: String) : Result() } }
  27. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.results.observeNotNull(this) { return when

    (it) { is Loading → showLoading() is Success → showSuccess() is Failure → showFailure(it.message) } } }
  28. DataBinding

  29. with(reservation) { with(ad) { if (photoUrl.isBlank()) { bookingReservationAdImage.setBackgroundResource(R.color.common_grey_medium) bookingReservationAdImage.setPlaceHolderImage(R.drawable.common_ic_no_picture_white_80dp) }

    else bookingReservationAdImage.setImageURI(photoUrl) bookingReservationAdTitle.text = title bookingReservationAdLocation.text = location bookingReservationAdCapacity.visibility = if (hasCapacityAndRoomsCount) VISIBLE else GONE bookingReservationAdCapacity.text = resources.getQuantityString(R.plurals.booking_reservation_capacity_label, roomsCount) bookingReservationHostName.text = authorName showDateTitle(nightsCount, location) } bookingReservationCheckInValue.text = dateFormatter.format(checkIn.time) bookingReservationCheckOutValue.text = dateFormatter.format(checkOut.time) bookingReservationAdultsCount.text = adultsCount.toString() bookingReservationAdultsCount.setTextColor(if (adultsCount > 0) orangeTextColor else blackTextColor) bookingReservationChildrenCount.text = childrenCount.toString() bookingReservationChildrenCount.setTextColor(if (childrenCount > 0) orangeTextColor else blackTextColor) bookingReservationChildrenRemoveImage.isEnabled = (childrenCount > MIN_CHILDREN_COUNT) bookingReservationChildrenAddImage.isEnabled = (childrenCount < MAX_PEOPLE_COUNT) }
  30. <data> <variable name="reservation" type="fr.leboncoin.bookingreservation.models.Reservation"/> </data> <TextView android:id="@+id/bookingReservationCount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{

    String.valueOf(reservation.count) }" android:textColor="@{ reservation.count > 0 ? @color/orange : @color/black }" /> DataBinding
  31. <data> <variable name="reservation" type="fr.leboncoin.bookingreservation.models.Reservation"/> </data> <TextView android:id="@+id/bookingReservationCount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{

    String.valueOf(reservation.count) }" android:textColor="@{ reservation.count > 0 ? @color/orange : @color/black }" /> DataBinding
  32. • Compilation errors DataBinding

  33. • Compilation errors • Separation of concerns DataBinding

  34. DataBinding • Compilation errors • Separation of concerns • Divided

    code
  35. DataBinding • Compilation errors • Separation of concerns • Divided

    code • Compilation time
  36. CustomView

  37. class ProfileCardView : CardView { init { inflate(context, R.layout.profile_card, this)

    } fun setProfile(profile: UserProfile) { nameTextView.text = profile.name adsCountTextView.text = profile.adsCount.toString() verifiedBadgeView.isVisible = profile.isVerified } }
  38. class ProfileCardView : CardView { init { inflate(context, R.layout.profile_card, this)

    } fun setProfile(profile: UserProfile) { nameTextView.text = profile.name adsCountTextView.text = profile.adsCount.toString() verifiedBadgeView.isVisible = profile.isVerified } }
  39. Repositories

  40. Command processor

  41. Command processor

  42. Command processor

  43. Command processor

  44. Command processor

  45. Command processor

  46. Command processor

  47. Command processor • Hard to debug • Require to write

    a lot of code • Inheritance & responsabilities • Response broadcasted with EventBus
  48. Solution • Retrofit + RxJava

  49. Solution • Retrofit + RxJava • Isolation

  50. Solution • Retrofit + RxJava • Isolation • Wrapper

  51. None
  52. AdRepository @Deprecated OldAdRepository

  53. class AdRepository @Inject constructor(oldAdRepository: OldAdRepository, eventBus: EventBus)

  54. class AdRepository @Inject constructor(oldAdRepository: OldAdRepository, eventBus: EventBus){ fun fetchAd(id: String):

    Single<Ad> { oldAdRepository.fetchAd(id) } }
  55. class AdRepository @Inject constructor(oldAdRepository: OldAdRepository, eventBus: EventBus){ private val adProcessor

    = PublishProcessor.create<Ad>() fun fetchAd(id: String): Single<Ad> { oldAdRepository.fetchAd(id) return Single.fromPublisher(adProcessor.take(1)) } @Subscribe fun onFetchAdSuccess(event: FetchAdSuccessEvent) { adProcessor.onNext(event.ad) } }
  56. CommonRepository • Legacy code • Contains all kind of information

    • Bones of our app
  57. class SplashScreenActivity: AppCompatActivity() { @Inject lateinit var commonRepository: CommonRepository override

    fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) commonRepository.setData(data) .setOtherData(otherData) .build() } }
  58. class SplashScreenActivity: AppCompatActivity() { @Inject lateinit var commonRepository: CommonRepository override

    fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) commonRepository.setData(data) .setOtherData(otherData) .build() } }
  59. What we thought of • Move initialization to App

  60. What we thought of • Move initialization to App

  61. What we thought of • Move initialization to App •

    Move initialization to every activities
  62. What we thought of • Move initialization to App •

    Move initialization to every activities
  63. What we thought of • Move initialization to App •

    Move initialization to every activities • Build itself
  64. What we thought of • Move initialization to App •

    Move initialization to every activities • Build itself
  65. What we thought of • Move initialization to App •

    Move initialization to every activities • Build itself • App restart
  66. Long term solutions • Avoid using CommonRepository • Every Activity

    is an entry point • Avoid Singletons • Use persistence • Divide to conquer
  67. Architecture

  68. None
  69. None
  70. None
  71. sealed class UserResult { class Success(val user: User) : UserResult()

    class Failure(val exception: Exception) : UserResult() } val userResults = MutableLiveData<UserResult>()
  72. disposables += useCase.fetchUser(id) .subscribe({ userResults.postValue(Success(it)) }, { userResults.postValue(Failure(it)) })

  73. disposables += useCase.fetchUser(id) .subscribe({ userResults.postValue(Success(it)) }, { userResults.postValue(Error(it)) }) User

    from Domain
  74. None
  75. None
  76. Package structure

  77. Package structure

  78. GodPOJO • Huge list of properties • Parcelization / Serialization

    • Test maintenance • Business logic inside
  79. GoodPOJO @Parcelize data class Ad( val id: String, val title:

    String, val location: String, val numberImages: Int?, val thumbnailUrl: String?, val priceList: List<Int> ) : Parcelable
  80. GoodPOJO

  81. GoodPOJO

  82. GoodPOJO

  83. GoodPOJO

  84. Retrospective Monolith → 28 Modules Build time ½ Delivery Speed

    x2 Super happy
  85. Tests

  86. What’s upon us ?

  87. Chapter

  88. Chapter

  89. Spike

  90. Documentation Links • https://github.com/nickbutcher/plaid • https://developer.android.com/topic/libraries/architecture • https://proandroiddev.com/using-dagger-in-a-multi-module-project • https://antonioleiva.com/clean-architecture-android/

    • https://www.leadingagile.com/spikes
  91. Merci

  92. Merci, on recrute !