Why I Don't Recommend Aspect-Oriented Programming in Android in 2023
Background
In the past, I had written articles about using Aspect-Oriented Programming (AOP) in Android, including a setup/overview article guide and, a few years later, my thoughts on switching to a new AOP plugin. These were pretty popular, as far as articles on such a niche topic go, just behind this guide which is likely more popular. As such, I feel like it's my responsibility to take a more critical stance and talk about why I wouldn't recommend using AOP in Android in 2023. I won't go too much into what AOP is in this article, so if you are curious take a look at one of the articles mentioned previously to see what kinds of things you can do with AOP, even if I don't recommend it.
I will stop short of saying you should never use Aspect-Oriented Programming in Android, but there are substantial hurdles to leveraging it effectively in an Android project in 2023. My complaints will speak only to the tooling I have worked with personally (AspectJ, various Android/AOP plugins, etc.), so there may be other tools that solve the problems I will outline below. Finally, I will mention a few use cases where it might make sense to use AOP still.
Criticisms of AOP in an Enterprise Application
One of the main problems that I have with AOP is the fact that the code needs to be maintained by someone in the organization, and this someone usually gets labeled "Subject Matter Expert" and asked to work on related issues. This invariably leads to the inner workings of whatever system is using AOP to be siloed. While this is not only a problem with AOP, it is exacerbated by the fact that AOP is not a common pattern used in Android development, so many developers on the team would not even be aware of AOP in the first place.
If information about a common tool, such as Dagger, is siloed within a company and the developer responsible for its maintenance leaves, it can be problematic. However, this may not be a significant issue since the organization would know that they were using Dagger and at least be able to look at the official documentation and other sites such as StackOverflow. In all likelihood, the team would be able to pick up the pieces and understand the tool in order to continue development.
For projects using AOP, that may not be the case. For example, I had previously worked on a project that leveraged AOP for analytics. Hypothetically, if button click analytics stopped working suddenly, there would be no way for us to know why. Let's take a look at an example click listener:
view.setOnClickListener {
println("clicked")
}
This listener is not very interesting, but if we imagine that it had been previously tracking analytics and is now broken, we have a few avenues we can explore.
- We can try git blame to see if the file has changed. Since all of the click analytics stopped working in this hypothetical scenario, it is unlikely to be the culprit, but never hurts to check. In this case, let's assume nothing has changed.
- We can search the code for analytics packages. Depending on the structure of your project, this might be fruitful. In some cases though, aspects will be stored in a separate .aar or binary to be ingested into multiple apps. In this case, you might not find anything.
- You can check if any dependencies have changed recently. But what if the only difference you can find is a Kotlin upgrade or gradle update that is required to support some new Android version? Where does that leave you? Sure, you can revert the update but ultimately, you will need to resolve the underlying problem to update your app.
At this point, you might try to enlist the help of others to see if they can figure out what's going on. Maybe someone on the team has heard of AOP and is aware of AspectJ, meaning they can point you in the right direction. Even in that case, you will face hardships since it might become non-obvious whether the aspects themselves are at fault or if there is a problem with your tooling or AOP plugin.
We'll stop with the hypothetical for now but hopefully you can see that when you begin to dig into the issue, it will become more and more complex and it will likely have less external documentation that you can turn to for help than a more widely used tool or pattern in Android. Sure, AOP as a whole is not that uncommon and even in Java, AspectJ and the Spring Framework leverage AOP and are pretty popular as far as I can tell. However, in Android specifically, the tooling is not always well maintained and has to deal with an ever-changing landscape of Android Gradle Plugin (AGP) APIs to integrate into the build process and also an ever-evolving Kotlin language.
Technical Limitations
One of the old AOP Plugins that I had recommended in the past was android-gradle-aspectj. One of its main problems was that every time there was an AGP update, the plugin would begin failing. If I remember correctly, this was usually at compile time, so at least it was obvious that the change broke something but it would prevent staying up-to-date with other dependencies as I would usually prefer.
As far as I know, no AspectJ plugin for Android is officially supported (i.e. maintained by Google, the AOSP, or even JetBrains). As such, android-gradle-aspectj is an open-source project maintained by the community. Due to this fact, the time between releases can be months or even years since contributors would need to meet all of the following criteria:
- Have a need to use an AOP plugin
- Knowledgeable about plugin development
- Able to understand and apply updates properly
At the time of writing, the android-gradle-aspectj plugin is bottle-necked by Pull Requests as none have been merged by the maintainers in over a year-and-a-half, and the PR merged before that was almost a year prior. Because of this, using this plugin without forking it yourself would require you to use an AGP version over 1.5 years old. Not ideal.
Around the time this plugin stopped getting regular updates (although contributors were trying to get changes merged and making forks of their own), I had done a bit more research and wrote Switching AspectJ Plugins in Android. This article goes a bit more in-depth on the AGP issues I mentioned previously and talked about a much newer plugin, gradle-aspectj-pipeline-plugin (sorry that the names are so similar!).
This new plugin seemed to work pretty well and not break for every new AGP update, which was great! Up until now, the plugin has generally worked with all of the AGP versions and I have been able to update my sample project easily in regards to AGP, even if this plugin didn't have all of the functionality of the first. Unfortunately, cracks in the seams did begin to appear.
AOP in a Kotlin-first environment
Since 2019, Google has pushed for a Kotlin-first approach in Android. Anecdotally, I have enjoyed working in Kotlin since being introduced to it and have seen codebases that I have worked on gradually diminish in the amount of regular Java code they contain, which has been great.
Another thing that has also been great is the steady pace of updates to Kotlin and the Kotlin compiler, which often leads to performance improvements and reduced APK size. Although Kotlin and Java are interoperable and AspectJ will work on both, changes in language specifications can mean that the underlying generated code is slightly different than expected, meaning that the defined AOP pointcuts do not match. I found one such issue when upgrading to Kotlin 1.5.0. The underlying issue was that Kotlin 1.5.0 uses invokedynamic for compiling Single Abstract Methods, which theoretically has benefits but broke AOP in the sample app, yielding a silent failure. No compilation issue occurred, but the aspects were no longer applied as expected.
Luckily, in the "What's new in Kotlin 1.5.0" article, they provided a way to get around this issue, by setting a compiler option to use the old method:
kotlinOptions { freeCompilerArgs = ["-Xsam-conversions=class"] }
kotlinOptions { freeCompilerArgs = listOf("-Xsam-conversions=class") }
Since this is just a workaround, it would be ideal to fix the underlying issue, but because I am not an expert, the solution was non-obvious to me. Further, the eventual solution might not be something that could be fixed in the plugin and instead require writing extra pointcuts that could match the new Kotlin 1.5.0 lambdas. In the end, the AOP plugin just updated its documentation to mention adding the compiler argument to the and this Kotlin lambda The Kotlin 1.4.32 and 1.5.0 compiler (without setting extra compiler arguments) generated the following bytecodekotlinOptions
block when leveraging Kotlin 1.5.0 and above.Click here to see more technical information on this problem if you are curious about the difference in generated bytecode.
With this pointcut@Pointcut("execution(void *.onClick(..))")
fun onButtonClick() {
}
button2.setOnClickListener {
Toast.makeText(this@MainActivity, "Button 2 clicked", Toast.LENGTH_SHORT).show()
}
.class final Lme/jdvp/androidaspectexample/activity/MainActivity$onCreate$2;
.super Ljava/lang/Object;
.source "MainActivity.kt"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lme/jdvp/androidaspectexample/activity/MainActivity;->onCreate(Landroid/os/Bundle;)V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x18
name = null
.end annotation
.annotation runtime Lkotlin/Metadata;
d1 = {
"\u0000\u000e\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0010\u0000\u001a\u00020\u00012\u000e\u0010\u0002\u001a\n \u0004*\u0004\u0018\u00010\u00030\u0003H\n"
}
d2 = {
"<anonymous>",
"",
"it",
"Landroid/view/View;",
"kotlin.jvm.PlatformType"
}
k = 0x3
mv = {
0x1,
0x5,
0x1
}
xi = 0x30
.end annotation
# instance fields
.field final synthetic this$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
# direct methods
.method constructor <init>(Lme/jdvp/androidaspectexample/activity/MainActivity;)V
.registers 2
.param p1, "$receiver" # Lme/jdvp/androidaspectexample/activity/MainActivity;
.line 1
iput-object p1, p0, Lme/jdvp/androidaspectexample/activity/MainActivity$onCreate$2;->this$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final onClick(Landroid/view/View;)V
.registers 5
.param p1, "it" # Landroid/view/View;
.line 34
invoke-static {}, Lme/jdvp/androidaspectexample/aspect/AspectLogging;->aspectOf()Lme/jdvp/androidaspectexample/aspect/AspectLogging;
move-result-object v0
invoke-virtual {v0, p1}, Lme/jdvp/androidaspectexample/aspect/AspectLogging;->onClickAdvice(Landroid/view/View;)V
iget-object v0, p0, Lme/jdvp/androidaspectexample/activity/MainActivity$onCreate$2;->this$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
check-cast v0, Landroid/content/Context;
const-string v1, "Button 2 clicked"
check-cast v1, Ljava/lang/CharSequence;
const/4 v2, 0x0
invoke-static {v0, v1, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 35
return-void
.end method
.class public final synthetic Lme/jdvp/androidaspectexample/activity/-$$Lambda$MainActivity$i-qbjTK8eb3QNeeBxGWVSmW89UI;
.super Ljava/lang/Object;
.source "lambda"
# interfaces
.implements Landroid/view/View$OnClickListener;
# instance fields
.field public final synthetic f$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
# direct methods
.method public synthetic constructor <init>(Lme/jdvp/androidaspectexample/activity/MainActivity;)V
.registers 2
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lme/jdvp/androidaspectexample/activity/-$$Lambda$MainActivity$i-qbjTK8eb3QNeeBxGWVSmW89UI;->f$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
return-void
.end method
# virtual methods
.method public final onClick(Landroid/view/View;)V
.registers 3
iget-object v0, p0, Lme/jdvp/androidaspectexample/activity/-$$Lambda$MainActivity$i-qbjTK8eb3QNeeBxGWVSmW89UI;->f$0:Lme/jdvp/androidaspectexample/activity/MainActivity;
invoke-static {v0, p1}, Lme/jdvp/androidaspectexample/activity/MainActivity;->lambda$i-qbjTK8eb3QNeeBxGWVSmW89UI(Lme/jdvp/androidaspectexample/activity/MainActivity;Landroid/view/View;)V
return-void
.end method
Kotlin 1.7.0 also broke the plugin, although in a more straightforward way that was easier to understand. The plugin had been able to be updated without incurring extra work on consumers.
It's worth noting that these issues would likely also impact other AOP plugins, so they aren't unique to the gradle-aspectj-pipeline-plugin plugin. Even with these issues, the amount of technical problems I have experienced with the plugin has been reduced when compared to the previous plugin. Unfortunately, that seemingly won't always be the case.
With the upcoming release of AGP 8.0, the Transform APIs that the gradle-aspectj-pipeline-plugin plugin uses for bytecode transformation will be removed. The maintainers for the project have updated the README to indicate that the project will not be maintained going forward. Although there are theoretically other AGP APIs that can be used, this plugin is backed by a company that has decided to no longer leverage AOP and as such will not be updated to use them. Although they have indicated that they would be willing to have other maintainers take over, the number of contributors is low and I personally would probably be on the shortlist as someone has contributed to the project and raised many of the aforementioned issues. As I am in the position of writing this kind of article in the first place, and also don't use the paradigm in my professional development anymore, I do not have the appetite to do so.
If you are using AOP and this plugin specifically, you will probably have to find another plugin and waste development time implementing the new one and learning its idiosyncrasies, assuming you can even find a plugin that meets your needs. If you had started using AOP in your Android app five years ago, you would have potentially been blocked dozens of times by the plugin breaking due to AGP changes, switching plugins due to lack of support, having your pointcuts silently break due to Kotlin changes, and now have to update your plugin again. Definitely not ideal.
Kotlin Compose
Another thing to consider is that use of Kotlin compose is on the rise. Let's take a look at some sample code:
@Composable fun TopLevelScreen( viewState: TopLevelScreenState, eventHandler: (TopLevelScreenEvent) -> Unit ) { Column(Modifier.fillMaxSize()) { SubComposableA(viewState.subStateA, eventHandler) SubComposableB(viewState.subStateB, eventHandler) SubComposableC(viewState.subStateC, eventHandler) } }
sealed interface TopLevelScreenEvent { object SaveClicked: TopLevelScreenEvent object SearchClicked: TopLevelScreenEvent data class SearchQueryEntered( val queryEntered: String ): TopLevelScreenEvent }
If I wanted to do general monitoring or log analytics for this using AOP, which are some of the more likely cross-cutting concerns AOP is used for, I would have a hard time getting started. To be frank, I did try to add a few pointcuts to ComposeButton onClick events in my sample project but it seemed non-trivial and I didn't get it working after a couple of hours. Even if I were to get it working, I would have to maintain a set of Compose and non-Compose pointcuts unless the app was fully in Compose.
However, if we take a step back, it would appear (to me) that AOP is not necessary in this case. Since all of the events route through the same event handler, that single event handler can monitor the events and do logging or analytics work as required in a single place without using AOP. Depending on the app's architecture, you might need logging code in multiple different VMs, but at least then you have the benefit of being able to unit test that the functionality is working.
I don't know how common using the more unidirectional data flow style of Compose is, where events are routed to the VM for processing in a sort of MVI manner. To me, it appears not to be entirely uncommon and could help solve your logging needs without leveraging AOP paradigms.
When it might make sense to use AOP
While I don't think it makes sense to use in an enterprise application or on teams where the knowledge can become siloed, I think it's alright to AOP in apps developed by a single individual who will remember what technologies they've used, given that:
- The developer has a real need for AOP - they have cross-cutting concerns they want to keep track of in one place or want to do something like adding analytics to every button in an existing app without much effort.
- The developer understands that getting any plugin to work may be tedious upfront and will require ongoing maintenance work as tooling changes, whether that be AGP, the Kotlin compiler, or other tooling changes altogether.
- The developer can either wait for open source fixes or is capable of making the plugin changes themselves on a forked version of the plugin and doesn't necessarily need the latest updates all the time.
- The developer doesn't have better alternatives. I mentioned the Compose example above, but a good app architecture may save you from needing to use something like AOP in the first place.
Final thoughts
Although I personally would not recommend using AOP anymore, I've learned a lot over the years as I debugged using it (LOL) and still think it's pretty cool. Aspect-Oriented Programming may not have a place in modern Android development anymore (in my opinion) due to enterprise concerns and technical limitations, but there may be niche places where it is still useful to developers.
What do you think? Please let me know in the comments below if you feel differently or think I am missing something. Are you using AOP in your app to great success with a plugin I haven't heard of before? Or are you using AspectJ directly in your app's build process without a plugin? Let me know; I would like to hear about it!
March 26, 2023 Update
Various small punctuation and grammar fixes have been made after reading through this article again.
March 28, 2023 Update
I decided to investigate whether the gradle-aspectj-pipeline-plugin was actually broken by AGP 8.0.0. Surprisingly, I discovered that it was not. For more details on my research, please refer to this discussion, but the key takeaway is that the existing plugin doesn't rely on the Transform APIs at all. As a result, the plugin continues to work with AGP 8.0.0 and 8.1.0 (at least the beta and alpha versions, respectively) without needing updates.
However, the original maintainers of the plugin do not plan to maintain the project any longer, as it is no longer in use at their company. They encountered several issues that I had not previously mentioned:
- Testing AOP functionality can be cumbersome and unwieldy, as seen in the plugin's unit tests or my sample project's UI tests.
- AGP and Android build API documentation can be difficult to understand and limited in scope for more in-depth topics. Despite receiving direct support from Gradle, the Ibotta team was unclear about whether the
registerPostJavacGeneratedBytecode
method was part of the removed Transform API, as they had assumed all bytecode APIs were the same. - AspectJ pointcuts can rely heavily on reflection, potentially impacting device performance. Additionally, renaming an advised method and forgetting to update the pointcut can lead to non-obvious bugs.
I recommend reviewing the issue I raised for insights from an enterprise app team that has used AOP. Although the plugin will remain functional in the next major AGP release, I advise caution when considering adding AOP to your application in 2023.
There are currently no comments on this article, be the first to add one below
Add a Comment
Note that I may remove comments for any reason, so try to be civil. If you are looking for a response to your comment, either leave your email address or check back on this page periodically.