Now, edit the Dependencies.kt file and add Kotlin code to declare the dependencies and versions as constant immutable variables. To have a bit of separation, versions are declared inside a singleton named Versions and the dependency inclusion rule is declared inside a singleton named Deps:


// 1
object Versions {
  // 2
  const val kotlin = "1.2.50"
}

// 3
object Deps {
  // 4
  const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
}

Here you:

  1. Define an object (singleton in Kotlin) named Versions.
  2. Define a constant named kotlin and assign it a value that is the version of Kotlin language plugin you are using.
  3. Define another object named Deps.
  4. Define a constant named kotlinStdLib and assign it a value that is the string for the Kotlin standard library dependency. Note how the kotlinStdLib value can reference values from the Versions singleton.

You can declare more variables here such as your minSdkVersion, targetSdkVersion, versionCode and even plugin version. Update the Dependencies.kt file to include all the version numbers and dependencies you need:


object Versions {

  // Build Config
  const val minSDK = 14
  const val compileSDK = 27
  const val targetSDK = 27

  // App version
  const val appVersionCode = 1
  const val appVersionName = "1.0.0"

  // Plugins
  const val androidGradlePlugin = "3.1.3"

  // Kotlin
  const val kotlin = "1.2.50"

  // Support Lib
  // const val support = "27.1.1"
  // Comment above and uncomment below to cause conflict in dependency
  const val support = "26.0.1"
  const val constraintLayout = "1.1.0"

  // Testing
  const val junit = "4.12"
  const val espresso = "3.0.2"
  const val testRunner = "1.0.2"

}

object Deps {

  // Plugins
  const val androidGradlePlugin = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}"

  // Kotlin
  const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
  const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"

  // Support Library
  const val appCompat = "com.android.support:appcompat-v7:${Versions.support}"
  const val supportAnnotations = "com.android.support:support-annotations:${Versions.support}"
  const val constraintLayout = "com.android.support.constraint:constraint-layout:${Versions.constraintLayout}"

  // Testing
  const val junit = "junit:junit:${Versions.junit}"
  const val espressoCore = "com.android.support.test.espresso:espresso-core:${Versions.espresso}"
  const val testRunner = "com.android.support.test:runner:${Versions.testRunner}"
}

This is all you need to set up the whole dependency management system. You’ll use this now in your app module.

Open the app module’s build.gradle file, go to the dependencies block and replace the Kotlin stdlib dependency by typing Deps.ko. Wait for autocomplete to suggest kotlinStdLib to you:


// Replace this
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

// With 
implementation Deps.kotlinStdLib

Dependency Management

Update the full dependencies block to be:


dependencies {
  implementation fileTree(dir: 'libs', include: ['*.jar'])

  // Kotlin
  implementation Deps.kotlinStdLib

  // Support Libraries
  implementation Deps.appCompat
  implementation Deps.constraintLayout

  // Testing
  testImplementation Deps.junit
  androidTestImplementation Deps.testRunner
  androidTestImplementation Deps.espressoCore
}

I’ll leave it for you as a challenge to use the other version numbers from Dependencies.kt. For example, the compileSdkVersion becomes:


compileSdkVersion Versions.compileSDK

Check out the final project if you run into any trouble.

Once you’re done, you can sync your project with Gradle and run the app. You will see the app runs successfully as before:

Starter app

You now have autocompletion and code navigation (in Android Studio) for jumping to definitions enabled for your dependencies from any module. You only need to make the changes once in the Dependencies.kt file and it works across all modules.

Handling Dependency Conflicts

It is pretty common to encounter conflicts when you move to a cross-module dependency management system like the one explained above. For example, one of your modules might transitively depend on a different version of a dependency but another module depends on another version of the same dependency. Having different versions of the same dependency can cause inconsistencies in the project.

You can use Gradle to define strategies around dependency resolution in the event of conflicts. For example, for forcing certain dependency versions, using substitutions, and conflict resolutions.

First, you set up the project to show version conflicts. Add the following code block after the dependencies block in the app module build.gradle file, replacing // TODO: Add Dependency Conflict Management block here:


// 1
configurations.all {
  // 2
  resolutionStrategy {
    // 3
    failOnVersionConflict()
  }
}

