Framework size
You might be wondering why the Nutrient .aar
file is 31 MB.
The Nutrient SDK covers most of the PDF specification, which contains thousands of pages, making it quite complex. Nutrient includes a complete PDF renderer, cryptography, and many UI components. This results in a lot of code and, as such, a sizable binary, although there are certain factors that make it appear larger than it actually is. We’re working hard to ensure the framework size stays as low as possible.
Method count
Nutrient builds upon mature and widely used third-party and open source software. Version 4.5.1 of Nutrient has a total of 23,000 methods (including references). Since most apps already ship with the Android support library and RxJava, the actual number of additional methods when using Nutrient is usually smaller.
Dex method limit
The Android dex format has a major flaw, in that an app with a single dex file can only have a maximum of 65,536 method references. While this limit is hardly reachable for an app on its own, it’s very likely that your app’s method count exceeds this limit when adding several third-party dependencies — for example, Google’s support libraries, HTTP/Rest libraries, or Nutrient. If your app hits this limit (and you did not set any precautions) it’s likely you’ll see following error while trying to build your app:
Unable to execute dex: method ID not in [0, 0xffff]: 65536 Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
The message above is created by the dx
tool, which is part of the Android Build Tools and is responsible for converting your Java classes to Android’s dex format.
ProGuarding
One technique to avoid hitting this limit is to enable ProGuard for development builds. ProGuard can detect unused methods and remove them (this is called minification), thereby lowering the total method count of your app. You can enable ProGuard minification inside your app’s build.gradle
file:
android { buildTypes { debug { minifyEnabled true // By using a special debug ProGuard file, you can turn off obfuscation, which would // otherwise hinder debugging. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard-rules-debug.pro' } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }
Now create the file app/proguard-rules-debug.pro
and add the following code, which will turn off the obfuscator:
-dontobfuscate
This minification technique requires you to have already configured the main `proguard-rules.pro` file of your app, according to the integration guide.
Multidex
Originally, Android only supported applications with a single dex file. Newer versions of Android (starting with API 21) support multiple dex files, which triages the issue. For devices prior to API 21, there is the multidex
support library, which allows loading classes from different dex files packed into your app’s APK. The Google Developer document has a comprehensive guide on enabling and configuring multidex for your app.
While multidex is a good solution for increasing the method limit on all newer devices, it can cause performance issues and other problems on devices prior to Lollipop. As such, we recommend using ProGuard and manual authoring of your app’s dependencies to lower the method count, and only using multidex as the second choice.
Reducing the size of your app
If you have extremely low APK size limits, you may decide to only ship ARMv7 native binaries without ARM64 or x86. This will force ARMv8 devices to load the ARMv7 binaries and X86 devices to load ARMv7 binaries via the libhoudini layer.
Be aware that this will cause significant performance penalties and possible bugs on devices with x86 CPUs. ARMv8 devices like the Samsung Galaxy S7, the Nexus 5X/6P, and similar will also show slower performance.
To enable ABI filtering, you need to explicitly define the set of ABIs that you would like to include in your final APK:
android { defaultConfig { // This will strip x86 and arm64-v8a binaries from your APK. ndk.abiFilters = ["armeabi-v7a"] } }
ABI split
Another technique to reduce the overall download size of your final APK is by using ABI splits. ABI splits require you to prepare both your build setup and your Google Play entry (if you are publishing via Google Play) by doing the following:
-
Activate and configure the APK split for ABIs. The process is described in the official Android Gradle plugin documentation.
-
Enable multiple APK support inside the Google Play Developer Console, as described here.
Android App Bundles
Android App Bundles were introduced during Google I/O 2018 and provide automated modularization of your distributed app, which can yield much smaller APK download and install sizes than what was possible before. Our own trials with PDF Viewer showed an average download size reduction of 50 percent for most users. However, the final number depends upon the specific app and needs to be evaluated on a per case basis.
Moreover, App Bundles allow for dynamic distribution of features inside your app, which can be used to load “secondary features” on demand, rather than installing them with the initial APK. An in-depth guide about Android App Bundles can be found at the official Android App Bundle documentation.
Third-party libraries
RxJava
Internally, we make heavy use of the popular RxJava and its Android adjunct RxAndroid for structuring our code, coordinating asynchronous operations, and — most importantly — performing secure and stable multithreading and scheduling. Also, many of the public API methods of Nutrient are available in two flavors: a blocking call, directly returning the requested result; and an asynchronous version of the same method, returning an RxJava Observable
, which is non-blocking and returns the result as soon as it is available.
Here’s an example snippet that searches a document in a non-blocking way. Search is performed on a background thread, while the results are published on the main thread, allowing you to simply update the UI:
// Perform an asynchronous search on a computation thread and update the UI on the main thread. val searchDisposable = search.performSearchAsync(query) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { nextResult -> // This will be called once for every `SearchResult` object. // Put your search result handling here. }
// Perform an asynchronous search on a computation thread and update the UI on the main thread. final Disposable searchDisposable = textSearch.performSearchAsync(query) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<SearchResult>() { @Override public void accept(SearchResult nextResult) { // This will be called once for every `SearchResult` object. // Put your search result handling here. } });
Keep an eye open for
*Async()
methods, which usually return an RxJavaObservable
orFlowable
.
RxJava is very popular for Android development (CodePath lists it as one of the recommended advanced libraries) and is supported by many renown libraries. Due to the generic nature of RxJava, its API can be used throughout your whole app without the need to declare a callback type for every asynchronous method. Moreover, it allows connecting your own asynchronous code with code of any third-party library (like Nutrient) as long as this library also supports RxJava.
Rendering PDF documents
Rendering PDF documents is rather complex. We have a separate guide explaining some of the challenges.