android

Android Modularization

Out of the box, Android Studio provides one module: the app module. Because of this, most developers write their entire application in this one module. This is fine for small teams and small applications. But, as an application grows, more team members are added and the application becomes more complex, build times can increase - with Gradle builds sometime taking up to 10-15 minutes - and developer productivity goes down.

One way to solve this problem for complex android applications is to modularize. Modularization means to divide a program into separate sub-programs (or modules) based on features. Each feature will be its own module.

Some advantages of modularization are:

  • Ease of incremental builds and deliveries
  • Smaller modules are easier to unit test
  • Modules can be added, modified, or removed without any impact on another module
  • Modules can be reused
  • Reused modules can lead to smaller APKs
  • Easily pluggable into Instant Apps
  • Increased developer productivity, as one person can have sole responsibility of a module

How do we split up a monolithic application into modules?

For business critical applications, discarding the current application and rewriting it from scratch is not an option. Therefore, the only option for critical apps is an iterative approach where you figure out the features that can be separated from the main application and do it in an orderly sequence with the ultimate goal being a set of feature modules.

The key to splitting up a monolithic application into modules is understanding the dependencies between features. This is the software’s architecture. Knowing this will allow you to make decisions on which modules can be split apart and how much work that will entail.

Here are the steps to follow:

  1. Understand what will be shared. This might seem counterintuitive, but if we didn’t do this we end up duplicating large amounts of code. Many modules share resources such as activities, fragments, and layout files. Looking at the application’s architecture can help identify the utilities that will be shared across the various modules. Looking at the “as-is” architecture can even identify where these utilities have incorrect dependencies.
  2. Understand how to split what isn’t shared. Modules can end up with definitions of objects that are partial views of the real object. For instance, a shipping module may not need the order history of the customer but does need to know the address where a product will be shipped. By understanding the dependencies from the shipping code, we can understand the actual dependencies that code has on different parts of our domain objects. This can help determine the code needed to interoperate between modules.
  3. Understand how to split up the data. Splitting up a database means understanding the dependencies within the database. The first thing to think about is foreign key-primary key relationships between tables. But what about stored procedures that access parts of the database? Are there embedded triggers in one of the tables that have dependencies on another table within the database? Again, looking at the architecture of the database (all the dependencies) can help you plan how you will split up the database
  4. Manage the evolution. Refactoring is never done in isolation. The business still needs new functionality and bug fixes. You need to be able to both add new features and fix bugs at the same time. This is where incremental refactoring can be beneficial. Update a few pieces of the architecture and see how the application is affected by the changes.

Conclusion

There are many benefits to modularizing (refactoring) your Android application: improved maintenance, improved quality, faster builds, and improved developer efficiency. Modularizing your architecture does not have to be a risky, error prone process that requires a massive rewriting the entire application. Instead, it can be accomplished in an orderly and incremental way that minimizes risk while bringing the benefits of modularization.

Lattix lets you see what your software architecture looks like today (“as-is”) and what you can do to modularize it. To see a demo or try our solution for yourself, click here.

Android Kernel: Lacking in Modularity

android-panda-dsm

We decided to take a look at the architecture of the Android Kernel. We selected the panda configuration for no particular reason - any other configuration would have worked just as well. The kernel code is written in C and it is derived from the Linux kernel. So, our approach will work on any configuration of the generic Linux kernel, as well.

Now we all know that C/C++ is a complex language and so we expect the analysis to be hard. But that difficulty just refers to the parser. Armed with the Clang parser we felt confident and were pleased that we didn't run into any issues. Our goal was to examine all the C/C++ source files that go in the panda configuration and to understand their inter-relationships. To do this, it was necessary to figure out what files are included or excluded from the panda build. And then there were issues dealing with how all the files were compiled, included and linked. That all took effort. The resulting picture showed how coupled the Linux kernel is.

First, let's acknowledge that the Linux kernel is well-written. What goes into it is tightly controlled. Given its importance in the IT infrastructure of the world, that is just what one would hope. Let us also remember that many of the modularity mechanisms in use today were invented in Unix. The notion of device drivers that plug into an Operating System was popularized by Unix and is commonplace today. Application pipes were pioneered by Unix. And yet, the Linux kernel itself has poor modularity.

Part of the problem is that that when Unix/Linux kernels were developed programming language support for modularity was poor. For instance, C does not have the notion of an interface and so dependency inversion is not naturally supported (it is possible, however). And, Linux has no real modularity mechanisms to verify or enforce modularity

A few things become apparent after a partitioning algorithm is applied. This partitioning algorithm reorders the subsystems based on dependencies, revealing what is "lower" and what is "higher." In an ideal implementation, the developers of the higher layer need only understand the API of the lower layers, while the developers of the lower layers need to worry about the higher layers only when an interface is affected. In a coupled system developers need to understand both layers making the job of understanding the impact of change considerably harder. Indeed, in the Android kernel where nearly all the layers are coupled, developers may sometimes have to understand thousands of files to feel confident about their changes.

This also means is that the intent behind the decomposition has been lost. For instance, 'arch.arm' is so strongly coupled with 'kernel' that it is hard for developers to understand one without understanding the other. Notice how even the 'drivers' are coupled to rest of the system. I experimented by creating a separate layer for the base layer of the drivers and I even moved some of the basic drivers such as 'char' and 'tty' and yet the coupling remained. Sadly, even some of the newer drivers are also coupled to the kernel.

All this goes to show that unless there is a focus on architecture definition and validation, even the best managed software systems will experience architectural erosion over time.

If you would like to discuss the methodology of this study or if you would like to replicate the results on your own, please contact me (neeraj dot sangal at lattix dot com). You can peruse a Lattix white paper on the Android kernel for some more details.