Here you:

  1. Declare the task that will monitor all configurations.
  2. Define the resolution strategy in case of conflicts.
  3. Fail eagerly on version conflict (includes transitive dependencies), e.g., multiple different versions of the same dependency (group and name are equal).

Add a new dependency to your app module build.gradle file:


implementation "com.android.support:support-annotations:26.0.1"

Next, in Dependencies.kt change the version of support:appcompat-v7 specified in the support constant to 27.1.1 from 26.0.1. Sync the project with your Gradle files, and you will see that the project doesn’t compile and instead shows the below error:

Dependency Conflict

Why did this happen? Because one of the dependencies depends on a different version of support:appcompat-v7 library.

To fix this conflict, update the resolutionStrategy block as below:


configurations.all {
  resolutionStrategy {
    failOnVersionConflict()

    // 1
    preferProjectModules()

    // 2
    force 'com.android.support:support-annotations:26.0.1'

    // 3
    forcedModules = ['com.android.support:support-annotations:26.0.1']

    // 4
    dependencySubstitution {
      substitute module('com.android.support:support-annotations:27.1.1') with module('com.android.support:support-annotations:26.0.1')
    }
  }
}

Here you:

  1. Prefer modules that are part of this build (multi-project or composite build) over external modules.
  2. Force certain versions of dependencies (including transitive), append new forced modules.
  3. Force certain versions of dependencies (including transitive), replace existing forced modules with new ones.
  4. Add dependency substitution rules, e.g., replace one dependency with another.

    Note: This property is incubating and may change in a future version of Gradle.

What you have done is told Gradle to force the version the 26.0.1 for the support:support-annotations in all configurations.

Once these changes are added and Gradle is synced, there will be no more conflict errors! Great work at fixing them!

Run the app. You will see the app runs successfully as before:

Starter app

Implementing APK Splits for Native Code by Platform

For those of you who work with native C and C++ code in Android projects, you probably have been bundling the compiled native code and the native shared library for all platform types (armeabi-v7a, arm64-v8a, x86, x86_64) along with your generated .apk file, leading to huge .apk sizes.

What if I told you there is a simple trick to make your APK file smaller by only bundling compiled native code for only one platform type and then generating a different APK for each? Gradle makes it possible once again, via the splits block

Note: From the official documentation:

The splits block is where you can configure different APK builds that each contains only code and resources for a supported screen density or ABI.

You can even split your APK based on the density of pixels and, with a little bit of Groovy magic, you can make sure each of these .apk files has a version that is dynamically generated.

To use splits, simply add the splits code block and two other items within the android block in your app module’s build.gradle file, replacing // TODO: Add .apk splits block here:


android {
  ...
  // Add this block
  splits {

    // 1
    abi {

      // 2
      enable true

      // 3
      reset()

      // 4
      include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"

      // 5
      universalApk true
    }

    // 6
    density {

      // 7
      enable false

      // 8
      exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
    }
  }

  // 9
  project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]

  // 10
  android.applicationVariants.all { variant ->
    // assign different version code for each output
    variant.outputs.each { output ->
      output.versionCodeOverride =
          project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
              100000 +
              android.defaultConfig.versionCode
    }
  }
}

A line by line explanation is as follows:

  1. Configures multiple APKs based on ABI.
  2. Enables building multiple APKs.
  3. By default, all ABIs are included, so use reset() and include to specify the APKs you want.
  4. Specify a list of ABIs for which Gradle should create APKs.
  5. Specify that you want to also generate a universal APK that includes all ABIs.
  6. Configure multiple APKs based on screen density.
  7. Turn off density-based APKs; set to true to turn on.
  8. Exclude these densities when building multiple density-based APKs.
  9. Map for the version code that gives each ABI a value.
  10. Iterate over all artifacts that are generated, and assign each with proper versioning.

Once you have added this new code, sync the project with your Gradle files. Next, go to the Build top menu option and click MakeProject:

Make Project

When the make process finishes, navigate to the app/build/outputs/apk/debug/ folder in your Project view to see the split .apk files by platform types:

Split APKs

Also, here are the size differences:

Before and after apk split

