Skip to content

Hilt migration experiment#15653

Closed
irfano wants to merge 17 commits intowordpress-mobile:trunkfrom
irfano:feature/hilt-migration
Closed

Hilt migration experiment#15653
irfano wants to merge 17 commits intowordpress-mobile:trunkfrom
irfano:feature/hilt-migration

Conversation

@irfano
Copy link
Copy Markdown
Member

@irfano irfano commented Dec 5, 2021

This PR adds Hilt infrastructure. This doesn't change any user-facing feature. Details explained here: pbMoDN-35Z-p2

  • WordPress class is converted to an abstract class to be used by real application and test application commonly.
  • @AndroidEntryPoint could be added to all fragment, activity classes but we didn't want to touch so many classes. So, AppComponent is converted to an EntryPoint aggregator. With this change, fragments can still access AppComponent class and use its inject() methods.
  • A separate AppInitializer class is created to handle initializing in different way for real and test applications.

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

  1. Potential unintended areas of impact
    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.
  2. What I did to test those areas of impact (or what existing automated tests I relied on)
    I smoke tested the app, it's working as expected. For release build, I relied on automated tests.
  3. What automated tests I added (or what prevented me from doing so)
    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:

  • I have completed the Regression Notes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

- 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.
@peril-wordpress-mobile
Copy link
Copy Markdown

peril-wordpress-mobile bot commented Dec 6, 2021

You can trigger optional UI/connected tests for these changes by visiting CircleCI here.

@peril-wordpress-mobile
Copy link
Copy Markdown

peril-wordpress-mobile bot commented Dec 6, 2021

You can test the changes on this Pull Request by downloading the APKs:

Copy link
Copy Markdown
Contributor

@renanferrari renanferrari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes, @irfano! They are looking pretty good so far. Just left one minor question below.

I've also submitted #15659 to force trigger CI here, and it seems that we have some failing lint checks when running ./gradlew ktlint. Could you take a look at it?

Comment on lines +168 to +172
// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@irfano
Copy link
Copy Markdown
Member Author

irfano commented Dec 12, 2021

Thank you for the changes, @irfano! They are looking pretty good so far. Just left one minor question below.

I've also submitted #15659 to force trigger CI here, and it seems that we have some failing lint checks when running ./gradlew ktlint. Could you take a look at it?

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.
@irfano
Copy link
Copy Markdown
Member Author

irfano commented Dec 13, 2021

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:
12 public static functions,
11 public static variables,
6 public functions,
2 public variables.
These members are being used by other classes. They aren't removed on purpose since other classes are using them. Even some logics moved to AppInitializer, gateway functions were left in WordPress.

Important details for be0ed5d:

  • New structure is tested on LoginTest. Other test use cases will be fixed in future commits.
  • 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. (restClientUtils, restClientUtilsV1_1, ..., defaultUserAgent, userAgent)

Copy link
Copy Markdown
Contributor

@renanferrari renanferrari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

@irfano
Copy link
Copy Markdown
Member Author

irfano commented Dec 16, 2021

Thanks for your review @renanferrari !

how to handle those classes that are casting Application to WordPress. How are you planning to tackle that?

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.
(Another solution would be using WordPress as a base Application, but that was our main problem hor Hilt testing, we can't do that.)


Lint is also failing with the following error:
Class referenced in the manifest, .WordPressRelease, was not found in the project or the libraries

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.

Copy link
Copy Markdown
Member

@hichamboushaba hichamboushaba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +29 to +36
override fun androidInjector(): AndroidInjector<Any> {
if (!this::injector.isInitialized) {
injector = EntryPoints.get(
applicationContext,
AndroidInjectorEntryPoint::class.java
).injector()
}
return injector
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for warning me. I'll consider it while fixing UI tests. 🙏🏻

Comment on lines +148 to +149
// Well sql is initialized with `WPWellSqlConfig` for debug, `WellSqlConfig` for release.
@Inject lateinit var mWellSqlInitializer: WellSqlInitializer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. It's not super clear, and that's why we depend on the comment to explain why it's needed.
  2. 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

@hichamboushaba hichamboushaba Dec 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about initialize them in AppInitializer's init and injecting AccountStore lazily?

Yes, this is another possibility, I like it 👍

@irfano
Copy link
Copy Markdown
Member Author

irfano commented Dec 22, 2021

Thanks for your review @hichamboushaba
I'm aware of failing UI tests and currently working on it.

- 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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

irfano added 4 commits January 9, 2022 00:38
# 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.
Comment on lines -44 to -47
// 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
Copy link
Copy Markdown
Member Author

@irfano irfano Jan 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed isInitialized here. This was a wrong approach. AppInitializer should be recreated for each test cases.

Comment on lines +11 to +19
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [InterceptorModule::class]
)
@Module
class DummyInterceptorModule {
@Provides @IntoSet @Named("network-interceptors")
fun provideNetworkInterceptor(): Interceptor = Interceptor { it.proceed(it.request()) }
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Member

@hichamboushaba hichamboushaba Jan 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense now. Thanks! 👍🏻

Comment on lines 88 to 92
public void confirmSignup() {
// Confirm signup
waitForElementToBeDisplayed(R.id.nav_sites);
new MySitesPage().go();
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@renanferrari renanferrari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 88 to 92
public void confirmSignup() {
// Confirm signup
waitForElementToBeDisplayed(R.id.nav_sites);
new MySitesPage().go();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@irfano irfano mentioned this pull request Mar 6, 2022
3 tasks
@irfano irfano changed the title Hilt migration Hilt migration experiment Mar 22, 2022
@irfano irfano mentioned this pull request Mar 22, 2022
3 tasks
@irfano
Copy link
Copy Markdown
Member Author

irfano commented Mar 22, 2022

This PR is canceled and the migration is being implemented in #16143.

@irfano irfano closed this Mar 22, 2022
@irfano irfano deleted the feature/hilt-migration branch January 2, 2023 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants