Return internal class calling object function - android

I'm testing with Kotlin and I'm writing a small library to be imported and used by a test App project.
In the library project I marked my classes as internal because I don't want them to be visible for the App project, but I would like to have a single entry point for the library, and for that I am using a Kotlin object like shown below
LIBRARY
object Library {
fun getComponent() = AwesomeComponent()
}
internal class AwesomeComponent() {
// some implementation
}
TEST APP
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val component = Library.getComponent()
}
}
The problem is that this doesn't compile because the function in the Library object returns an internal type and therefore need to be marked as internal as well, but doing so would hide the function from the TestApp.
Another option would be to not have the internal modifier at all so the TestApp can see the Library method, but then it can also see the classes inside the Library project
Is there an easy solution that I am overlooking here or does it need to go through re-planning of packages and structure of the Library project? (not sure how to do it in that case)

You have to publish some sort of public API for the app module to be able to use the component that the getComponent() method returns. If you want to publish minimal information about your library, you can have it return an interface that contains only the publicly available method calls to the library, and make your class implement that interface:
object Library {
fun getComponent(): IAwesomeComponent = AwesomeComponent()
}
interface IAwesomeComponent {
// methods you want to call on the component in the app module
}
internal class AwesomeComponent(): IAwesomeComponent {
// implementations of the interface methods
}

Related

Koin Context Isolation: setup correctly and avoid random NoBeanDefFoundException

