Build / Modules / Muselee

Muselee 2: Basic Modules

Muselee is a demo app which allows the user to browse popular music artists. It is not intended to be a fully-featured user app, but a vehicle to explore good app architecture, how to implement current best-practice, and explore how the two often go hand in hand. Moreover it will be used to explore how implementing some specific patterns can help to keep our app both maintainable, and easy to extend.

Previously we looked at how important it is to have a strong, coherent strategy for handling dependency version, and mentioned starting with a three module structure consisting of core, topartists, and app modules . We also briefly mentioned how important it is to limit what we put in to the core module, and in this article we’ll look in to this further.

One of the big problems with having a multi-module project is correctly determining which module should contain a particular piece of code. In many cases where the code in question is part of a specific feature, it will naturally go in to the appropriate feature module. This is the easy bit. When it becomes a little harder is where the code may be used within more than one feature module. In such cases, the tendency is to simply put it in to core, but doing this can ultimately affect your build speeds.

The problem arises because most builds that developers will be doing while developing new features or fixing bugs will be incremental builds during which only the parts that have actually changed will be re-built.

In pure Java/Kotlin modules this is highly efficient because only the classes that have changed need to be re-compiled, and then the JAR file can easily be rebuilt using the new .class files for the source files that have changed, along with the .class files from previous builds for the source files which haven’t changed.

Android modules are rather more complex because they do not consist solely of Java/Kotlin code, but also such things as resources, manifests, assets, renderscript, and native code to name but a few (and as all projects will use different combinations of these, I won’t give any hard rules in this article because all projects will have different constraints). Also the file structures of both APKs and AABs are rather more complex than a JAR file. A JAR file consists of the zipped .class files plus some metadata, whereas in an APK, .class files are collected in to a .dex file, which is more efficient at runtime; resources are converted to a binary form, which is also more efficient at runtime; etc., etc.

Moreover there is also code generated during the build even when we are not explicitly using any third-party libraries (such as Dagger2 / Glide) which work using code generation. An example of this is R.java. This is a java file which is created by the Android Asset Packaging Tool (AAPT) as part of resource packaging. Whenever AAPT is run, it maps each resource name to a unique numeric ID, and R.java is a mapping of the human-readable resource name to that numeric ID. It is what permits us to include named resources within our Java / Kotlin code:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

The real value of this becomes apparent when we understand that successive invocations of AAPT will result in consistent numeric IDs being used provided that nothing changes. However, if we add or remove resources, then actual values of these numeric IDs may change. If our code relied on precise numeric values which could change, then this would cause no end of problems. However, the mappings in R.java protect us from this because the mappings within R.java will be updated as the numeric IDs change, so we can use a value of R.layout.activity_main safe in the knowledge that it will still work even if the numeric value of the resource assigned to R.layout.activity_main changes.

Although we seem to have shifted attention away from our three modules, understanding what happens internally is vitally important to keeping our build times as short as possible. Let’s first consider what happens if we make changes to the resources in a given module. That will cause R.java to be re-created, which may have knock-on effects with Java / Kotlin files which depend on this needing to be re-compiled. Now if we consider the knock on effects of changing resources within the core module upon which everything other module depends. This can cause all other modules to have to be re-built. But if we made the resource change in topartists instead, it would only affect that module and its dependencies (in this case app). When we start adding more feature modules, changing a single one of them will have a much smaller impact on the next build than making the change to core which will pretty much trigger a full rebuild.

Hopefully, the problems of having a very large core module should be pretty apparent. If too much of your code lives within this module, the chances are that you will need to change it frequently, and any changes to this module will require a much longer build. So think very carefully about what goes in there.

If there is a a piece of functionality which is required by more than one feature module, then the tendency is to put it in to core. However, any changes to that functionality may require modules which do not use it to be rebuilt if it changes. Therefore you should only put code in to core which everything else requires. For functionality that is required by a sub-set of your modules, consider adding a shared library which is common to only the modules which require it. Therefore changes to that common module will only affect its consumers. We’ll look at this in greater depth later in the series.

In some cases it may be possible to avoid using a core module altogether, and if it is possible then it is certainly worth considering but not if it requires you to violate principles such as DRY. In actuality most projects will have a small section of the codebase which is required by all modules, and this is what should live in core, but that module should not become the home for everything which does not sit cleanly within a single feature module.

In the next article we’ll turn our attention to how we can use Dagger 2 dependency injection to help wire up these modules in a clean way.

Although we haven’t actually added any code within this article, the current project is available here.

© 2019 – 2018, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.