For a simple app like this, the size savings are small, but they can be more significant for larger projects.

Good job!

Building Flags for Profit

Often times, you will have a requirement in which you need to modify the behavior of your app based on the type of build. For example, for your debug build, you might want to enable logs but disable logging in your release builds, or simply use a different endpoint URL for a different build type.

You can use Gradle to handle these requirements pretty gracefully.

You are able to pass variables from your build to both Java and Kotlin code and to native code:

Java and Kotlin Code

The Gradle plugin provides a feature called the buildConfigField, which, once declared inside build.gradle for a particular build type, is generated as static properties of the BuildConfig class. The BuildConfig class is a special class that is generated by Gradle at build time. It contains other information like version, version code, etc.

Give it a spin and declare your own variables. Add the code below to your debug build type, replacing // TODO: Declare debug specific buildConfigField and // TODO: Add app_type string resource here with value specific to debug type. Then define the equivalent for release block:


buildTypes {
    ...
    debug {
       // 1 
       buildConfigField "String", "SERVER_ENDPOINT", '"http://www.myendpoint.dev.com"'
       buildConfigField "boolean", "ENABLE_LOGS", "true"
       buildConfigField "String", "PLAYTIME_STARTED", '"No"'

       // 2 
       resValue "string", "app_type", "Debug"
    }
    ...
 }

Here you:

  1. Define your buildConfigField variables and give them values.
  2. Add a new generated resource, in particular, a string resource item generated at build time.

Likewise, you can do this for other build types and modify the values.

Next, sync your project and Gradle files. You can now access the variables you defined in your build.gradle file, via the BuildConfig class file in your Java and Kotlin code.

Navigate to your MainActivity.kt file and replace // TODO: Append custom defined build variables to the textToDisplay, inside btnLetsPlay.setOnClickListener lambda with:


textToDisplay.append("Playtime started : ${BuildConfig.PLAYTIME_STARTED}").append("nn")
textToDisplay.append("Android logs enabled : ${BuildConfig.ENABLE_LOGS}").append("nn")
textToDisplay.append("Server endpoint: ${BuildConfig.SERVER_ENDPOINT}").append("nn")
textToDisplay.append("App type: ${getString(R.string.app_type)}").append("nn")

Build and run the app. Click the Let’s Play button. You will see the following:

Build Flags

Native Code

Accessing flags defined at build time from your native code — i.e., C/C++ code — is a bit tricky. To understand the process, you will need to understand how the native code is compiled.

Your native code is first passed to a compiler — i.e., gcc/clang — and then is passed on to the CMake tool, which adds other shared libraries to the classpath. So to be able to pass a flag to your native code, you need to pass an argument to the compiler first. To do that, Gradle provides the externalNativeBuild block.

Now, based on what kind of toolchain you are using to compile your native code, you can either have ndkBuild or cmake set up for you. In the current example, you are using the CMake toolchain.

The way you pass arguments to the compiler is defined in the help manual for the compiler (clang):


-D<macroname>=<value>
  Adds an implicit #define into the predefines buffer which is read before the source file is preprocessed.

To pass values to your native code, you will use something like the following:


// Use this to pass arguments to your C++ code:
cppFlags.add("-D<macroname>=<value>")

// Use this to pass arguments to your C code:
cFlags.add("-D<macroname>=<value>")

You can read more about these here.

Open your build.gradle file for the app module and add the below code under your build_type/externalNativeBuild/cmake, replacing // TODO: Declare flags to be passed to native code, where build_type can be any of your build types such as debug or release:


debug {
  ...
  externalNativeBuild {
    cmake {
      // Passed to your compiler i.e clang/gcc, hence available to your c/c++ code
      cppFlags.add("-DNDK_ENABLE_LOGS=true")
      cppFlags.add("-DNUMBER_OF_KIDS=3")
    }
  }
}

Sync the project with your Gradle files and navigate to app/src/main/cpp/native-lib.cpp via the Project view.

Inside native-lib.cpp, replace // TODO: Add LOGD statements to log flags set at build time with the following snippet:


 LOGI("Number of kids: %d", NUMBER_OF_KIDS);

 // Flag shown in logs would be as per: true == 1, false == 0
 LOGW("Logs enabled: %d", NDK_ENABLE_LOGS);

