I have a fragment class as follows:
class MainFragment : Fragment(R.layout.main_fragment) {
#Inject lateinit var runner: Runner
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
runner.runProcess()
}
}
Here is my Runner.java file:
public class Runner {
#Inject
public Runner(#ApplicationContext Context applicationContext) {
// some setup code
}
public runProcess() {...}
}
I am getting an error at the runner.runProcess() line saying that lateinit property runner is not initialized. I am using Hilt in my Android app. How can I go about fixing this?
MainActivity.kt:
#AndroidEntryPoint
class MainActivity #Inject constructor() : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MainFragment>(R.id.fragment_container_view);
}
}
}
}
Found the issue. I have to add the #AndroidEntryPoint annotation to my fragment as well, despite the fragment being added to the main activity which has this annotation.
For future reference, the #AndroidEntryPoint annotation should be provided for every android class which requires dependency injection. It marks the entry points for hilt to inject modules. It is not to be confused with the entry point of the app i.e., the launch activity.
Source
Related
I am using a Room Database with AndroidViewModel (without Factory)
class RoomModel(app: Application) : AndroidViewModel(app) {
// ....
}
and not sure about how to correctly initialize that and also, if I could use one-time initilization for whole app.
I have two ways to deal with initializations which seem to work ok:
Initializing in onViewCreated():
...
private lateinit var database: RoomModel
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
Initializing by activityViewModels():
...
private val mData: RoomModel by activityViewModels()
...
The both seem to work, but not sure which one would never lead to an app crash at some point.
Also, I would like to know, if it's a good practce using one shared reference variable of my RoomModel declared and initialized in a base Fragment that is used by other fragments like this:
class BaseFragment : Fragment() {
...
lateinit var database: RoomModel
// Or
val database: RoomModel by activityViewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
}
Some other fragments extended by BaseFragment() like this:
class FragmentA : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference variable database to observe or add or edit data
}
}
and
class FragmentB : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference database to observe or add or edit data
}
}
and so on...
In this way I have only to init the Room once and use it without reinitializing in each fragmet I need to access to. Is that a goog idea or I rather need to use individual declaration and initialization of RoomModel in each Fragment I need access to?
Many thanks in advance.
Your Room Database instance should be a singleton. The instance should live as long as the application itself. It is costly to initialize it every time you need to use it. You should initialize Room Database in the application and each ViewModel that needs Room Database instance will get it from the application as a dependency. Or you can provide the Room Database as a dependency to your ViewModel, but then you need to create a factory for your ViewModel. You can also use a Dependency Injection library to manage it for you, but it can be overwhelming at first.
Without using any dependency injection library, you can see the example below.
Override the Application, and don't forget to declare it in the AndroidManifest.xml file.
AndroidApp.kt
class AndroidApp : Application() {
lateinit var myRoomDatabase: MyRoomDatabase
override fun onCreate() {
super.onCreate()
myRoomDatabase = Room
.databaseBuilder(applicationContext, MyRoomDatabase::class.java, "my-room-database")
.build()
}
}
AndroidManifest.xml
...
<application
android:name=".AndroidApp"
...
>
...
When you extends the AndroidViewModel you have the Application instance as a dependency. Cast it to AndroidApp and now you can get myRoomDatabase.
MainViewModel.kt
class MainViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentAViewModel.kt
class FragmentAViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentBViewModel.kt
class FragmentBViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
I'm trying to inject some dependency to both activity and fragment using Koin and I expect it to live as long as activity lives, but it turned out a headache for me.
I managed to create a module that resolves MainRouter, inject it into an activity, but it doesn't work for a fragment.
val appModule = module {
scope<MainActivity> {
scoped { MainRouter() }
}
}
MainActivity extends ScopeActivity, MyFragment extends ScopeFragment.
in MainActivity private val router : MainRouter by inject() works fine, but in MyFragment it throws org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.example.app.MainRouter'. Check your definitions!
Finally I managed to inject, but it doesn't look pretty
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scopeId = scopeActivity!!.getScopeId()
scope.linkTo(getKoin().getScope(scopeId))
mainRouter = get()
...
I also don't like that scopeActivity can't be accessed in the init method. Does this mean that activity scoped dependencies cannot be resolved in fragment using by inject()?
As I can see in your code, you have to declare a Fragment instance, just declare it as a fragment in your Koin module and use constructor injection. Like below:
val appModule = module {
single { MyService() }
fragment { MyFragment(get()) }
}
Please refer link for more details.
Is there a way to mock ViewModel that's built is inside of a fragment? I'm trying to run some tests on a fragment, one of the fragment functions interacts with the ViewModel, I would like to run the test function and provided a mocked result for the ViewModel. Is this even possilbe?
MyFragment
class MyFragment : Fragment() {
#Inject
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
(requireActivity().application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RoboeltricTestRunner::) {
#Before
fun setup() {
FragmentScenario.Companion.launchIncontainer(MyFragment::class.java)
}
}
Yeah, just mark your ViewModel open and then you can create a mock implementation on top of it.
open class MyViewModel: ViewModel() {
fun myMethodINeedToMock() {
}
}
class MockMyViewModel: MyViewModel() {
override fun myMethodINeedToMock() {
// don't call super.myMethodINeedToMock()
}
}
So, register your MockMyViewModel to the DI framework when testing.
I thought I would post this for anyone else struggling to find a solution. You'll want to use a Fragment Factory, that has a dependency on the ViewModel. Injecting the ViewModel into the fragments constructor allows the ViewModel to easliy be mocked. There are a few steps that need to be completed for a FragmentFactory but it's not that complicated once you do a couple of them.
Fragment Add the ViewModel into the constructor.
class MyFragment(private val viewModel: ViewModel) : Fragment {
...
}
FragmentFactory, allows fragments to have dependencies in the constructor.
class MyFragmentFactory(private val viewModel: MyViewModel) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
MyFirstFragment::class.java.name -> {
MyFragment(viewModel)
}
// You could use this factory for multiple Fragments.
MySecondFragment::class.java.name -> {
MySecondFragment(viewModel)
}
// You also don't have to pass the dependency
MyThirdFragment::class.java.name -> {
MyThirdFragment()
}
else -> super.instantiate(classLoader, className)
}
}
}
Main Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create your ViewModel
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// create the FragmentFactory and the viewModel dependency.
supportFragmentManager.fragmentFactory = MainFragmentFactory(viewModel)
// FragmentFactory needs to be created before super in an activity.
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RobolectricTestRunner::class)
class MyFragmentUnitTest {
#Before
fun setup() {
val viewModel: MainViewModel = mock(MyViewModel::class.java)
...
}
}
I am trying to use hilt for my project which contains dynamic features. I am facing an error that I cannot fully understand why. I get an error like this:
java.lang.ClassCastException: com.social.analysis.DaggerApp_HiltComponents_ApplicationC$ActivityRetainedCImpl$ActivityCImpl$FragmentCImpl cannot be cast to com.social.login.intro.IntroFragment_GeneratedInjector
at com.social.login.intro.Hilt_IntroFragment.inject(Hilt_IntroFragment.java:94)
at com.social.login.intro.Hilt_IntroFragment.initializeComponentContext(Hilt_IntroFragment.java:58)
at com.social.login.intro.Hilt_IntroFragment.onAttach(Hilt_IntroFragment.java:50)
at androidx.fragment.app.Fragment.onAttach(Fragment.java:1602)
at com.social.login.intro.Hilt_IntroFragment.onAttach(Hilt_IntroFragment.java:40)
My ViewModel in LOGÄ°N MODULE (dynamic features)
class IntroViewModel #Inject constructor(): ViewModel() {
// TODO: Implement the ViewModel
}
My Fragment in LOGIN MODULE
#AndroidEntryPoint
class IntroFragment : BaseFragment<FragmentIntroBinding, IntroViewModel>(
R.layout.fragment_intro
) {
companion object {
fun newInstance() = IntroFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onInitDataBinding() {
viewBinding.viewModel = viewModel
}
}
My Base Fragment in UI Module
abstract class BaseFragment <B: ViewDataBinding, M: ViewModel>(
#LayoutRes
private val layoutId: Int
): Fragment() {
#Inject
lateinit var viewModel: M
lateinit var viewBinding: B
...
My Application Class in App Module
#HiltAndroidApp
class App : SplitCompatApplication() {
}
My Main Activity in App Module
#AndroidEntryPoint
class MainActivity : AppCompatActivity()
I call the IntroFragment from the App module. Then the application crashes.
The project structure looks like this:
From an answer to a similar question:
Delete the .gradle directory (in the project base directory)
Invalidate Caches and restart Android Studio.
I am still pretty new to Daggers dependency injection. I am working on a new application utilizing Dagger 2 and Kotlin. I started with a basic starter app meant for building on. Within App.kt every activity is being injected automatically, which up until now is pretty cool. However I am running into an issue now with implementing Facebook and Google social logins.
When the app tries to launch Facebook or Googles sign in activities I get the error:
"No injector factory bound for Class<external.activities.classNameHere>"
I cannot #Provides those external classes since they do not implement the #Module annotation.
My temporary solution is to check the activity being injected before the automatic injection, and skip those external classes. This seems a little odd though, I am wondering if there is a better solution to this or if I am missing something. I can see this if statement getting pretty long over time.
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks() {
override fun onActivityCreated(p0: Activity?, p1: Bundle?) {
p0?.let {
if (p0 is FacebookActivity || p0 is CustomTabMainActivity || p0 is CustomTabActivity ) {
Log.d("KSULog", "App.kt is not injecting activity " + p0.toString())
}
else {
AndroidInjection.inject(p0)
}
}
}
})
}
Thanks for taking a look.
The way to do this is quite simple.
If You look at Google Samples You will have a clear direction. Like GitHubBrowserSample
So You will create an interface Injectable like this, basically a marker interface.
/**
* Marks an activity / fragment injectable.
*/
interface Injectable
Each activity or fragment will implement this interface for example like this (in Kotlin)
open class BaseActivity : AppCompatActivity(),Injectable {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var baseActivityViewModel: BaseActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
baseActivityViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(BaseActivityViewModel::class.java)
}
}
Important lines are :
open class BaseActivity : AppCompatActivity(),Injectable
AndroidInjection.inject(this)
Create an Activity module to contribute Activity object
/**
* Module to contribute all the activities.
*/
#Module
abstract class ActivityModule {
#ContributesAndroidInjector
internal abstract fun contributeSplashActivity(): SplashActivity
}
and finally DaggerInjector to enable injection
/**
* Helper to inject all the activities and fragments that are marked Injectable.
*/
object DaggerInjector {
fun injectAll(application: TurtleApp) {
DaggerAppComponent.builder()
.application(application)
.build().inject(application)
application
.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
injectComponents(activity)
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
})
}
private fun injectComponents(activity: Activity) {
if (activity is Injectable) {
AndroidInjection.inject(activity)
}
// (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
// object : FragmentManager.FragmentLifecycleCallbacks() {
// override fun onFragmentCreated(fm: FragmentManager?, f: Fragment?,
// savedInstanceState: Bundle?) {
// if (f is Injectable) {
// AndroidSupportInjection.inject(f)
// }
// }
// }, true)
}
}
Uncomment the code to enable Fragment injection.
Your solution is fine but as you say it won't scale well.
You can have a look at one of the Google Samples where they implement a HasSupportFragmentInjector interface to determine whether they want to inject an Activity.
private fun handleActivity(activity: Activity) {
if (activity is HasSupportFragmentInjector) {
AndroidInjection.inject(activity)
}
if (activity is FragmentActivity) {
activity.supportFragmentManager
.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
if (f is Injectable) {
AndroidSupportInjection.inject(f)
}
}
}, true
)
}
}
You should be able to inject these as you would other classes. Providing the examples in Java. Assuming you have AppComponent and AppModule classes:
#Component(modules = AppModule.class)
public interface AppComponent {
....
void inject(App app);
....
}
#Module
public class AppModule {
#Provides
FacebookActivity providesFacebookActivity() {
return new FacebookActivity();
}
}
Then you can annotate the FacebookActivity to be injected into your main activity.
#Inject FacebookActivity mFacebookActivity;
So my external activity is injected into my main activity, which in turn implements the AndroidInjection.inject(this) defined in the AppComponent. The component links to the AppModule which has the #Provides for the FacebookActivity.