Replace Dagger-Android with Hilt and remove Kapt (#4423)

Hilt is an annotation processor built on top of Dagger which allows to
remove all the Android dependency injection boilerplate code (currently
around 900 lines) by writing it for us.

Hilt can use KSP instead of Kapt so Kapt can be completely removed from
the project. Kapt is slow, deprecated and has a few compatibility
issues. Removing Kapt will improve build times since no Java stubs have
to be generated for Kotlin classes anymore (Note that KSP also processes
annotations in Java classes so it can completely replace Kapt).

- Remove all modules related to manual dependency injection
configuration.
- Rename `AppModule` to `StorageModule` since it now only contains
configuration to retrieve the DataBase and SharedPreferences.
- Annotate all entry points (Activities, Fragments, BroadcastReceivers
and Services) with `@AndroidEntryPoint`.
- Annotate all injected ViewModels with `@HiltViewModel` and replace the
custom ViewModel Factory with the default one (which integrates with the
one generated by Hilt).
- Add a public field to allow overriding the default
ViewModelProvider.Factory in `BaseActivity` in tests.
- Annotate tested Activities with `@OptionalInject` since Activity tests
currently rely on the Activities not being injected automatically.
- Annotate injected `Context` arguments with `@ApplicationContext`. Hilt
provides the `Context` binding automatically but requires to specify if
the Application or Activity Context is wanted.
- Add WorkManager Hilt integration so all Workers are injected by Hilt
automatically using `HiltWorkerFactory`.
- Lazily initialize WorkManager in `TuskyApplication`.
- Remove Kapt and Kapt workarounds.
- ~~Remove toolchain configuration for Java 21. Toolchains force the
Java bytecode to match the JDK version used to build the project, and
apparently Hilt doesn't run inside the toolchain so cannot process the
source code if the JDK version of the toolchain is higher than the JDK
used to run Gradle. [And configuring a toolchain for an older Java
version causes other
issues](https://jakewharton.com/gradle-toolchains-are-rarely-a-good-idea/).
**Removing toolchains configuration doesn't prevent the project from
being built using JDK 21** or more recent versions but allows to build
the project using older JDKs as well.~~
Added a fix to allow Hilt to properly use the JDK toolchain.
- ~~Set the Java and Kotlin bytecode target to Java 17. The standard
bytecode target for Android projects is usually Java 8 or 11 (any higher
version doesn't provide any benefit but may cause compatibility issues).
However, since the app currently uses a library built against Java 17
bytecode (`networkresult-calladapter`), it needs to target at least Java
17 bytecode as well.~~
- Update the Dagger 2 URL in the licenses screen. Hilt is part of Dagger
2 so the label wasn't changed.
This commit is contained in:
Christophe Beyls 2024-05-10 15:55:07 +02:00 committed by GitHub
commit dc4ca06551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 344 additions and 1253 deletions

View file

@ -26,13 +26,12 @@ import com.keylesspalace.tusky.components.systemnotifications.updateUnifiedPushS
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.network.MastodonApi
import dagger.android.AndroidInjection
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
@DelicateCoroutinesApi
@AndroidEntryPoint
class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
@Inject
lateinit var mastodonApi: MastodonApi
@ -45,7 +44,6 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
lateinit var externalScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
if (Build.VERSION.SDK_INT < 28) return
if (!canEnablePushNotifications(context, accountManager)) return

View file

@ -31,11 +31,12 @@ import com.keylesspalace.tusky.service.SendStatusService
import com.keylesspalace.tusky.service.StatusToSend
import com.keylesspalace.tusky.util.getSerializableExtraCompat
import com.keylesspalace.tusky.util.randomAlphanumericString
import dagger.android.AndroidInjection
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
private const val TAG = "SendStatusBR"
@AndroidEntryPoint
class SendStatusBroadcastReceiver : BroadcastReceiver() {
@Inject
@ -43,8 +44,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
if (intent.action == NotificationHelper.REPLY_ACTION) {
val serverNotificationId = intent.getStringExtra(NotificationHelper.KEY_SERVER_NOTIFICATION_ID)
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)

View file

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.receiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
@ -26,14 +25,13 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.worker.NotificationWorker
import dagger.android.AndroidInjection
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver
@DelicateCoroutinesApi
@AndroidEntryPoint
class UnifiedPushBroadcastReceiver : MessagingReceiver() {
companion object {
const val TAG = "UnifiedPush"
@ -49,13 +47,7 @@ class UnifiedPushBroadcastReceiver : MessagingReceiver() {
@ApplicationScope
lateinit var externalScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
AndroidInjection.inject(this, context)
}
override fun onMessage(context: Context, message: ByteArray, instance: String) {
AndroidInjection.inject(this, context)
Log.d(TAG, "New message received for account $instance")
val workManager = WorkManager.getInstance(context)
val request = OneTimeWorkRequest.from(NotificationWorker::class.java)
@ -63,7 +55,6 @@ class UnifiedPushBroadcastReceiver : MessagingReceiver() {
}
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
AndroidInjection.inject(this, context)
Log.d(TAG, "Endpoint available for account $instance: $endpoint")
accountManager.getAccountById(instance.toLong())?.let {
externalScope.launch {
@ -75,7 +66,6 @@ class UnifiedPushBroadcastReceiver : MessagingReceiver() {
override fun onRegistrationFailed(context: Context, instance: String) = Unit
override fun onUnregistered(context: Context, instance: String) {
AndroidInjection.inject(this, context)
Log.d(TAG, "Endpoint unregistered for account $instance")
accountManager.getAccountById(instance.toLong())?.let {
// It's fine if the account does not exist anymore -- that means it has been logged out