Dagger 2 Save and Restore State when Activity Stops
Asked Answered
R

1

11

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 ));
}
Recruitment answered 7/11, 2015 at 20:41 Comment(5)
Can you provide the code where you inject your activity and also where are you building the graph?Tinderbox
I'm just finally getting back to this question. I re-wrote the question to focus on how I set up the dependency injection rather than the parcelables. I'm pretty confident that I am doing that part of it correctly, so the error has to be in the Dagger 2 part.Recruitment
I believe you would need to serialize the state of what you provide in your module through onSaveInstanceState() and hack it back in onRestoreInstanceState(), considering process death kills the entire application process and only the bundles survive. You'd probably need to construct your component in onRestoreInstanceState() if it doesn't exist, and put it back in once by instantiating your dependencies through the component's provision methods.Erythropoiesis
Yes, I saved my values through onSaveInstanceState(). Could you explain more about what you mean by 'put it back in once by instantiating your dependencies through the component's provision methods'. I believe that this is where I am going wrong, but I don't know how to fix this. I expanded my question to show what I am doing here. Thank you!Recruitment
What is Injector can you please explain this line Injector.INSTANCE.getAppComponent().inject(this);Karleen
F
1

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.

Footed answered 10/12, 2015 at 0:44 Comment(2)
Thanks. SessionKeyExchangerService actually has no state, other than its dependency upon AESCipherService. I originally chose dependency injection as a way of creating singleton objects that were threadsafe. I can't use SharedPreferences because doing would force me to store crypto keys on disk, and I want these to go away when the program does. I didn't foresee that I would be creating a stateful situation when I started out or I probably would have just put this in a class with static methods. I found a workaround to this, but it's sort of hacky. I may have to refactor my codeRecruitment
eventually to pull out these dependencies. I think I have concluded that dependency injection was not the ideal architectural decision in this case. Live and learn... Thanks for the answer.Recruitment

© 2022 - 2024 — McMap. All rights reserved.