Aspect-Oriented Programming in Android

Update June 18, 2020: The sample project has been updated to use the latest stable Android Gradle Plugin release (4.0.0) and is now using Kotlin for the example. If anyone has any issues, feel free to reach out!
Aspect-Oriented Programming (AOP) is a paradigm that allows for cross-cutting concerns to be taken care of without cluttering your files with the same redundant code everywhere. This article will try to give a broad overview of why you’d want to use AOP, how it works, and then give some concrete examples of usage.

AOP in Android: The Why

Have you ever found yourself copying the same code for every time an action occurs? For instance:

For all of the above cases, we could do something simple such as copying the relevant code multiple times and it might not even look too bad. For example, if we have multiple buttons and we want to log the button clicks we could just simply write the following:

button1.setOnClickListener(v -> {
    String text = ((TextView) v).getText().toString();
    loggingViewModel.logItem(text);
    //some other actions performed when clicking Button 1
    }
});

button2.setOnClickListener(v -> {
    String text = ((TextView) v).getText().toString();
    loggingViewModel.logItem(text);
    //some other actions performed when clicking Button 2
    }
});

This would work well enough. But what if we had hundreds of buttons or the method of logging was more than 2 lines? This could quickly become cumbersome but could also lead to a problem of forgetting to actually do the logging in some cases.

With Aspect-Oriented Programming, we can write the logging code once and guarantee that any buttons we currently have onClickListeners for and any we add in the future will have the logging behavior that we are aiming to achieve.

AOP in Android: The How

Most AOP libraries that can be used in Android add tasks to the gradle build that ‘weaves’ code into the specified places. This injection of code can be specified in .aj files that contain aspect language or through regular java classes using annotations. I personally prefer the annotation approach since it makes the code easier to debug.

In order for the aspect weaver to figure out where code should be injected, we write Pointcuts - expressions specifying where code should be injected. These can have as narrow a scope as a single method or as broad a scope as any method in any class (which probably wouldn’t realistically be very useful). Here are some examples:

@Pointcut("execution(void *.onClick(..))")

The pointcut can be broken into multiple parts that can be used to figure out which methods will be affected. The execution portion specifies what type of designator we are using. Execution is usually the designator we will use as it designates that we will inject code where the specified method(s) are executed. For a list of designators and what they do see section 8.2.3 of this guide.

The stuff wrapped inside the execution designator specifies what methods to match. These have the signature of [return type] [fully qualified method name]([argument types]). We can either specify the types we want or use wildcards. In the above, we are matching void methods that are in any file (*. before the method name) that have any or no arguments (.. specifies any amount of arguments).

@Pointcut("execution(* *.activity.*.onCreate(android.os.Bundle))")

The above will match any method named onCreate with any return type as long as it has a single argument of type android.os.Bundle and and lies within a package named activity. As imagined, pointcuts can become complex but luckily, they can be chained together using && and || statements.

So if a pointcut specifies where injected code should go, an advice specifies what code is injected. An advice can be simply thought of as a method that will be called where the pointcut specifies. The advice also specifies at what point it should be called in relation to the method designated by the pointcut. For instance, you can specify that the method should be run before, after, or around the pointcut-specified method.

Taken together, a pointcut and an advice is a single aspect, which is where AOP gets its name.

Actual implemenation

If we want to actually implement AOP, we should choose a library to do the weaving for us. I personally use GradleAspectJ-Android by Archinmon on Github since it has worked well for me in the past but I believe that AspectJ should work just as well. If you do go with the first option, there is a setup guide in the readme. However, no matter what you choose the following should be pretty similar assuming annotation-based aspects are allowed. After setting up the dependency, create a file for storing your aspects. You might consider have multiple files to keep things cleaner, e.g. one for logging, one for security, one for analytics, etc.

Next, write an empty method for the pointcut you want to add. For instance, if I am tracking button clicks, I might write the following:

@Pointcut("execution(void *.onClick(..))")
public void onButtonClick() {}

After you have a pointcut defined, you can write an aspect. The aspect should specify when the code should run in relation to the pointcut using the @Before, @After, or @Around annotations. Before will cause the code to run in the aspect to run before the code in the targeted method and after will cause the aspect to run after code in the targeted method. The Around annotation is quite a bit more strange and powerful. Around can allow your aspect to run both before and after the target code and even to run instead of the target code if that’s what you decide. There are a few other types that can be read about in section 8.1.1 of this guide.

Say we decide that we want to log our button clicks to the console every time any button is clicked but before the onClickListeners actually do anything. In that case we might write the following aspect.

@Before("onButtonClick()")
public void onClickAdvice() {
    //todo
}

This is good and code would be weaved but we actually don’t know what button was clicked so logging might be useless. Luckily, we can specify that we want to capture the arguments to the targeted method using the args designator! We specify a name in the args designator and then use that name in conjunction with a type in the advice signature. This will then guarantee that only methods that have the specified argument type will be matched when doing injection. Thus we will get this method:

@Before("onButtonClick() && args(view)")
public void onClickAdvice(View view) {
    if (view instanceof TextView) {
        String text = ((TextView) view).getText().toString();
        loggingViewModel.logItem(text);
    }
}

We specify that we are looking for a View object as the argument to the method. In this case the name ‘view’ doesn’t matter and is only used within the aspect and would match no matter what the actual source code had named the View item.

The above code runs. If you build your project at this point, you will log every time a button is clicked (assuming you have added buttons with onClickListeners to your project). You can check this manually, or you check the build directory for a file named ajc-transform.log that will specify where code has been injected and if there were any issues. If you don’t have any issues you’ll see something like this:

Sun Jul 01 16:22:25 EDT 2018
Join point 'method-execution(void me.jdvp.androidaspectexample.activity.MainActivity$2.onClick(android.view.View))' in Type 'me.jdvp.androidaspectexample.activity.MainActivity$2' (MainActivity.java:37) advised by before advice from 'me.jdvp.androidaspectexample.aspect.AspectLogging' (AspectLogging.class(from AspectLogging.java))
   
Join point 'method-execution(void me.jdvp.androidaspectexample.activity.MainActivity$1.onClick(android.view.View))' in Type 'me.jdvp.androidaspectexample.activity.MainActivity$1' (MainActivity.java:30) advised by before advice from 'me.jdvp.androidaspectexample.aspect.AspectLogging' (AspectLogging.class(from AspectLogging.java))

This tells us that our onClickAdvice advice from AspectLogging.java matched two separate items in MainActivity. Nice!

At this point, you can add new onClickListeners for new buttons and they will automatically have this logging attached when the project is built. But these kinds of pointcuts are only the start. You can write more complicated pointcuts that match based on inherited interfaces rather than specific packages or method names or choose only methods that have specific annotations for instance. If you have questions about those kinds of things feel free to ask!

I hope that this article could provide some high-level insight into reasons why you might want to leverage Aspect-Oriented Programming techniques in your own projects and that the provided examples were helpful. A sample project with this implemented can be found here on my Github. Be sure to let me know if you have any suggestions or questions! Thanks!

Wiki on Aspect-Oriented Programming and Cross-Cutting Concerns

Spring guide to AOP. I used this guide a lot when trying to figure out how to use pointcuts. It was especially useful for more complicated things such as targeting implementing classes of interfaces and annotation-based pointcuts since it provides a lot of samples.

This article that I found about AOP in Android specifically. Uses a different library but the ideas are the same!

---
This article is also available on Medium. You can find it here.
Filed under:
Android
Aspect-Oriented Programming

Comments

Add a Comment