MercadoLibre Android app is on a diet: Dynamic Features — How To

How to implement a Dynamic Feature at Mercado Libre’s scale? What about our architecture? What changed? Learn all these things and much more!

Rodrigo Pintos
Mercado Libre Tech
9 min readNov 23, 2022

--

Read this story in Spanish.

How do we apply Dynamic Features in Mercado Libre?

Welcome back! In this post, we will continue with the second part of our story with Dynamic Features, we hope you are enjoying this process as much as we have.

Remember that to understand this reading, it is necessary that you first go through our first story so as to get a better general understanding of the definition of Dynamic Features, what we can achieve with this tool and what types of dynamic facilities Google offers us, among other things.

With that said, let’s get started.

Before starting development, we asked ourselves some questions that we answered as we progressed with the analysis:

At the architecture level…

Our applications were already divided into modules and from here we proceeded by analyzing the following:

What changes have we had at the level of Mercado Libre & Mercado Pago apps?

At the app level, we begin to have dynamic modules as teams start to get on board with dynamic features. We need to establish a project architecture to make it scalable, so the first step has been to define this architecture at the project level within our applications.

What if we navigate to a dynamic module from a push or email notification or some external form?

We need to create a library that acts as a wrapper to do this handling. In fact, this was the most scalable solution that we’ve found that not only allowed us to control the navigation but also to see other advantages of this wrapper. We’ll be sharing some of our insights shortly.

How does this wrapper work? The flow that we define in our library is executed, validating whether the module is found or not; if found, then the wrapper proceeds to validate it and allows us to download or navigate it if it’s already downloaded”.

What if our module needs some initial configuration before starting?

For this case, we’ve defined an architecture that allows us to inject the necessary configuration on the module once it has been installed inside the wrapper that we’ve created.

At the release process level…

Do these changes modify our current Release process?

No, the process keeps on being the same as it is today; when generating the bundleRelease, the package already contains the dynamic module or modules with their configurations. PlayStore is in charge of installing each module based on its settings, where applicable.

What happens when we update the application?

We need to keep in mind that any update that is uploaded to the store — whether it is an update on a change to the base app or a change that only affects the dynamic module — will always be a bundle of the entire application. Therefore, whether or not the user had a version with dynamic features, they will see the available update.

Reference on delta updates.

At the dependency management level…

What if both the base module and the dynamic module share the same dependency and only one of them is updated?

The version defined in the base app is taken, unless we specify the opposite with some force from the dynamic module, which we should not do in order not to generate inconsistencies in the flow.

What happens in the app when we add more than one dynamic module?

According to Dynamic Features architecture, all dynamic modules depend directly on the base module, in this case our apps.

This can cause problems with dependencies, for example if any of the app modules depends on a library that comes with different versions at the app and dynamic module level.

For these cases, we must force the version of that library in the base module, i.e. in our apps, to avoid these conflicts.

At a general level we were left with…

What if we clear the cache and delete the saved data of our apps?

Nothing would happen, once the dynamic module is downloaded, it is already part of the general application as a whole, so it will continue with the downloaded module and you will not have to download it again.

What happens if we compile by console with assembleRelease or assembleDebug?

For these cases, we will not have the dynamic module since it is not part of the assembly packages. To test it in release, we use bundleRelease, and to use it in debug we use bundleDebug.

After answering these questions, we started with a POC in 2018/19 where in order to migrate a module to dynamic we obtained the following conclusiones:

  • The external resources used within the dynamic module must be referenced with the full path, otherwise the application will not find them at runtime making it crash.
  • It is an implementation that, although it can be adapted to other stores, requires much more work, so today we use the implementation only adapted to Google PlayStore.
  • For external navigation, the only possible handling is to intercept it and handle the installation/navigation to the module through a wrapper.
  • At first, the uninstallation of the module did not work, and although it works today, it is not something we can control; we do not know when it happens nor do we have a callback of the event in question.

We advanced in the development and improvements of some of these points in conjunction with Google in order to get closer to the possibility of carrying out a productive development of this feature. Further, while we left the feedback to Google the initiative remained on standby.

We resumed development in 2020 since some of the points mentioned above had been resolved in the interim and we went on to define when we should have a dynamic module.

When should our module be a Dynamic Feature?

