How to Load Heavy Libraries on Splash Screen [the proper way]

In this post I would like to showcase a situation when a developer has an external library which is very slow when initializing. In this case, a developer probably doesn’t want to initialize this library on the main thread because it will freeze the application. Instead, a developer wants to load it in the background and propagate the result on the main thread.

Splash screen

First of all, if you already have some initialization stuff in your custom Application class, you might want to do a proper splash screen. It means that a splash screen should be shown simultaneously after clicking on the application icon. It can be easily achieved by setting the background of your SplashActivity in a theme.

<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
  <item name="android:windowBackground">@drawable/background_splash</item>
</style>

And in your AndroidManifest.xml:

<activity
  android:name=".splash.SplashActivity"
  android:theme="@style/SplashTheme">
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>

Usually, a splash screen is a logo, so this @drawable/background_splash could be a layer-list for example:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:drawable="@android:color/holo_blue_dark"/>

  <item>
    <bitmap
      android:gravity="center"
      android:src="@drawable/ic_hockey_stick"/>
  </item>
</layer-list>

All credits to this implementation go here.

BTW, if you use <vector> asset as a src for your bitmap, be aware of this bug. Unfortunately, there is no workaround for this, sorry. So in case of a splash screen you should use png files for API levels <23.

Initializing the library

Now we have instant start up. What should we do next? Now we should think of a way how we initialize this slow library. Dagger 2 and RxJava to the rescue!

If this ‘long initialization’ library is needed only at splash screen to load some data, then we can define it in the SplashModule, so we will be able to clear all the references to the library after using it.

@Module
public class SplashModule {
  @Provides @NonNull @SplashScope 
  public SplashLibrary splashLibrary() {
    return new SplashLibrary(); // Takes >5 seconds.
  }
}

Right now we cannot @Inject this library anywhere because it will freeze our UI. Instead, we will create an Observable which receives a SplashLibrary instance but still not initialized because we pass a Lazy<> instance of it.

@Module
public class SplashModule {
  // ...

  @Provides @NonNull @SplashScope
  public Observable<SplashLibrary> observable(final Lazy<SplashLibrary> library) {
    return Observable.defer(new Func0<Observable<SplashLibrary>>() {
      @Override public Observable<SplashLibrary> call() {
        return Observable.just(library.get());
      }
    });
  }
}

Injecting the library

Finally, we are able to inject the Observable<SplashLibrary> to our SplashActivity.

/** Observable which will emit an item when fully initialized. */
@Inject Observable<SplashLibrary> splashLibraryObservable;

/** Subscription to unsubscribe in onStop(). */
private Subscription subscription;

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // ...

  subscription = splashLibraryObservable
      // Init library on another thread.
      .subscribeOn(Schedulers.computation())
      // Observe result on the main thread.
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(new Action1<SplashLibrary>() {
        @Override public void call(SplashLibrary splashLibrary) {

          // Use the initialized library.

          Intent intent = new Intent(activity, MainActivity.class);
          startActivity(intent);
        }
      });
  }
}

There are still some pitfalls that one should be aware of:

  1. The possibility of library throwing an exception => we need to implement onError() method.
  2. The possibility of user leaving/rotating the Activity before the library initializes. This possibility leads to memory leaks because our code references Activity in the callback.

Handling errors while initializing a heavy library

In order to address this issue, one can pass an Observer instance to subscribe() method. Pretty easy:

.subscribe(new Observer<SplashLibrary>() {
  final String TAG = "Observer<SplashLibrary>";
  
  @Override public void onCompleted() {  }

  @Override public void onError(Throwable e) {
    Log.d(TAG, "Library init error!", e);
    // Possible UI interaction.
    // ...
    finish();
  }

  @Override public void onNext(SplashLibrary splashLibrary) {
    // ...
    // Use the initialized library.

    Intent intent = new Intent(activity, MainActivity.class);
    startActivity(intent);
    finish();
  }
});

Handling memory leaks if the user leaves the Activity

In this example it’s not enough to just unsubscribe from the Subscription because while the object is initializing, Subscription cannot free resources and that’s why we hold destroyed Activity in memory which causes a memory leak. This can be easily seen in the LogCat if one enables StrictMode.enableDefaults(); in the Application class. When rotating an Activity, StrictMode logs several instances of an Activity.

E/StrictMode: class .SplashActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class .SplashActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(
StrictMode.java:1)

That’s why we need to free the activity referenced in the created Observer. We can do that by creating a static class that implements Observer<SplashActivity>, passing an Activity reference there and then clearing it in onDestroy(). In this way, we can ensure that nothing was leaked.

private static final class OnInitObserver implements Observer<SplashLibrary> {
  @Nullable private SplashActivity splashActivity;

  OnInitObserver(@NonNull SplashActivity splashActivity) {
    this.splashActivity = splashActivity;
  }

  @Override public void onCompleted() { /* ... */ }
  @Override public void onError(Throwable e) { /* ... */ }
  @Override public void onNext(SplashLibrary splashLibrary) { /* ... */ }

  public void releaseListener() {
    splashActivity = null;
  }
}
@Override protected void onDestroy() {
  super.onDestroy();

  onInitObserver.releaseListener();
}

Keeping in mind all these points, one can easily initialize a library, make a network request or do any heavy processing while showing a splash screen.

Thanks for reading! The source code is available here.

In the next post I’ll write about handling rotation while initializing a heavy library.

comments powered by Disqus