Reference activity in koin Module - android

I have a single activity application.
My MainActivity is referenced in a number of dependency injection modules, as the implementer of these interfaces. I currently have a work around, which is less than ideal.
class MainActivity : TransaktActivity(), RegistrationNavigator, IAuthPresenter,
IAuthButtonNavigator {
override fun navigateAwayFromAuth() {
navController.navigate(R.id.homeFragment)
}
override fun navigateToAuthPin(buttonId: Int) {
//todo navigate to auth with pin fragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_mainActivity = this
setContentView(R.layout.activity_main)
}
companion object {
private var _mainActivity: MainActivity? = null
fun getInstance() = _mainActivity
}
}
interface RegistrationNavigator {
fun navigateToCardDetails()
fun navigateToOtpCapture()
fun navigateToLoading()
fun navigateOutOfCardRegistration()
}
The appModule is a Koin Module
val appModule = module {
viewModel { SharedViewModel() }
single { MainActivity.getInstance() as RegistrationNavigator }
}
What is the preferred way of achieving this?

Android-lifecycled components such as activities should not be in koin modules.
For example you will have issues with e.g. configuration changes since the koin module would be serving references to stale activity after the activity is recreated.
I haven't really worked with NavController but rather rolled up my own navigation solution. As a generic approach I would refactor the RegistrationNavigator implementation to a separate class the instance of which you can provide from your koin module. If lifecycle-dependent params such as Context (or NavController) are needed, supply them as function args.

Related

Hilt injection into activity before super.onCreate()

I defined my own LayoutInflater.Factory2 class in a separate module. I want to inject it into each activity in my App, but the point is that I have to set this factory before activity's super.onCreate() method.
When I using Hilt it makes an injection right after super.onCreate(). So I have an UninitializedPropertyAccessException.
Is there any opportunity to have an injection before super.onCreate with Hilt?
Below is my example of module's di.
#Module
#InstallIn(SingletonComponent::class)
object DynamicThemeModule {
#FlowPreview
#Singleton
#Provides
fun provideDynamicThemeConfigurator(
repository: AttrRepository
): DynamicTheme<AttrInfo> {
return DynamicThemeConfigurator(repository)
}
}
You can inject the class before onCreate by using Entry Points like this.
#AndroidEntryPoint
class MainActivity: AppCompatActivity() {
#EntryPoint
#InstallIn(SingletonComponent::class)
interface DynamicThemeFactory {
fun getDynamicTheme() : DynamicTheme<AttrInfo>
}
override fun onCreate(savedInstanceState: Bundle?) {
val factory = EntryPointAccessors.fromApplication(this, DynamicThemeFactory::class.java)
val dynamicTheme = factory.getDynamicTheme()
super.onCreate(savedInstanceState)
}
}
If you need something like this a lot Id recommend creating an instance of it in the companion object of your Application class when your application starts (onCreate). That is before any of your views are created. So you don´t need to jump threw those hoops all the time, but can just access the instance that already exists. This code above won´t be available in attachBaseContext, when you need it there you have to create it in your application class I think.

Running Room Persistence Library queries on coroutines GlobalScope

I read that running routines on GlobalScope is bad practice.
What am doing now is:
class SplashScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen)
DataBaseHelper.isDbEmpty(this, object : DataBaseHelper.OnCompleteCheckDB {
override fun isDBEmpty(result: Boolean) {
//handle result
}
})
}
}
DatabseHelper:
class DataBaseHelper() {
companion object {
fun isDbEmpty(context: Context, param: OnCompleteCheckDB) {
val db = AppDatabase(context)
GlobalScope.launch(Dispatchers.IO) {
val count = db.movieDao().countElements() <= 0
withContext(Dispatchers.Main) {
param.isDBEmpty(count)
}
}
}
}
}
It works, but is it bad practice? What should I change if I wish to run it on the ActivityScope?
There'is lifecycleScope extension provided in lifecycle-runtime-ktx library, see Use Kotlin coroutines with Architecture components. Just add the library to your app's build.gradle
...
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
}
There's also viewModelScope available, if you're using ViewModel library. At first glance such a logic should be moved into a view model, so the query result will be retained during configuration changes. But let's use lifecycleScope since the question is about activity scope.
I've also replaced the callback by a suspend function. Coroutines are a great replacement for callbacks, so it's better to use coroutines when it's possible.
And one more thing, creating of a new AppDatabase instance multiple times doesn't look like a good idea. It's better to create it once and reuse it throughout your app. You can use Dependency Injection for that purpose, see Manage dependencies between components.
class DataBaseHelper() {
companion object {
suspend fun isDbEmpty(context: Context, param: OnCompleteCheckDB) = withContext(Dispatchers.IO) {
val db = AppDatabase(context)
db.movieDao().countElements() <= 0
}
}
}
class SplashScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen)
lifecycleScope.launch {
const dbIsEmpty = DataBaseHelper.isDbEmpty(this)
//handle result
}
}
}

