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

Cristian Garrido

April 24, 2019
Tweet

More Decks by Cristian Garrido

Other Decks in Programming

Transcript

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

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

    • GodActivities • Outdated patterns • No global architecture • “Unfixable” bugs • Long build times
  3. GodActivity (often known as MainActivity) • Context • Lifecycle •

    Navigation • Fragments • Deeplink • Presentation • Network Requests • Persistence • ...
  4. 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
  5. class BaseActivity : AppCompatActivity() { ... } class InjectedActivity :

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

    private lateinit var drawerDelegate: DrawerDelegate override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } }
  7. 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) } }
  8. 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) ... } }
  9. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

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

    } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun release() { ... } }
  11. MVVM + LiveData • App in background • Configuration changes

    • Boilerplate • Process restoration
  12. class ViewModelExample : ViewModel() { val results = LiveData<Result>() sealed

    class Result { object Loading : Result() object Success : Result() class Failure(val message: String) : Result() } }
  13. 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) } } }
  14. 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) }
  15. 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 } }
  16. 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 } }
  17. Command processor • Hard to debug • Require to write

    a lot of code • Inheritance & responsabilities • Response broadcasted with EventBus
  18. 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) } }
  19. class SplashScreenActivity: AppCompatActivity() { @Inject lateinit var commonRepository: CommonRepository override

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

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

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

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

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

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

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

    is an entry point • Avoid Singletons • Use persistence • Divide to conquer
  27. sealed class UserResult { class Success(val user: User) : UserResult()

    class Failure(val exception: Exception) : UserResult() } val userResults = MutableLiveData<UserResult>()
  28. GodPOJO • Huge list of properties • Parcelization / Serialization

    • Test maintenance • Business logic inside
  29. 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