Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dagger 2 Save and Restore State when Activity Stops

I'm at an impasse. I'm using Dagger 2 for dependency injection, but I'm losing state when the app goes into the background. Here is the scenario: the app starts up and creates the dependencies. All works perfectly as long as the app stays in the foreground. However, there is a scenario when the app has to go into the background. When it comes back, the values stored in one of my injected classes are lost.

For my injected classes that have no dependencies of their own, everything seems to recover correctly. However, there is one injected class that has an injected dependency, and this is the one that doesn't recover. Here is how I am setting it up:

AppComponent.java

@Singleton
@Component(
    modules = {
        AppModule.class
    }
)

public interface AppComponent {

    SessionKeyExchangerService provideSessionKeyExchangerService();
    AESCipherService provideCipherService();

    void inject(LoginActivity loginActivity);
}

AppModule.java

@Module
public class AppModule {

    @Provides @Singleton
    AESCipherService provideCipherService() {
        return new AESCipherService();
    }

    @Provides @Singleton
    SessionKeyExchangerService provideSessionKeyExchangerService(AESCipherService service) {
        return new SessionKeyExchangerService(service);
    }
}

And then when I go to inject these dependencies, I do it like this:

LoginActivity.java

@Inject 
SessionKeyExchangerService sessionKeyExchangerService;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);

    Injector.INSTANCE.getAppComponent().inject(this);

    if (savedInstanceState != null) {
        sessionKeyExchangerService = savedInstanceState.getParcelable(SESSION_KEY_PARCEL);
        Log.d(Constants.TAG, "session key retrieved in on create: " + sessionKeyExchangerService.getCipherService().getBase64EncodedSessionKey());
    }
}

So, my fundamental question is how to maintain the state of a Dagger 2 injected class. I'm happy to share more code, but this is the essential idea.

Thanks for any help.

EDIT Assuming that what I've done above is OK, let me move on to how I save and retrieve the values stored in those injected objects. This will show that there is a problem somewhere.

When I go into the background, and then come back, I can see that I get a new PID. I can also see that I am able to store and retrieve the injected values correctly in the LoginActivity class. However, other classes that also have a reference to the injected value now have different values meaning that their reference is to a different memory location, right?

My best guess as to where I am going wrong is in LoginActivity onCreate where I am restoring the sessionKeyExchangerService value from the saved parcel. I think I am creating new values that are not recognized across the app as injected dependencies, but I don't know why this is wrong or how to fix it.

This code is also in LoginActivity.java:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(SESSION_KEY_PARCEL, sessionKeyExchangerService);
    Log.d(Constants.TAG, "session key saved: " + sessionKeyExchangerService.getCipherService().getBase64EncodedSessionKey());
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    sessionKeyExchangerService = savedInstanceState.getParcelable(SESSION_KEY_PARCEL);
    Log.d(Constants.TAG, "session key retrieved in on restore state: " + sessionKeyExchangerService.getCipherService().getBase64EncodedSessionKey());
}

Here is the console output that illustrates the issue. Notice 1) how the PID changes after onStop() is called, and 2) how the class Authenticator (which has a reference to AESCipherService has a different session key value:

1398-1398/com.mysite.myapp D/MYTAG﹕ on save instance state
1398-1398/com.mysite.myapp D/MYTAG﹕ session key saved: 93Zuy8B3eos+eCfBQk9ErA==
1398-1398/com.mysite.myapp D/MYTAG﹕ on stop
3562-3562/com.mysite.myapp D/MYTAG﹕ session key retrieved in on create: 93Zuy8B3eos+eCfBQk9ErA==
3562-3562/com.mysite.myapp D/MYTAG﹕ on start
3562-3562/com.mysite.myapp D/MYTAG﹕ session key retrieved in on restore state: 93Zuy8B3eos+eCfBQk9ErA==
3562-3562/com.mysite.myapp D/MYTAG﹕ authenticator class says that the session key is: 28HwdRCjBqH3uFweEAGCdg==

SessionKeyExchangerService.java

protected SessionKeyExchangerService(Parcel in) {
        notifyOn = in.readString();
        sessionKeyExchangeAttempts = in.readInt();
        MAX_SESSION_KEY_EXCHANGE_ATTEMPTS = in.readInt();
        sessionKeyExchangeHasFailed = (in.readByte() == 1);
        cipherService = in.readParcelable(AESCipherService.class.getClassLoader());
    }

    public static final Creator<SessionKeyExchangerService> CREATOR = new Creator<SessionKeyExchangerService>() {
        @Override
        public SessionKeyExchangerService createFromParcel(Parcel in) {
            return new SessionKeyExchangerService(in);
        }

        @Override
        public SessionKeyExchangerService[] newArray(int size) {
            return new SessionKeyExchangerService[size];
        }
    };

@Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(notifyOn);
        dest.writeInt(sessionKeyExchangeAttempts);
        dest.writeInt(MAX_SESSION_KEY_EXCHANGE_ATTEMPTS);
        dest.writeByte((byte) (hasSessionKeyExchangeFailed() ? 1 : 0));
        dest.writeParcelable(cipherService, flags);
    }