Designing Modular Apps - Circular Dependency problem in navigation

As you know, designing Android app as modules is one of the popular practices nowadays in the Android development world. But this trend comes with some challenges. One of them is Circular Dependency.
For example, I have a navigation module which opens HomeActivity from Home Feature module. Also, I have to open another activity such as ProductListActivity from products module.
Home feature must include navigation module and navigation module should include HomeFeature if i navigate between activities like the following:
val intent = Intent(activity, HomeActivity::class.java)
This'll cause circular dependency problem.
For a fastest solution to figure out this problem is creating intents like the following and build navigation system on this approach.
Intent(Intent.ACTION_VIEW).setClassName(PACKAGE_NAME, className)
So my questions are, what other possible problems we'll face off with this navigation approach? Are there another practises to handle navigation in modular android apps?
Here is my solution for stiuation. This enables the use of explicit intents. You can also apply this approach to single activity application with navigation component with a little modification.
Here is navigation object for module B
object ModuleBNavigator {
internal lateinit var navigationImpl: ModuleBContract
fun setNavigationImpl(navigationImpl: ModuleBContract) {
this.navigationImpl = navigationImpl
}
interface ModuleBContract {
fun navigateModuleA(self: Activity, bundle: Bundle?)
}
}
And here is module B Activity
class ModuleBActivity : Activity() {
companion object {
private const val BUNDLE = "BUNDLE"
fun newIntent(context: Context, bundle: Bundle?) = Intent(context, ModuleBActivity::class.java).apply {
putExtra(BUNDLE, bundle)
}
}
}
And here is app module class to inject navigation impl to module A navigation object
class ApplicationModuleApp : Application() {
// Can also inject with a DI library
override fun onCreate() {
super.onCreate()
ModuleBNavigator.setNavigationImpl(object : ModuleBNavigator.ModuleBContract {
override fun navigateModuleA(self: Activity, bundle: Bundle?) {
self.startActivity(ModuleBActivity.newIntent(self, bundle))
}
})
}
}
And finally you can navigate from module A -> module B with provided implementation
class ModuleAActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ... Some code
ModuleBNavigator.navigationImpl.navigateModuleA(this, Bundle())
// .. Some code
}
}
This approact avoids circler dependency and you don't have to use implicit intents anymore.
Hope this helps.
For a different approach -actually similar which I mentioned in my question- which implementation belongs to sanogueralorenzo
Create a loader which laods the module classes
const val PACKAGE_NAME = "com.example.android"
private val classMap = mutableMapOf<String, Class<*>>()
private inline fun <reified T : Any> Any.castOrReturnNull() = this as? T
internal fun <T> String.loadClassOrReturnNull(): Class<out T>? =
classMap.getOrPut(this) {
try {
Class.forName(this)
} catch (e: ClassNotFoundException) {
return null
}
}.castOrReturnNull()
Create a String extension function for loading Intents dynamically.
private fun intentTo(className: String): Intent =
Intent(Intent.ACTION_VIEW).setClassName(BuildConfig.PACKAGE_NAME, className)
internal fun String.loadIntentOrReturnNull(): Intent? =
try {
Class.forName(this).run { intentTo(this#loadIntentOrReturnNull) }
} catch (e: ClassNotFoundException) {
null
}
Create another String extension function for loading Fragments dynamically
internal fun String.loadFragmentOrReturnNull(): Fragment? =
try {
this.loadClassOrReturnNull<Fragment>()?.newInstance()
} catch (e: ClassNotFoundException) {
null
}
Create an Feature interface for your feature implementations
interface Feature<T> {
val dynamicStart: T?
}
I assume that you have a Messages feature. Implement your dynamic feature interface
object Messages : Feature<Fragment> {
private const val MESSAGES = "$PACKAGE_NAME.messages.presentation.MessagesFragment"
override val dynamicStart: Fragment?
get() = MESSAGES.loadFragmentOrReturnNull()
}
And finally use it in another module without dependency
Messages.dynamicStart?.let {
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fl_main, it)
.commit()
}
}

