Hilt migration experiment#15653
Hilt migration experiment#15653irfano wants to merge 17 commits intowordpress-mobile:trunkfrom irfano:feature/hilt-migration
Conversation
- Updated Dagger modules with @Installin(SingletonComponent::class) annotation. - AppComponent is converted to entry point aggregator. This is a temporary change. AppComponent will be removed when Hilt migration is done. - Added @ApplicationContext to context parameters in modules.
Before this commit, app was crashing at start. Because some initializing stuff were working after Hilt injections. Now they are starting together with other injections.
|
You can trigger optional UI/connected tests for these changes by visiting CircleCI here. |
|
You can test the changes on this Pull Request by downloading the APKs: |
| // Even if `mWebViewDataDirectorySuffixInitializer`, `mWellSqlInitializer` aren't used, they need to be initialized | ||
| // at application startup. | ||
| @Inject WebViewDataDirectorySuffixInitializer mWebViewDataDirectorySuffixInitializer; | ||
| // Well sql is initialized with `WPWellSqlConfig` for debug, `WellSqlConfig` for release. | ||
| @Inject WellSqlInitializer mWellSqlInitializer; |
There was a problem hiding this comment.
Thank you for adding these helpful comments!
| class WordPressTestRunner : AndroidJUnitRunner() { | ||
| override fun newApplication(classLoader: ClassLoader, className: String, context: Context): Application { | ||
| return super.newApplication(classLoader, WordPressTest::class.java.name, context) | ||
| return super.newApplication(classLoader, WordPress::class.java.name, context) |
There was a problem hiding this comment.
I might be missing something here, but since we're not using a custom application for instrumentation tests anymore, do we still need to use this custom test runner?
There was a problem hiding this comment.
You're right to catch here. This line doesn't make sense. But I didn't want to focus test in this commit.
My next commit will be for fixing test, and I'm creating a new test application. I'll explain the details for that commit.
Thanks! I fixed ktlint error. |
Hilt test requires test application but test applications have some drawbacks. While real applications can inject before onCreate, test applications can only inject after onCreate. Thus, AppInitializer is created and will be used as a rule in test cases.
|
Hilt is working for real application and unit tests, but UI tests require architecture update. Hilt test requires test application but test applications have some drawbacks. While real applications can inject before onCreate, test applications can only inject after onCreate. Because of test application's injection order problem, AppInitializer is created in be0ed5d and will be used as InitializationRule in test cases. Wordpress has many dependency to other classes. It has: Important details for be0ed5d:
|
renanferrari
left a comment
There was a problem hiding this comment.
@irfano Thank you for the changes!
I spent some time smoke testing the app, specifically features that I know were affected by the introduction of AppInitializer, and everything seemed to be working as expected. There's always the possibility for something to be silently failing, but I think we're good for now.
As for the changes you made to the code, they all make sense to me so far. As you mentioned before, I think the main hurdle right now is to figure out how to handle those classes that are casting Application to WordPress. How are you planning to tackle that?
Even some logics moved to AppInitializer, gateway functions were left in WordPress.
Before, WorkManager config difference for debug and release builds were being handled in WordPressDebug. Now it's handled with BuildConfig.DEBUG in AppInitializer.
Lazy initialization were being used for some variables in WordPress. Kotlin's lazy property feature is used to provide same feature
Good calls! 👍
Aside from the failing UI tests, Lint is also failing with the following error:
Class referenced in the manifest, .WordPressRelease, was not found in the project or the libraries
I haven't looked into this. Do you have any idea what could be causing it?
|
Thanks for your review @renanferrari !
I think the only way is changing the code where accessing WordPress. I plan to change them to access AppInitializer instead of WordPress. After that change, I probably will remove public gateway functions in WordPress.
Fixed that, it was a small mistake. I'd left release build untested. But then I realized I can test release build by just changing signing config with debug signing, at least I can be sure building is not failing. I did it, and now it's working. |
hichamboushaba
left a comment
There was a problem hiding this comment.
Good job @irfano, I left some comments on the code, but nothing major.
But UI tests are not working currently, they sometimes crash with the following exception:
Caused by: java.lang.ClassCastException: org.wordpress.android.WordPressTest_Application cannot be cast to org.wordpress.android.WordPress
at org.wordpress.android.push.GCMMessageService.onCreate(GCMMessageService.java:41)Caused by: java.lang.ClassCastException: org.wordpress.android.WordPressTest_Application cannot be cast to org.wordpress.android.WordPress
at org.wordpress.android.push.GCMMessageService.onCreate(GCMMessageService.java:41)
And sometimes they just don't start, and they show a toast about DB inconsistency.
| override fun androidInjector(): AndroidInjector<Any> { | ||
| if (!this::injector.isInitialized) { | ||
| injector = EntryPoints.get( | ||
| applicationContext, | ||
| AndroidInjectorEntryPoint::class.java | ||
| ).injector() | ||
| } | ||
| return injector |
There was a problem hiding this comment.
@irfano I did the same mistake here on my initial migration in WCAndroid, but later one we found out that when running multiple tests on the same session, the caching of the AndroidInjector causes an issue of having inconsistent copies of dependencies.
For more details check my last PR in WCAndroid: woocommerce/woocommerce-android#5445
There was a problem hiding this comment.
Thank you for warning me. I'll consider it while fixing UI tests. 🙏🏻
| // Well sql is initialized with `WPWellSqlConfig` for debug, `WellSqlConfig` for release. | ||
| @Inject lateinit var mWellSqlInitializer: WellSqlInitializer |
There was a problem hiding this comment.
np, depending on the comment here to ensure WellSql is initialized is not the best approach IMO, since you added a separate class for the initialization, I would suggest adding an explicit init function to it and calling it from the AppInitializer#init function, instead of depending on the init bloc.
This would also ensure that the behavior is correct even if Dagger's behavior changed from eager injecting to lazier injecting here 🙂.
Same remark for WebViewDataDirectorySuffixInitializer.
There was a problem hiding this comment.
I initialized them with @Inject annotation instead of AppInitializer#init function, because other @Inject initializations need them.
If I init WellSql in AppInitializer#init, AccountStore crashes because of uninitialized WellSql.
I want to initialize WebViewDataDirectorySuffixInitializer and WellSqlInitializer before everything else. I think doing it in AppInitializer#init is not a right way to do that but we can discuss alternative ways.
What is your main concern here? Does it seem weird to inject a class just for initializing them?
There was a problem hiding this comment.
If I init WellSql in AppInitializer#init, AccountStore crashes because of uninitialized WellSql.
Yes, I forgot about this, so maybe calling init directly in the app's onCreate 🤔
What is your main concern here? Does it seem weird to inject a class just for initializing them?
It's nothing major, but just two small issues IMO:
- It's not super clear, and that's why we depend on the comment to explain why it's needed.
- It depends on Dagger's logic, if Dagger was to inject the dependencies lazily by default (like Koin for example), this wouldn't work, since the class won't be instantiated until it's injected.
Anyway, it's just a small point, so whatever you and @renanferrari agree on, for WCAndroid, we didn't create any class or WellSql's initialization, and instead it's done manually in the Application's onCreate.
There was a problem hiding this comment.
Yes, I forgot about this, so maybe calling init directly in the app's onCreate 🤔
It's called in WooCommerce in WCAndroid. At first I thought doing the same, but then I thought keeping application class minimal could be a better approach. Otherwise, I'd have to handle WellSql initialization also in test application, like this made in WCAndroid.
What about initialize them in AppInitializer's init and injecting AccountStore lazily?
https://gist.github.com/irfano/790096d16ed4b0c4ca1adc366102d191
There was a problem hiding this comment.
What about initialize them in AppInitializer's init and injecting AccountStore lazily?
Yes, this is another possibility, I like it 👍
|
Thanks for your review @hichamboushaba |
- Added HiltAndroidTest to UserAgentTest - Moved getUserAgent(), getDefaultUserAgent(), USER_AGENT_APPNAME from WordPress to AppInitializer
| // InitializationRule must be initialized only once. Otherwise some static functions in AppInitializer throws | ||
| // exception, if they are initialized more than once. (e.g. WebView.setDataDirectorySuffix(), | ||
| // EventBusBuilder.installDefaultEventBus) | ||
| private var isInitialized = false |
There was a problem hiding this comment.
With @Rule annotation, the TestRule runs for each test cases. That means InitializationRule will be called for each test cases. But that causes crash because some functions in AppInitializer must be called only once for application lifecycle.
With isInitialized, I ensured that AppInitializer will be called once. The disadvantage of this way can be that same AppInitializer instance will be used for each test cases. If I encounter this problem, I'll try to clean resources in AppInitializer before test cases.
I tried to use @ClassRule to run test rule once for this class, but it didn't work because of Hilt's Entry Point. I couldn't find the reason. I may try to use @ClassRule again later.
# Conflicts: # WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java # build.gradle
Since SingletonComponent is recreated for each test cases, caching AndroidInjector and AppInitializer causes failing UI tests. AndroidInjector and AppInitializer are no longer cached for UI tests. But some static functions were crashing while AppInitializer is recreating. Added a boolean to AppInitializer to manage reinitializing of these functions.
Other classes are casting application to WordPress but WordPress couldn't be used in UI tests. And test application (WordPressTest) was crashing. With this common abstract WordPress application, other classes can cast application to WordPress safely.
| // InitializationRule must be initialized only once. Otherwise some static functions in AppInitializer throws | ||
| // exception, if they are initialized more than once. (e.g. WebView.setDataDirectorySuffix(), | ||
| // EventBusBuilder.installDefaultEventBus) | ||
| private var isInitialized = false |
There was a problem hiding this comment.
I removed isInitialized here. This was a wrong approach. AppInitializer should be recreated for each test cases.
| @TestInstallIn( | ||
| components = [SingletonComponent::class], | ||
| replaces = [InterceptorModule::class] | ||
| ) | ||
| @Module | ||
| class DummyInterceptorModule { | ||
| @Provides @IntoSet @Named("network-interceptors") | ||
| fun provideNetworkInterceptor(): Interceptor = Interceptor { it.proceed(it.request()) } | ||
| } |
There was a problem hiding this comment.
@hichamboushaba I copied DummyInterceptorModule from WCAndroid. It works but I couldn't understand what this actually does. Is @Named("network-interceptors") required for fluxc library? Should I check fluxc documents?
There was a problem hiding this comment.
@irfano I need to check, but if I remember correctly the reason we needed this it was just to replace the actual InterceptorModule in tests with a dummy one, so the main thing is:
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [InterceptorModule::class]
)and not the interceptor itself.
I didn't test this, but maybe you can just keep the module empty, without the provided interceptor at all 🤔
There was a problem hiding this comment.
Thanks for the explanation.
I learnt that "network-interceptors" is mainly for logging, "interceptors" is for mocking here.
WCAndroid is using Flipper, WPAndroid is using Stetho for network debugging. By adding DummyInterceptorModule, WCAndroid removed Flipper for testing. I couldn't understand the exact reason why do we need to cancel network debugging, but I will also do the same for WPAndroid.
I mean I removed Stetho for testing by adding DummyInterceptorModule.
There was a problem hiding this comment.
We remove the logging because it adds an overhead which is not necessary for tests, since the network requests are already mocked, but I don't think it will hurt if it stayed, if it's just pure logging.
There was a problem hiding this comment.
It makes sense now. Thanks! 👍🏻
| public void confirmSignup() { | ||
| // Confirm signup | ||
| waitForElementToBeDisplayed(R.id.nav_sites); | ||
| new MySitesPage().go(); | ||
| } |
There was a problem hiding this comment.
Before this commit, the test was passing successfully. Then activity was finishing while recylerview is still refreshing and then ReaderExpandableTagsView on "Reader" screen was crashing because of unaccessible Hilt objects.
If test was waiting enough for reloading of recyclerview, test was passing. But sometimes it was failing.
Waiting for 2 seconds at confirmSignup() can prevent failing.
I chose navigating "My Sites" screen that also prevent failing.
There was a problem hiding this comment.
Just replicating here what I said on Slack, for visibility: I personally think both approaches are fine in this case, but maybe we should add an extra comment there, because it’s not immediately obvious that removing that piece of code could cause the test to fail.
renanferrari
left a comment
There was a problem hiding this comment.
Thank you for the changes, @irfano! All CI checks are now passing, including UI tests 🎉 I also smoke tested the app and, from what I can tell, it's still working as expected.
The code is looking good overall and I like the approach used to solve the WordPress casting issue 👍
@hichamboushaba Thank you for your assistance with this so far! Let us know if you have any additional comments.
| public void confirmSignup() { | ||
| // Confirm signup | ||
| waitForElementToBeDisplayed(R.id.nav_sites); | ||
| new MySitesPage().go(); | ||
| } |
There was a problem hiding this comment.
Just replicating here what I said on Slack, for visibility: I personally think both approaches are fine in this case, but maybe we should add an extra comment there, because it’s not immediately obvious that removing that piece of code could cause the test to fail.
|
This PR is canceled and the migration is being implemented in #16143. |
This PR adds Hilt infrastructure. This doesn't change any user-facing feature. Details explained here: pbMoDN-35Z-p2
@AndroidEntryPointcould be added to all fragment, activity classes but we didn't want to touch so many classes. So,AppComponentis converted to an EntryPoint aggregator. With this change, fragments can still accessAppComponentclass and use itsinject()methods.Hilt and dagger can be used together in the project with this PR.
To test:
This change is a big one and there is no feature change. All build variants, debug and release builds must be tested.
Regression Notes
This PR intents to have no feature change. This is just infrastructure change. But since dagger is being used on every class, that means all screens can be impacted.
I smoke tested the app, it's working as expected. For release build, I relied on automated tests.
It's not added any new test because there is no new feature. Tests were broken after adding Hilt, I fixed them.
PR submission checklist:
RELEASE-NOTES.txtif necessary.