多语言展示
当前在线:1788今日阅读:103今日分享:49

Building Multiple Editions of an Android App

A mobile application we’ve worked on recently had a special requirement. The app would be released multiple times, under different name, branding and minor feature differences. This can very easily turn into a mess quickly, if all the apps are not maintained in a single code base.The way you would usually approach this is by building the whole app as an Android library project, and you would then build an Android application project for each edition. That can work, but you would need a lot of work to get the library/app separation to work, because your library is actually an application, and that could cause some confusions.However, we were already migrating our Android development to Android Studio, and the new build system (Gradle) has a specific solution to this problem: meet Flavors.Flavors are basically different editions of the application, that are built from a single code base. The library approach is no longer necessary, and the whole process is just streamlined. Through this post, we’re going to configure a build of an application with two flavors, take a look over the process quickly and see how to differentiate the two flavors with different graphics or features.
工具/原料
1

Android Studio 1.1

2

Mac OSX 10.10.2

方法/步骤
1

Our base applicationWe’re going to use Android Studio for this app, it would still be relatively easy to apply this without Android Studio though. First, let’s create a new project MultiFlavorSampleApp, with the default empty Activity.This will get you started with a default app/build.gradle, it should be similar to this:build.gradle

2

Adding a FlavorNow we want to add flavor that builds a different edition of the application. Since we don’t have any flavors, we’ll have to define 2 flavors, one for the default edition, and another for the flavor we want to add. Let’s call the default edition “Vanilla”, and the other flavor “Strawberry”.We’ll need to add a productFlavors section inside the android section.productFlavors {        vanilla {         }        strawberry {        }    }After modifying your build.gradle, sync your Android Studio with the new changes.

3

Now if you open the “Build Variants” view, you can easily switch between build variants from within the IDE. A build variant is basically a combination of flavors and build types ( by default debug and release ). So, with 2 flavors, you get 4 build variants by default.

4

Now, first thing you would want to do is provide different package names per flavor, so you can distribute them separately.Update: Check post comment regarding packageName.productFlavors {        vanilla {            packageName 'com.example.multiflavorapp'        }         strawberry {            packageName 'com.example.multiflavorapp.strawberry'        }    }From the build variants window you can now just change the variant and you’re good to go. So far though, we’re just building the same app with a different package name, now let’s start the fun.Note: The packageName is not supported any more. Instead, using applicationId.

5

Providing Alternate ResourcesNow we’re going to start customizing the application per flavor, starting with resources. As we’ve created a new project in Android Studio, I’ll assume you now have the default project structure:app/|--libs/|--src/   |--main/      |--java/      |  |--...      |--res/      |  |--layout/      |  |  |--activity_main.xml      |  |--...      |--AndroidManifest.xmlIn this structure, main is your default source directory (i.e. the “unflavored” source). So, where you would put your flavor customizations? in a flavored directory, that is. Let’s say we want to provide a different layout file for activity_main.xml in strawberry flavor. If you follow the same default structure, you won’t need to modify your Gradle script, you just need to provide an alternative source in the strawberry source directory, so basically you will have this structure:app/|--libs/|--src/   |--strawberry/   |  |--res/   |     |--layout/   |     |  |--activity_main.xml   |--main/      |--java/      |  |--...      |--res/      |  |--layout/      |  |  |--activity_main.xml      |  |--...      |--AndroidManifest.xmlNote: You may face minor inconveniences creating files (code files or resources) under a flavor that’s not currently selected from Android’s Build Variants view. Selecting the right flavor before working on its source directory would be more convenient, but not necessaryNow if you build strawberry, you’re going to get the strawberry/res/layout/activity_main.xml layout. If you build vanilla, you’ll get the main/res/layout/activity_main.xml because it wasn’t overwritten.Note: Be ware of the ids you’re using in the layouts. If you add new ids in strawberry‘s activity_main.xml that are not in main‘s activity_main.xml, these ids will not be visible when building vanilla, resulting build failure if you use these ids in code.You should note that resources aren’t merged on file-level, but actually on resource-level. For string resources for example, if you have these two files:src/main/res/values/strings.xmlsrc/main/res/values/strings.xml           Vanilla App           Hello world!src/strawberry/res/values/strings.xml           Strawberry Appwhen you build the strawberry flavor, you’ll get these resources (you won’t see this inside the IDE, your files will stay the same, but this just represents how the final resources would be.):           Strawberry App           Hello world!If you build vanilla, you’ll get main‘s strings.xml as is.Although this works, I’d recommend splitting customizable strings in a different file (e.g. flavor_strings.xml), to separate actual application strings, from strings that should be customized by each flavor. This way it’s more maintainable, and when adding a flavor, you would know exactly what to customize, without touching the application’s general strings and possibly their localized versions.Note: When providing alternate resources in a flavor, make sure you provide alternates for all qualifiers that exist in main for the same resource.For example, if you have logo.png in main provided in drawable-hdpi and drawable-xhdpi, and you only provide the hdpi version in strawberry flavor, the logo.png from main will be loaded on xhdpi devices.