It’s important to understand when our flow should be part of a dynamic flow and when it shouldn’t. There are a few factors that help us determine when we should have a flow run on demand by the user or run on demand in the background at some specific time based on some action.

Always keep in mind that the flow must be modularized.

General Conditions

We detail some general conditions to understand when our flow should be dynamic for a better experience:

  • If our flow is not enabled for all countries of the application,
  • If our flow is turned on/off from the backend and is not always available but under certain conditions,
  • If our flow has very little use by the end user (register in the app) or something that does not run many times,
  • If our flow needs third-party dependencies that add a lot of weight when integrating with the main app (X%).

How has our architecture changed?

At this time, we made the decision to bring a small module as the first interaction with Dynamic Features in a productive way, using the onDemand installation type.

  • For this, our application went from being a dependency container to having “separate” dynamic modules, generating a dependency from the dynamic module to the base application, and leaving a reference within the base app to the dynamic module.

We went from this architecture:

To this other one:

We also developed the wrapper that we mentioned at the beginning, which gave us the following benefits:

  • We can have control over the navigation that we want to manage, both internal and external, for a better experience in general.
  • Both internal and external navigation is resolved in the Dynamic Features wrapper that we have, which is in charge of validating if we need to download the module, if we already have it downloaded and just navigate, if the module to download exists, in addition to maintaining backward compatibility with the “old” deep links so that teams don’t need to migrate input to their current streams to make their module dynamic, among other things.
  • We launch modules that need initial configurations when installed.
  • We have the possibility of giving visual feedback to the personalized end user, which gives us the option to iterate and continue improving based on the good practices provided by the official Android documentation and the validations with the UX team.
  • We can keep metrics on various points of our dynamic flow by answering questions such as:
  • How many users download the dynamic module?
  • How many users are actively browsing the dynamic module?
  • How many users have errors downloading a dynamic module?
  • Do we have any distinctions on what type of errors the user can have and if they have recovered from them?
  • How long does it take to download a dynamic module?

At the same time, we wanted to go a little further than what Google offered us, and create a new type of installation that adapts to the flow we have within our applications.

This is how we gave birth to … Background.

This distribution mode has been created by us in a custom way to be able to download a dynamic module without the user receiving feedback on the operation being carried out, and so that the experience is transparent when they need to access it.

After these changes, we needed to test our flow very well to see that everything was still working properly.

From these tests, we were able to obtain more interesting data, based on our background flow being reinvented by us, we mainly saw that we had installation errors when the application was in the background.

In this way, we had to adapt the installations in the background to foreground events from coupling to the life cycle of the activities to the registration flows, ending up opting that the best way to handle it was in conjunction with the injection of configurations that we carried out each time the app woke up, i.e. when it started in the foreground. Thus, we allowed the flows to be installed in the background as part of that process.

How do we test our dynamic module?

We have two options to test our dynamic stream, one is locally with bundletools and the other is through internal app sharing.

For the Local test, you can read the documentation, and for tests with internal app sharing, it is necessary to upload a bundleRelease of our application since the Google console validates that the package is officially signed.

A good tip when you need to debug a dynamic compilation is to upload a bundleDebug with the package id of our application in release (that does not include the .debug). In this way, we can upload the package in debug to internalSharing as well as download it and debug on it.

Summing up

So far, we’ve had no changes in our release process but we had to adapt the project architecture to scale with several dynamic modules defined at app level. At the same time, we designed a scalable wrapper that would allow us to handle the navigation and offer us several advantages over having tracks and dashboards to gain visibility of the stability of the modules part of this experience. Another advantage was having an architecture that enabled initial configurations for the modules that needed it before starting their flows.

We even went a step further to have a new type of background installation that would allow us a greater range when a team needed to join in to be dynamic, thinking about the best experience for the end user.

In conjunction with all this development, we devoted considerable time to testing in order to continue improving the experience, mainly thinking about the teams that would be added to this new initiative, and with the aim of having a scalable solution that remained stable and user-friendly for developers.

How do we continue after this development?

After having our wrapper developed and our first module in production, we moved forward measuring the results and defining our next steps:

  • How did we adapt this new dynamic flow to the internal testing tools we have?
  • How did we do with the weight of the application as the teams added to this experience?
  • What are we currently working on in this initiative?

To find out these answers, we invite you to read the last entry in our series of Dynamic Features and we hope you are enjoying it :)

--

--