Skip to content

Commit

Permalink
Replace Dagger-Android with Hilt and remove Kapt (#4423)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cbeyls authored May 10, 2024
1 parent c1fe58c commit dc4ca06
Show file tree
Hide file tree
Showing 111 changed files with 344 additions and 1,253 deletions.
26 changes: 6 additions & 20 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.google.ksp)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlin.parcelize)
}

Expand Down Expand Up @@ -81,6 +81,7 @@ android {
resValues true
viewBinding true
}

testOptions {
unitTests {
returnDefaultValues = true
Expand Down Expand Up @@ -156,8 +157,10 @@ dependencies {
implementation libs.bundles.glide
ksp libs.glide.compiler

implementation libs.bundles.dagger
kapt libs.bundles.dagger.processors
implementation libs.hilt.android
ksp libs.hilt.compiler
implementation libs.androidx.hilt.work
ksp libs.androidx.hilt.compiler

implementation libs.sparkbutton

Expand Down Expand Up @@ -189,20 +192,3 @@ dependencies {
androidTestImplementation libs.androidx.room.testing
androidTestImplementation libs.androidx.test.junit
}

// Work around warnings of:
// WARNING: Illegal reflective access by org.jetbrains.kotlin.kapt3.util.ModuleManipulationUtilsKt (file:/C:/Users/Andi/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-annotation-processing-gradle/1.8.22/28dab7e0ee9ce62c03bf97de3543c911dc653700/kotlin-annotation-processing-gradle-1.8.22.jar) to constructor com.sun.tools.javac.util.Context()
// See https://youtrack.jetbrains.com/issue/KT-30589/Kapt-An-illegal-reflective-access-operation-has-occurred
tasks.withType(org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask).configureEach {
kaptProcessJvmArgs.addAll([
"--add-opens", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"])
}
5 changes: 3 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch

class AboutActivity : BottomSheetActivity(), Injectable {
@AndroidEntryPoint
class AboutActivity : BottomSheetActivity() {
@Inject
lateinit var instanceInfoRepository: InstanceInfoRepository

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.BindingHolder
Expand All @@ -45,17 +43,15 @@ import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import javax.inject.Inject
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

private typealias AccountInfo = Pair<TimelineAccount, Boolean>

class AccountsInListFragment : DialogFragment(), Injectable {
@AndroidEntryPoint
class AccountsInListFragment : DialogFragment() {

@Inject
lateinit var viewModelFactory: ViewModelFactory

private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory }
private val viewModel: AccountsInListViewModel by viewModels()
private val binding by viewBinding(FragmentAccountsInListBinding::bind)

private lateinit var listId: String
Expand Down
20 changes: 18 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;

import com.google.android.material.color.MaterialColors;
Expand All @@ -41,7 +42,6 @@
import com.keylesspalace.tusky.components.login.LoginActivity;
import com.keylesspalace.tusky.db.entity.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.settings.AppTheme;
import com.keylesspalace.tusky.settings.PrefKeys;
Expand All @@ -55,7 +55,10 @@

import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME;

public abstract class BaseActivity extends AppCompatActivity implements Injectable {
/**
* All activities inheriting from BaseActivity must be annotated with @AndroidEntryPoint
*/
public abstract class BaseActivity extends AppCompatActivity {

public static final String OPEN_WITH_SLIDE_IN = "OPEN_WITH_SLIDE_IN";

Expand All @@ -65,6 +68,12 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@NonNull
public AccountManager accountManager;

/**
* Allows overriding the default ViewModelProvider.Factory for testing purposes.
*/
@Nullable
public ViewModelProvider.Factory viewModelProviderFactory = null;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down Expand Up @@ -152,6 +161,13 @@ protected void attachBaseContext(Context newBase) {
super.attachBaseContext(fontScaleContext);
}

@NonNull
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
final ViewModelProvider.Factory factory = viewModelProviderFactory;
return (factory != null) ? factory : super.getDefaultViewModelProviderFactory();
}

protected boolean requiresLogin() {
return true;
}
Expand Down
12 changes: 4 additions & 8 deletions app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
Expand All @@ -57,22 +55,20 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class EditProfileActivity : BaseActivity(), Injectable {
@AndroidEntryPoint
class EditProfileActivity : BaseActivity() {

companion object {
const val AVATAR_SIZE = 400
const val HEADER_WIDTH = 1500
const val HEADER_HEIGHT = 500
}

@Inject
lateinit var viewModelFactory: ViewModelFactory

private val viewModel: EditProfileViewModel by viewModels { viewModelFactory }
private val viewModel: EditProfileViewModel by viewModels()

private val binding by viewBinding(ActivityEditProfileBinding::inflate)

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import android.widget.TextView
import androidx.annotation.RawRes
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import dagger.hilt.android.AndroidEntryPoint
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.buffer
import okio.source

@AndroidEntryPoint
class LicenseActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
19 changes: 4 additions & 15 deletions app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.databinding.DialogListBinding
import com.keylesspalace.tusky.databinding.ItemListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.hide
Expand All @@ -53,22 +51,15 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?)

class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
@AndroidEntryPoint
class ListsActivity : BaseActivity() {

@Inject
lateinit var viewModelFactory: ViewModelFactory

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

private val viewModel: ListsViewModel by viewModels { viewModelFactory }
private val viewModel: ListsViewModel by viewModels()

private val binding by viewBinding(ActivityListsBinding::inflate)

Expand Down Expand Up @@ -287,8 +278,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
}

override fun androidInjector() = dispatchingAndroidInjector

companion object {
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
}
Expand Down
12 changes: 5 additions & 7 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,17 @@ import com.mikepenz.materialdrawer.util.addItems
import com.mikepenz.materialdrawer.util.addItemsAtPosition
import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.migration.OptionalInject
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@OptionalInject
@AndroidEntryPoint
class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {

@Inject
lateinit var eventHub: EventHub
Expand Down Expand Up @@ -1215,8 +1215,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje

override fun getActionButton() = binding.composeButton

override fun androidInjector() = androidInjector

companion object {
const val OPEN_WITH_EXPLODE_ANIMATION = "explode"

Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@SuppressLint("CustomSplashScreen")
class SplashActivity : AppCompatActivity(), Injectable {
@AndroidEntryPoint
class SplashActivity : AppCompatActivity() {

@Inject
lateinit var accountManager: AccountManager
Expand Down
11 changes: 3 additions & 8 deletions app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,12 @@ import com.keylesspalace.tusky.entity.FilterV1
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch

class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
@AndroidEntryPoint
class StatusListActivity : BottomSheetActivity() {

@Inject
lateinit var eventHub: EventHub
Expand Down Expand Up @@ -399,8 +396,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
return true
}

override fun androidInjector() = dispatchingAndroidInjector

companion object {

private const val EXTRA_KIND = "kind"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,26 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import dagger.hilt.android.AndroidEntryPoint
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
@AndroidEntryPoint
class TabPreferenceActivity : BaseActivity(), ItemInteractionListener, ListSelectionFragment.ListSelectionListener {

@Inject
lateinit var mastodonApi: MastodonApi

@Inject
lateinit var eventHub: EventHub

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

private val binding by viewBinding(ActivityTabPreferenceBinding::inflate)

private lateinit var currentTabs: MutableList<TabData>
Expand Down Expand Up @@ -368,8 +364,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
}
}

override fun androidInjector() = dispatchingAndroidInjector

companion object {
private const val MIN_TAB_COUNT = 2
}
Expand Down
Loading

0 comments on commit dc4ca06

Please sign in to comment.