Now, build and run your app. Next, and click the Let’s Play button. Open up Logcat
Logcat Tab

Enter Native in the filter box to see the values as read from your native code, the values of which were defined in your build.gradle file:

Native Logs in Logcat

Wow, you are getting good at this! Great work!

Configuring Modules

Setting up different package names, resource prefixes, version suffixes for build types

This is a pretty common use case. You have multiple build types and you want to define the package name, version name and debuggable properties on each one differently. Gradle has a solution for you.

Add the following properties to your debug build types, replacing // TODO: Modify application id and version name and // TODO: setup debug flags:


buildTypes {
    ...
    debug {
      // 1
      applicationIdSuffix ".debug"

      // 2
      versionNameSuffix "-debug"

      // 3
      debuggable true

      // 4
      jniDebuggable true

      // 5
      renderscriptDebuggable false
      ...
   }
   ...
}

A line-by-line explanation:

  1. The Application ID suffix is appended to the “base” application ID when calculating the final application ID for a variant.
  2. The Version name suffix is appended to the “base” version name when calculating the final version name for a variant.
  3. Set whether this build type should generate a debuggable APK.
  4. Set whether this build type is configured to generate an APK with debuggable Native code.
  5. Set whether the build type is configured to generate an APK with debuggable RenderScript.

By modifying these in your build types, you can change the values accordingly per build type.

Now, sync your project and Gradle files, and navigate to app/build/intermediates/manifests/full/debug/universal/ via the Project view.

Next, open the AndroidManifest.xml file in this folder. This is the generated and merged manifest file for this build type. Here, you will see the changes in package name, version name and the debuggable flag being set in the manifest file:

Debuggable Flag

Also, if you build and run the app, then click the “Let’s Play” button, you can see the values in the text views inside the app:

Packagename Suffix and debug flag

If the build fails when you try to build and run the app, try cleaning the project by choosing Build ▸ Clean Project from the Android Studio menu.

Speeding Up Your Build Times

If you have been working with Android for awhile, you are aware of huge build times that Android projects can sometimes have.

Huge build times like these

Yeah, sometimes it can get out of hand. :]

You can fix this and speed up your builds by tweaking your Gradle settings a little bit.

Navigate to the gradle.properties file of your project via the Project view:

gradle.properties file

Append the below flags for the Gradle build system:


# 1
org.gradle.jvmargs=-Xmx2048m

# 2
org.gradle.daemon=true

# 3
org.gradle.configureondemand=true

# 4
org.gradle.parallel=true

# 5
android.enableBuildCache=true

# 6
org.gradle.caching=true

Here’s an explanation, line-by-line:

  1. Specifies the JVM arguments used for the Gradle Daemon. The setting is particularly useful for configuring JVM memory settings for build performance.
    • -Xmx2048m: Increase the amount of memory allocated to the Gradle Daemon VM to 2 Gb.
  2. When set to true, the Gradle Daemon is used to run the build. The default is true starting with Gradle v3.0.
  3. For Gradle to know exactly how to build your app, the build system configures all modules in your project, and their dependencies, before every build — even if you are building and testing only a single module. This slows down the build process for large multi-module projects. Setting this field will make Gradle attempt to configure only necessary projects.
  4. When set to true, this will force Gradle to execute tasks in parallel as long as those tasks are in different projects.
  5. When set to true, this will force Gradle to not run a task if its inputs and outputs are equivalent to what they were during its previous execution.
  6. When set to true, Gradle will reuse task outputs from any previous build, when possible.

With these settings enabled, your builds should be considerably faster. Build and run your app:

Successfull Build

Apart from the first build, all subsequent builds should be faster than the last one; the build time for your app would be much reduced. That’s Gradle magic for you. Enjoy this huge time saver for playing more video games!

Another way to reduce your build times: Offline Mode

If you are on a slow network connection, your build times may suffer when Gradle attempts to use network resources to resolve dependencies. You can tell Gradle to avoid using network resources by using only the artifacts that it has cached locally.

To use Gradle offline mode:

  1. Open the Preferences window by clicking File ▸ Settings (on Mac, Android Studio ▸ Preferences).
  2. Search for “offline.”
  3. Check the Offline work checkbox and click Apply or OK.