6

Providing Alternate Code filesCode files are treated differently from resources. They don’t override each other, they’re just combined. So if you have com.example.MainActivity in your mainsource directory, and provide a different implementation of the same class in a different flavor, the build system will complain that you have duplicate class definitions when building that flavor.However, this is still easily achievable. If you want to provide different implementations for a class in flavors, you have to omit them from the main source.Let’s say you want to provide a different class called com.example.Flavor. First you would want to make sure you don’t have the class itself in main source. However, it’s likely that you want a default implementation. So, now we’re going to add it to all flavors separately, but not in main, this way, whenever you build a flavor, it will only see one com.example.Flavor class definition.app/|--libs/|--src/   |--vanilla/   |  |--java/   |     |--com/example/   |        |--Flavor.java   |--strawberry/   |  |--java/   |     |--com/example/   |        |--Flavor.java   |--main/      |--java/      |  |--...      |--res/      |  |--...      |--AndroidManifest.xmlNote: If you’re going to use both class versions from the main code files, make sure you maintain the same package, same class name, and same publicly used methods. If one version doesn’t have a method of another, and you attempt to use it, it will result a build error.This works best if you structure the customizable classes correctly with interfaces or abstract classes. You may be interested to look at Dagger or Roboguice if you want the code be more flexible.

7

Controlling Code Path per FlavorIf your flavors have different features enabled, you would want to control the code execution per flavor as well. Basically you want to sayif(IS_VANILLA) {doSomething();} else if(IS_STRAWBERRY) {doSomethingElse();}You have many options to achieve this.You can depend on BuildConfig.FLAVOR value, to check which flavor are we building. This method however can be hard to manage if you’re going to add more flavors in the future.Put your flags in an XML resource file. This will force you to use a Context to get these values in runtime though.Add a BuildConfig boolean flag, something like (BuildConfig.HAS_PAYMENT, BuildConfig.IS_PRO_VERSION), this could be done using buildConfigField:productFlavors {        vanilla {            buildConfigField 'boolean', 'HAS_PAYMENT', 'true'        }         strawberry {            buildConfigField 'boolean', 'HAS_PAYMENT', 'false'        }    }You can’t add it manually of course as BuildConfig is automatically generated. We’d recommend using this method if applicable.Note: So far, I’ve been using the default project structure. You can always customize the locations of specific folders (for java files, resource directories…etc), but using the default structure will keep your build script short, and following the convention will help you find stuff easier instead of tracing around where files are coming from.

8

Signing ConfigurationWe’re almost done for publishing the applications. We need to add the signing configuration to build signed APKs. I will assume you have already generated the keystore files. You will need to add the signingConfigs section, anywhere before the productFlavors section:signingConfigs {        release {                storeFile file('../../app.keystore') //Path to the keystore file                keyAlias 'app'                storePassword '12345678'                keyPassword '12345678'        }}You may prefer to sign each flavor with different certificate. To do that, you would just need to write a signingConfig section per flavor and assign it to each flavor:signingConfigs {     vanilla {             storeFile file('../../app.keystore') //Path to the keystore file             keyAlias 'vanilla'             storePassword '12345678'             keyPassword '12345678'     }      strawberry {             storeFile file('../../app.keystore') //Path to the keystore file             keyAlias 'strawberry'             storePassword '12345678'             keyPassword '12345678'     } } productFlavors {     vanilla {         packageName 'com.example.multiflavorapp'     }      strawberry {         packageName 'com.example.multiflavorapp.strawberry'     } }  buildTypes {     release { //Only use the release key on a release buildType         productFlavors.vanilla.signingConfig signingConfigs.vanilla         productFlavors.strawberry.signingConfig signingConfigs.strawberry     } }Note: For simplicity, I’ve put the keystore and alias password in the build file directly. Most probably, you want to move these out of the file so you can check it into version control, but that’s a bit beyond our scope here.That’s all for this post. There’s a lot more you can do with flavors, and more configuration, like flavor-specific dependencies that were not covered here. You should not overuse flavors though, unless these flavors should really belong to the same code base. If you find yourself doing major work inside a specific flavor, you should probably reconsider using flavors at all. I think it’s better to think of a flavor as a thin layer, not an app in itself

推荐信息