Muselee / Version Management

Muselee 1: Library Versions

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.

In a previous series about Maintainable Architecture we looked at an existing app and saw how we could refactor it to make it more maintainable and make use of things such as the Jetpack component. Although there will be some similarity, for Muselee we will be creating an app from scratch and explore how making sensible decisions early on can significantly simplify things as our project grows. Of course everything that we’ll cover will be highly subjective, and include my opinions and interpretation on current best-practice. Feel free to either copy or ignore what I’m advocating, but it is important to understand why I am advocating these things. You may decide that another approach results in similar or even better benefits.

Before we even begin to look at feature development let’s think about our project structure and how to configure our build. For current Android apps the best build-time performance for developers can be achieved by breaking projects down in to individual modules. Although a full build will usually take a little longer by doing this, the times where individual developers will be performing full builds should be relatively few and far between, and incremental builds will be performed most of the time. Where a specific module and its dependencies have not changed since the last build, it can be skipped during an incremental build. So structuring our project well can significantly reduce build times for developers (but not necessarily on the CI which should be doing full builds each time).

Our app will have three modules initially:

  • core – common code for all feature modules
  • topartists – a feature module which displays a list of popular artists
  • app – the app itself

Both core and topartists are Android Library modules, and app is an Android Application module. core does not depend on any other project libraries; topartists depends on core; and app depends upon both core and topartists.

It is important to try and keep core as clean and focused as possible. It is easy to put lots in to core, but this defeats the object. If core is constantly changing, then other modules that depend on it may need to be rebuilt each time it changes resulting in longer build times. Furthermore, it can quickly become a single, monolithic core module, and monoliths are what we are trying to avoid here because they will make life much harder for us further down the line. We’ll dive in to what goes in to core in the next article.

With these three basic modules there will be a number of common third-party library dependencies, such as junit and kotlin-stidlib-jdk[7|8]. Where we have common dependencies such as these we need to keep the versions consistent otherwise subtle, and difficult to track down bugs can appear in our app. If there are breaking API changes between distinct versions of a specific third-party library, we may find that the build starts failing because different modules expect different APIs. What can be even worse is where different modules expect different behaviours from different versions of the same library and introduce subtle behaviour differences in to the app. It is best to avoid these kinds of problem, and the easiest way to do that is to ensure that we consistently use the same version of any given third-party dependency throughout the app. Doing this manually is prone to errors, particularly as the number of modules increases, therefore it is important to try to manage this cleanly.

The approach that I tend to use is one that I have seen others using and adds a dependencies.gradle file to the top level folder of your project:

ext {
    androidMinSdkVersion = 21
    androidTargetSdkVersion = 28
    androidCompileSdkVersion = 28

    androidBuildToolsVersion = '3.4.0-alpha09'

    jetpackVersion = '1.1.0-alpha01'
    constraintLayoutVersion = '2.0.0-alpha3'
    kotlinVersion = '1.3.11'
    ktxVersion = '1.1.0-alpha03'

    junit4Version = '4.13-beta-1'

    buildPlugins = [
            androidGradlePlugin: "com.android.tools.build:gradle:$androidBuildToolsVersion",
            kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    ]

    libraries = [
            kotlinStdLib    : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion",
            appCompat       : "androidx.appcompat:appcompat:$jetpackVersion",
            constraintLayout: "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion",
            ktxCore         : "androidx.core:core-ktx:$ktxVersion",
    ]

    testLibraries = [
            junit4: "junit:junit:$junit4Version",
    ]

    projectModules = [
            core      : project(':core'),
            topartists: project(':topartists')
    ]
}

Initially we declare the actual versions of various libraries that we’ll be using along with common declarations of our minSkdVersion, targetSdkVersion, and compileSdkVersion levels. Then we declare a number of maps for different collections of dependencies. We have different maps for plugins that are used in the build script; third party libraries used in the app and feature code; third party libraries that are used in the test code; and finally the modules within the project. For this last map, I haven’t included the app module because nothing else will depend upon it.

We need to ensure that the build.gradle files in each of our modules has access to these, and we can do this by adding a single line to the top level build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    apply from: 'dependencies.gradle'
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath buildPlugins.androidGradlePlugin
        classpath buildPlugins.kotlinGradlePlugin
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Just adding the apply from line means that we can now use these values within both the build script dependencies in this file, and the build.gradle files for each module:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion androidCompileSdkVersion
    defaultConfig {
        applicationId "com.stylingandroid.muselee"
        minSdkVersion androidMinSdkVersion
        targetSdkVersion androidTargetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation projectModules.core
    implementation projectModules.topartists

    implementation libraries.kotlinStdLib
    implementation libraries.appCompat
    implementation libraries.ktxCore
    implementation libraries.constraintLayout

    testImplementation testLibraries.junit4
}

This is the build.gradle from the app module,but the others are quite similar. We use the common androidCompileSdkVersion, androidMinSdkVersion, and androidTargetSdkVersion that we declared earlier. We then use the libraries and testLibraries maps for the individual dependencies.

There is one further thing that we can do to help monitor our external dependencies, and that is to use Ben Manes’ Versions plugin. We can add this in our top level build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    apply from: 'dependencies.gradle'
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath buildPlugins.androidGradlePlugin
        classpath buildPlugins.kotlinGradlePlugin
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }
}

plugins {
    id "com.github.ben-manes.versions" version "0.20.0"
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

We can get a dependency version report by invoking ./gradlew dependencyUpdates:

------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - androidx.appcompat:appcompat:1.1.0-alpha01
 - androidx.constraintlayout:constraintlayout:2.0.0-alpha3
 - androidx.core:core-ktx:1.1.0-alpha03
 - com.android.tools.build:gradle:3.4.0-alpha09
 - com.android.tools.lint:lint-gradle:26.4.0-alpha09
 - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.20.0
 - junit:junit:4.13-beta-1
 - org.jetbrains.kotlin:kotlin-android-extensions:1.3.11
 - org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.11
 - org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.11

Gradle updates:
 - Gradle: [5.1-rc-3: UP-TO-DATE]

Generated report file build/dependencyUpdates/report.txt

This shows all of the external dependencies and the versions being used. It will detect any anomalies in the versions that we are using, and will also check for newer versions of any third-party dependencies. This plugin does not affect the APK or AAB that is produced, it merely provides a reporting task which you can run periodically to check that you are using consistent and up-to-date versions of third party libraries.

Intelligent version management of third-party dependencies is really important in multi-module projects, and adopting a sensible strategy early on in a project can avoid many problems later on.

In the next article we’ll start looking at the three modules that we added and consider their roles and responsibilities.

Although there’s not much to see yet and the app really doesn’t do very much, the source code is available here.

© 2018, Mark Allison. All rights reserved.

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

7 Comments

  1. I love what you are doing here. Are you looking at doing this yourself or are you allowing others to do pull requests?

    1. At the moment I have a pretty clear road map laid out for where this is headed – there’s quite a few iterations and accompanying blog posts already in progress, so I won’t be accepting pull requests for the time being.

      1. I am excited to see where you go with this. This is great material to mentor newer engineers and help us old timers improve.

  2. Hi Mark,
    Little question about build.gradle:
    seems that you added kotlin gradle plugin dependency twice – first time using buildPlugins.kotlinGradlePlugin and second time using string definition below. Am I right or these definitions are different?

Leave a Reply to Mark Cancel 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.