Hilt

1 posts

daangn4 min readCurated summary

Solving a 20

Carrot’s short-form video editor expanded its Android module to 200MB, later reduced to 40MB by moving filters and effects to a CDN. Because editing is used by relatively few users and is unavailable globally, the team adopted an on-demand Dynamic Feature Module (DFM) so most users would not install the extra assets. Rather than moving all editor code into the DFM, they ultimately separated only the large native libraries, achieving most of the size reduction with less operational risk. ## The Problem: A Large Video-Editing Module - Video editing introduced filters, effects, stickers, and native SDK resources totaling about 200MB. - CDN delivery reduced the module to 40MB and enabled updates and A/B testing without app releases. - The remaining 40MB was still an unnecessary burden for global users and users who never edit videos. ## Choosing On-demand Dynamic Feature Modules - Android App Bundles support: - **Install-time delivery:** installed with the app. - **On-demand delivery:** downloaded when requested. - **Conditional delivery:** installed based on conditions such as country or API level. - The team selected **On-demand Delivery** so the editor would download only when a user tapped the edit button. - After installation, the module remains local for subsequent use. - The user experience also required handling download progress, network failures, and first-use delays. ## Challenges of Separating the Entire Feature ### Hilt and Dependency Injection - Hilt combines dependency graphs at compile time, but the base and DFM modules are built separately. - DFM annotations such as `@Inject` and `@Module` are therefore unavailable during the base build. - The solution was to expose dependencies from the base module through a Hilt `EntryPoint`. - The DFM then constructs its own Dagger component at runtime. ### SplitCompat - On-demand modules are installed as split APKs. - The default `ClassLoader` may not recognize classes and resources added later. - `SplitCompatApplication` or `SplitCompat.install(this)` is required. - Without it, the app can encounter `ClassNotFoundException` or resource-loading failures. ### Native SO Libraries - DFM native libraries are installed in SplitCompat-managed paths rather than the standard `nativeLibraryDir`. - `SplitInstallHelper.loadLibrary()` may be required instead of `System.loadLibrary()`. - Unmodifiable third-party SDKs may need to remain in the base module. - Base and DFM modules must use identical ABI filters, or bundle creation can fail. ### STL and Native Version Conflicts - Multiple native libraries may share `libc++_shared.so`. - Because Android’s linker follows a “first-loaded wins” rule, the base module’s STL version can be used by DFM libraries. - NDK or STL mismatches can cause symbol conflicts, runtime crashes, or memory corruption. - The team used packaging exclusions and `pickFirsts` rules to control which libraries were included in each module. ### R8 Configuration - Keeping R8 rules only in the DFM caused base-module types to be obfuscated incorrectly. - The team centralized keep rules in the base module and minimized DFM consumer rules. ## Final Architecture: Separate Only the SO Files - Most of the remaining 40MB came from native video-processing, filtering, and rendering libraries. - The editor’s Kotlin/Java code occupied only a few hundred kilobytes. - The team kept feature code in the base module and delivered only the large SO files on demand. ### Benefits - If the DFM download or native library loading fails, the app can show an error instead of crashing. - Local development and debugging remain straightforward. - Separating only native libraries still reduced the module size by more than 90%. ### Trade-offs - Engineers must explicitly manage module installation, library loading, and feature startup. - Keeping code in the base while native binaries reside in the DFM is less intuitive. - The launch flow checks installation, loads the library, and then starts the editor. ## Testing and Operational Practices - Test both installed and uninstalled DFM states. - Validate real download behavior through Play Console internal testing or Internal App Sharing. - Use `bundletool` for local iteration and device-specific APK generation. - Universal APKs can simplify rapid installation and verification. - Test module installation and failure scenarios, not just normal editor execution. The main recommendation is to optimize for stability and maintainability rather than complete architectural separation. For large Android apps, isolating the largest native dependencies with on-demand DFM delivery can protect global users from unnecessary downloads while preserving team autonomy and operational simplicity.

Read original(opens in new tab)