I have an Android library module that I package and publish. It's a dependency in different apps, some that use Koin and some that don't, therefore I want to use Koin's context isolation.
Having followed the docs, I added a koin component and context as follows:
internal object LocalKoinContext {
lateinit var koinApplication: KoinApplication
}
// Custom KoinComponent using the local instance & not the Global context
interface CustomKoinComponent : KoinComponent {
// override the used Koin instance to use the local koin application instance (LocalKoinContext.koinApplication)
override fun getKoin(): Koin = LocalKoinContext.koinApplication.koin
}
and add create the koinApplication like this:
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
I have an activity that uses a view model that lives within the local Koin component and I used to instantiate the koinApplication within the onCreate of the that activity:
class FirstActivity : AppCompatActivity(),
CustomKoinComponent {
private val sharedViewModel by viewModel<MySharedViewModel>()
...
override fun onCreate(savedInstanceState: Bundle?) {
...
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
}
The issue is that I get random NoBeanDefFoundException
Caused by org.koin.core.error.NoBeanDefFoundException: No definition found for class:'my.package.name.MySharedViewModel'. Check your definitions!
at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.java:264)
at org.koin.core.scope.Scope.resolveInstance(Scope.java:233)
at org.koin.core.scope.Scope.get(Scope.java:204)
at org.koin.androidx.viewmodel.factory.DefaultViewModelFactory.create(DefaultViewModelFactory.java:11)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at org.koin.androidx.viewmodel.ViewModelResolverKt.get(ViewModelResolverKt.java:23)
at org.koin.androidx.viewmodel.ViewModelResolverKt.resolveInstance(ViewModelResolverKt.java:12)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:86)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:72)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel(ViewModelStoreOwnerExtKt.java:68)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel$default(ViewModelStoreOwnerExtKt.java:58)
at my.package.name.FirstActivity$special$$inlined$viewModel$default$1.invoke(FirstActivity.java:75)
I suspect it's because sometimes the koinApplication doesn't have time to set itself up but as it's rare I haven't been able to reproduce the issue.
In the answer to this stack question, the dev creates a ContentProvider to instantiate the koinApplication. I've tried it out and so far so good (although, I will only know in months from now), but is this truly the way to go? Seems to be an overkill to have to create a content provider just for that.
Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?
Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?
You should do this inside your Application level class, for example this is what I have on my Application class onCreate
startKoin {
androidLogger(Level.ERROR)
androidContext(this#VeroApp)
modules(AppModules.modules) <-- list of defined koin modules
}
I'm not so sure about the usage of local KoinApplication interface. I know that AppCompatActivity for example is out of the box supported and you can just do a without extending anything
private val sharedViewModel by viewModel<MySharedViewModel>()

Android Annotation Processor: Invoke annotated method with a condition

I'm quite new to building a custom annotation processor,
I've followed some tutorials & guides over here on SO too but been stuck on adding a condition for the annotated method.
The problem:
I have a custom annotation directed only for methods,
say, #RUN(isDebug: Boolean) & the method would be:
#RUN(isDebug = true)
private fun runInDebugOnly() {
....
}
#RUN(isDebug = false)
private fun runInReleaseOnly() {
....
}
So in my Annotation Processor,
is it possible to execute these functions with a condition?
I know the concept of generating a custom class & methods inside it,
But how to exactly intercept the method & use the generated method instead.
Any guidance would be appreciated.
An annotation processor only runs at compile time, and is usually used to generate new classes or code.
Sounds to me like you want to generate a new method at compile time which will call the correct annotated method depending on the build type.
e.g. when you run a debug build you want to have
fun myNewMethod() {
runInDebugOnly()
}
but when you run a release build you want to have
fun myNewMethod() {
runInReleaseOnly()
}
The rest of your app will just call myNewMethod() and it won't care about the implementation.
You could achieve this another way without using an annotation processor
fun myNewMethod() {
if (Build.DEBUG) {
runInDebugOnly()
} else if (Build.RELEASE) {
runInReleaseOnly
}
}
Is this the kind of thing you're after?

Show Fragment from Dynamic Feature Module in base (app) module?

In my base module (app) I have a few Fragments. I would like to put one of them in a Dynamic Feature Module, which would get installed on-demand. Until the user decides to install this module, I would just show an empty placeholder instead of that Fragment. I know it's easy if it's an Activity from Dynamic Feature Module, but I would really need to show this Fragment in my base module Activity.
Is this possible?
You can use this way (Kotlin)
// example
val className: String
get() = "$MODULE_PACKAGE.ui.login.LoginFragment"
fun instantiateFragment(className: String) : Fragment? {
return try {
Class.forName(className).newInstance() as Fragment
} catch (e: Exception) {
// not install feature module
null
}
}
To enable interaction between the app module and feature modules, one can use dependency injection or service locator pattern. The app can locate the feature module's implementation class and invokes its public API which returns the fragment instance in the app module and load that in main Activity's container.
for ex: create a Feature interface in app and corresponding Impl in feature module. Then locate/inject this Impl class instance and invoke its function to get the fragment reference.

Multi module library with code obfuscation

I have a suite of libraries distributed as 3 separate aars. Is there any way to be able to obfuscate each library independently, but keep the ability to call methods between modules internally?
E.g. if libA needs to call myMethod from libB, but I don't want myMethod exposed to clients integrating my libraries (yet all libraries must be obfuscated).
Right now I'm forced to make myMethod public and exclude it from obfuscation so it can be called from libA. Is there a better solution to this problem that won't expose myMethod to clients while still be obfuscated?
I am unsure if you have already found a solution to your problem but I have played a little and find out a working solution. I made a small HelloWorld app to show how it is done: https://github.com/jmineraud/multi-module-android-app.
It contains the repo for the lib as a git submodule (https://github.com/jmineraud/multi-module-android-lib)
The trick is to obfuscate the libraries independently, but not to optimize them (we do not want to remove any function) and that all dependent submodule must reuse the same mapping.txt file.
For instant in my submodule proguard file, I added:
-dontshrink
-dontoptimize
#-useuniqueclassmembernames # Option does not work with R8
-repackageclasses "com.your.package" # I do not want to obfuscate the whole hierarchy to keep my obfuscated library class separate and avoid obfuscation problems when the lib is used in the app project (which will be in turn obfuscated)
-applymapping "../lib-core/build/outputs/mapping/release/mapping.txt"
The classes I want to keep are annotated with #Keep from androidx.annotation.Keep
It's a best practice that library expose only interface and not Objects.
Also, interface must respect interface segregation principle.
If your client do not need to use the method, your library too. Your libA is the first customer of your LibB.
Your libraries must be compliant with single responsibility principle.
I think you have to re-work library and extract interface for your classes and expose only interfaces.
I think that this will be a tiny task, a quick solution is to add a proguard rules for each protected method if your are using java.
-keep public class mypackage.MyPublicClass {
protected void myProtectedMethod();
}
Sample case for refactoring:
//libA
class LibA {
lateinit var libB:LibB
fun serviceA(){
libB.checkLicence()
}
}
//libB
class LibB{
fun serviceB(){}
//this method is needed by libA, due to obfuscation visibility is public instead of internal
//internal fun checkLicence(){}
fun checkLicence(){}
}
the refactored code maybe:
//new library distributed only internally not to customer
// LibInternal
interface InternalLib {
fun checkLicence()
}
internal class InternalLibImpl : InternalLib {
override fun checkLicence() {
}
}
object InternalLibFactory {
fun create(): InternalLib = InternalLibImpl()
}
//LibB
interface LibB {
fun serviceB()
}
class LibBImpl(private val internalLib: InternalLib) : LibB {
override fun serviceB() {
internalLib.checkLicence()
}
}
object LibBFactory {
fun create(): LibB = LibBImpl(InternalLibFactory.create())
}
//LibA
interface LibA {
fun serviceA()
}
internal class LibAImpl(private val libB: LibB, private val internalLib: InternalLib) {
fun serviceA() {
internalLib.checkLicence()
libB.serviceB()
}
}
object LibAFactory {
fun create(): LibB = LibBImpl(InternalLibFactory.create())
}

Third party library of dynamic feature module cannot access resources

I have a application which has a dynamic feature module. In dynamic feature module, there is a form with images, input fields and also it has a buttton which access another third-party library.
Third-party library has a activity and fragment. While opening fragment inside activity, I am receiving below error, although there is container in activity's layout:
No view found for id 0x7f080053 (com.app.sample:id/container) for fragment SampleFragment{eed53f7 (5e4c0693-09a2-4725-a6de-1df49dd818f0) id=0x7f080053}
When accessing drawables in this third-party library, getting below error:
java.lang.NoSuchFieldError: No static field ic_back of type I in class Lcom.third.library/R$drawable; or its superclasses (declaration of 'com.third.library.R$drawable' appears in /data/app/com.app.sample-QtC8XuamC1fHEVU4FUpWaA==/split_thirdparty.apk)
It is fine when I use this library in a application without dynamic feature module.
Generally, when SplitCompat.installActivity(this) isn't called in Activity2, this won't work. While not having the source code, you'd have to extract the package and re-package it properly, because the Activity2 (or even the whole library package) likely isn't compatible with DFM.
After you enable SplitCompat for your base app, you need to enable SplitCompat for each activity that your app downloads in a dynamic feature module.
Here's another answer of mine, which demonstrates access through reflection.
Dynamic Delivery is relatively new feature so it has a lot of limitations. One of those limitations it that you cannot access code and resources of a Dynamic Module in a conventional way, thus it cannot be a dependency for other modules. Currently you can access Dynamic Module via reflection and having dynamic features defined through public interfaces in a common library module and loading their actual implementations (located in the dynamic feature modules) at runtime with a ServiceLoader. It has its performance downsides. They can be minimized with R8 using ServiceLoaderRewriter but not completely removed.
While using reflection is very bug prone we can minimize it either with #AutoService — AutoService is an annotation processor that will scan the project for classes annotated with #AutoService, for any class it finds it will automatically generate a service definition file for it.
Here is small example of how it is done
// All feature definitions extend this interface, T is the dependencies that the feature requires
interface Feature<T> {
fun getMainScreen(): Fragment
fun getLaunchIntent(context: Context): Intent
fun inject(dependencies: T)
}
interface VideoFeature : Feature<VideoFeature.Dependencies> {
interface Dependencies {
val okHttpClient: OkHttpClient
val context: Context
val handler: Handler
val backgroundDispatcher: CoroutineDispatcher
}
}
internal var videoComponent: VideoComponent? = null
private set
#AutoService(VideoFeature::class)
class VideoFeatureImpl : VideoFeature {
override fun getLaunchIntent(context: Context): Intent = Intent(context, VideoActivity::class.java)
override fun getMainScreen(): Fragment = createVideoFragment()
override fun inject(dependencies: VideoFeature.Dependencies) {
if (videoComponent != null) {
return
}
videoComponent = DaggerVideoComponent.factory()
.create(dependencies, this)
}
}
And to actually access code of Dynamic Feature use
inline fun <reified T : Feature<D>, D> FeatureManager.getFeature(
dependencies: D
): T? {
return if (isFeatureInstalled<T>()) {
val serviceIterator = ServiceLoader.load(
T::class.java,
T::class.java.classLoader
).iterator()
if (serviceIterator.hasNext()) {
val feature = serviceIterator.next()
feature.apply { inject(dependencies) }
} else {
null
}
} else {
null
}
}
Taken from here. Also there a lot more info there so I would recommend you to check it.
Generally I just would not recommend to use Dynamic Feature as dependency and plan your app architecture accordingly.
Hope it helps.
For the resources, this code part can be usage
R.id.settings would be:
getResources().getIdentifier("settings", "id", "com.library.package");

Categories

Resources