Issue with automatically injecting activities with Dagger 2

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.

New Architecture with Dagger and Kotlin

I have a problem with New Architecture components in Kotlin, when I create ViewModel component in recomended way (in onCreate() method) the result is as suposed:
after activity orientation changes, I got the same instance of ViewModel as before
Here is the way i create this
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
val arrayMap = ArrayMap<Class<out ViewModel>, ViewModel>()
arrayMap.put(ListViewModel::class.java, ListViewModel(webApi, repoDao))
val factory = ViewModelFactory(arrayMap)
listViewModel = ViewModelProviders.of(this, factory).get(ListViewModel::class.java)
listViewModel.items.observe({ this.lifecycle }) {
Toast.makeText(this, it?.joinToString { it + " " } ?: "null", Toast.LENGTH_SHORT).show()
}
But when I have used Dagger for inject ListViewModel I got new instance of ListViewModel every time Activity was recreated. Here is a code of Dagger ListActivityModel.
#Module #ListActivityScopeclass ListActivityModule {
#Provides
#ListActivityScope
fun provideListViewModel(webApi: WebApi, repoDao: RepoDao, listActivity: ListActivity): ListViewModel {
val arrayMap = ArrayMap<Class<out ViewModel>, ViewModel>()
arrayMap.put(ListViewModel::class.java, ListViewModel(webApi, repoDao))
val factory = ViewModelFactory(arrayMap)
val result = ViewModelProviders.of(listActivity, factory).get(ListViewModel::class.java)
return result
}
}
Then ListActivity onCreate() method looks like:
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
listViewModel.items.observe({ this.lifecycle }) {
Toast.makeText(this, it?.joinToString { it + " " } ?: "null", Toast.LENGTH_SHORT).show()
}
}
And there is what I have notice after logging:
D/ListActivity: ---> onCreate() ListActivity: = [com.example.dom.app.new_arch.ListActivity#a0f2778]
D/ListActivity: ---> onCreate() listViewModel: = [com.example.dom.app.new_arch.ListViewModel#54a8e51]
//Activity orientation changes
E/ViewModelStores: Failed to save a ViewModel for com.example.dom.app.new_arch.ListActivity#a0f2778
D/ListActivity: ---> onCreate() ListActivity: = [com.example.dom.app.new_arch.ListActivity#6813433]
D/ListActivity: ---> onCreate() listViewModel: = [com.example.dom.app.new_arch.ListViewModel#55cf3f0]
The error I have received :
ViewModelStores: Failed to save a ViewModel for
comes from Android class HolderFragment with package android.arch.lifecycle.
There is something what I missed working with Dagger and new arch components?
The issue has to do with the order of dagger injection and activity creation. The view model implementation relies on a non-visual fragment for identity. By injecting the viewModelProvider before the activity has completed onCreate it is unable to complete this association.
Since super.onCreate does not likely depend on things you are injecting try injecting after the call to super.onCreate and you should be fine.
I had this exact same issue and solved it by this change in order.
Specifically from your code instead of:
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
go with:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_list)
JP
The way I do this is by only providing the ViewModelFactory using Dagger. Then it gets injected in the activity and you call ViewModelProviders.of(listActivity, factory).get(ListViewModel::class.java) from there. The reason your approach doesn't work is that AndroidInjection.inject() will create the ViewModel before onCreate, which leads to undefined behavior.
Also see: https://github.com/googlesamples/android-architecture-components/issues/202
I don't use AndroidInjection.inject() because it creates a new Dagger component. I create an Dagger Component in the Application class and I use that component instance to call inject in all other places of the app. This way your singletons are initialized only one time.

Categories

Resources