AESCipherService.java

protected AESCipherService(Parcel in) {
    sessionKeyBytes = in.createByteArray();
    ivBytes = in.createByteArray();
    sessionId = in.readLong();
    mIsSessionKeyEstablished = (in.readByte() == 1);
    verbose = (in.readByte() == 1);
}

public static final Creator<AESCipherService> CREATOR = new Creator<AESCipherService>() {
    @Override
    public AESCipherService createFromParcel(Parcel in) {
        return new AESCipherService(in);
    }

    @Override
    public AESCipherService[] newArray(int size) {
        return new AESCipherService[size];
    }
};

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeByteArray(sessionKeyBytes);
    dest.writeByteArray(ivBytes);
    dest.writeLong(sessionId);
    dest.writeByte((byte) (isSessionKeyEstablished() ? 1 : 0));
    dest.writeByte((byte) (verbose ? 1 : 0 ));
}
like image 811
AndroidDev Avatar asked Nov 07 '15 20:11

AndroidDev


People also ask

How do I use @assistedinject in dagger?

With AssistedInject we can tell Dagger which constructor argument we want to assist with by annotating the argument with @Assisted annotation and instead of using @Inject, to inject the ViewModel, we will use @AssistedInject. The next step is to make AssistedInject generate the factory for this ViewModel.

How do I use Gradle dependencies with dagger?

First, let’s include the Gradle dependencies: With AssistedInject we can tell Dagger which constructor argument we want to assist with by annotating the argument with @Assisted annotation and instead of using @Inject, to inject the ViewModel, we will use @AssistedInject.

How to save activity state in Android activity?

Activity class has two methods you must override when you want to save activity state. Values saved are stored in a Bundle object that is essentially a key-value pairs map and passed into the onCreate method of every Android Activity.

How does the factory work with the savedstateregistryowner?

The factory takes an instance of GithubApi and SavedStateRegistryOwner instance as a 2nd argument, which is in this case, the Activity. This 2nd argument is very important since it ties the saved state to the current component (either Activity or Fragment).


Video Answer


1 Answers

Injecting values means, that you don't assign the value yourself. This said,

@Inject 
SessionKeyExchangerService sessionKeyExchangerService;

// then in onCreate() after the injection
sessionKeyExchangerService = savedInstanceState.getParcelable(SESSION_KEY_PARCEL);

is not what you want to do.

If your SessionKeyExchangerService depends on some saved state, you will have to pass it into your module.

AppModule seems to be the wrong place to provide the SessionKeyExchangerService. You should probably outsource to some SessionModule which you then can swap, as I think is well explained here. In this sample, the UserModule lifecycle is managed by the app, and not dagger.

By providing a module with a constructor you can hence pass in your Parcelable state from the savedInstanceState.

Without knowing your whole project, I think you can greatly reduce the complexity and probably should not save state in the activity, but rather use SharedPreferences or plain files. This would also remove the need for maintaining the module lifecycle with your activity state.

like image 81
David Medenjak Avatar answered Sep 23 '22 17:09

David Medenjak