Gradle offline mode

After you have enabled offline mode, build and run your app. You will see your app up and running with a much faster build time.

Successfull Build

If you’re building from the command line, pass the –offline option.


$ ./gradlew build --offline

When using offline mode, if you add a new dependency to one of your modules, you’ll need to make sure your connected to a network and disable offline mode temporarily in order to sync the new dependency.

Playing With Product Flavors

Often, you would like to have different versions of your Android app for different audiences, such as Free, Pro, Beta, etc. Gradle has a feature called Product Flavors, which represents different versions of your app that you may release to users.

Furthermore, you can define different package names and version names for these flavors. To do so, you add the following snippet within the android block inside your app/build.gradle file, replacing // TODO: Add Product Flavors block here:


android{
  ...
  // Add this block
  productFlavors {
    // Free Product Flavor
    free {
      applicationIdSuffix ".free"
      versionNameSuffix "-free"
    }

    // Beta Product Flavor
    beta {
      applicationIdSuffix ".beta"
      versionNameSuffix "-beta"
    }

    // Pro Product Flavor
    pro {
      applicationIdSuffix ".pro"
    }
  }
  ...
}

Product flavors, by default, combine with build types to generate Build Variants, which are nothing but combinations of your product flavor with build type, e.g., freeDebug, freeRelease, betaDebug, etc.

Now sync the project with your Gradle files. Open the BuildVariants tab in Android Studio and switch to the freeDebug variant:

Build variants

Check out the few changes that you added in your product flavors. Navigate to app/build/intermediates/manifests/full/free/debug/universal/ via the Project View and open AndroidManifest.xml. This is the generated and merged manifest file for freeDebug build variant. Here, you see the changes in package name and version name in the generated manifest file.

Note how the package name now has .free.debug appended and version name has -free-debug appended:

freeDebug Variant

Now, build and run the app. Click the Let’s Play button. You will see:

Successfull Build

Increasing Productivity With Gradle

Inspecting the dependency graph

Sometimes, you just want to inspect the dependency graph of your project. Gradle has just the right thing for you called the androidDependencies tasks. It generates a graph of all dependencies your project pulls in, even transitive ones.

First, open a Terminal:

terminal tab

Now, execute the following in the Terminal, passing the androidDependencies task to the Gradle wrapper:


$ ./gradlew androidDependencies

You will see the following output:

dependency graph

It is also possible to execute this Gradle task from the UI in Android Studio. Simply open the Gradle side tab on the right-hand side and navigate to your_project/Tasks/android/ and double-click androidDependencies. This will execute the task:

android dependencies

You will see the output in the Run window:

output in run window

Testing Builds via Dry Run

There are times when you may wish to see all the tasks that will be executed during the build, but you don’t want to execute them. This is a so-called dry run of your build. You can use the dry run of a build to see if the task dependencies are defined properly.

To execute this, from the Terminal, pass the –dry-run option to Gradle wrapper, as shown below:


$ ./gradlew build --dry-run

You will see the build task execute successfully but that all the sub tasks are skipped. That is because you only wanted to dry run the particular task. You will see the following on a dry run execution:

dry run

Where to Go From Here?

Phew, that was a lot of stuff, but you really did make it through all the training! Now, you are a master in the art of Gradle-fu!

You can find the final project in the tutorial .zip file, which you can download using the Download materials button at the top and bottom of the tutorial.

Gradle is a solid and very customizable build automation system. It makes the life of developers so much better by handling a lot of the nuances around the build system in a graceful manner — making the build system a joy to work with.

Hopefully, after finishing this tutorial, you have a solid understanding of how you can leverage the powerful features provided by Gradle and the Android Studio Gradle plugin to make your development/build cycle more approachable and easier to work with.

If you want to learn more about working with Gradle for Android Development, checkout Google’s official documentation.

I hope you enjoyed this tutorial on exploring Gradle and for Android Development. If you have any questions or comments, please join the forum discussion below!



Source link https://www.raywenderlich.com/5532-gradle-tips-and-tricks-for-android

LEAVE A REPLY

Please enter your comment!
Please enter your name here