Follow treslines by email clicking Here!

Wednesday, May 11, 2016

How to define build strategies in your android app over BuildConfig

Hi there!

Today i'm gonna show you, how your could define your build strategies in android studio in an elegant, enhanceable way using the strategy design pattern.

Most of the times, Google defines its product making usage of best practises and good, enhanceable design pattern. I saw recently, in a big project we got, that they were not taking advantage of this and instead of that, they were using enumerations, resulting in a bunch of switch cases and later on another bunch of if-else cases and a lot of repeated code that breaks the DRY-principle and even more important the open-close-principle.

What does it mean to be programming respecting the open-close-principle? It means that every time a new component is created, that i don't have to change running code. i.e. I don't have to create another enum type, another switch-case to identify this type and later on another if-else case to know, depending on the comparation result, what kind of environment i want to set.

That's the code we got. This code has a lot of problems. It is fragile. Every new environment forces you to change a chain of steps in the code. Further it forces you to hold 4 variables in a static way to be able to share it over the application instead of hold only one object responsible for that. We define new enum types and define a new if statement running the risk of breaking existing logic. All of this can be avoided using the strategy pattern.

Let's take a look at the actual code, before enhancing it to visualize the problems:

    // somewhere in code this method was called passing the enum defined in BuildConfig
    setEnvironmentKeys(BuildConfig.ENVIRONMENT);
    // later in the class who defined this method over if-statements we identify the environment
    public static void setEnvironmentKeys(ENV env){
        try{
         if (MainApplication.isProduction()) {
                CONTENT_SERVER_URL = "https://content.mycompany.com/";
                PROFILE_SERVER_URL = "https://profile.mycompany.com/";
                IMAGE_SERVER_URL = "https://image.mycompany.com/log";
                Utils.GOOGLE_DEVELOPER_KEY = Utils.getStringRes(R.string.key_production);
            } else if (env == MainApplication.ENV.STAGING) {
                CONTENT_SERVER_URL = "http://content-staging.mycompany.com/";
                PROFILE_SERVER_URL = "http://profile-staging.mycompany.com/";
                IMAGE_SERVER_URL = "http://image.staging.maycompany.com/";
                Utils.GOOGLE_DEVELOPER_KEY = Utils.getStringRes(R.string.key_staging);
            }  else if (env == MainApplication.ENV.STAGING_TWO) {
                Utils.GOOGLE_DEVELOPER_KEY = Utils.getStringRes(R.string.key_staging);
                CONTENT_SERVER_URL = "http://content-staging2.mycompany.com/";
                PROFILE_SERVER_URL = "http://profile-staging2.mycompany.com/";
                IMAGE_SERVER_URL = "http://image.staging2.mycompany.com/";
            }else if (MainApplication.isDev()) {
                Utils.GOOGLE_DEVELOPER_KEY = Utils.getStringRes(R.string.key_development);
                CONTENT_SERVER_URL = "https://content.dev.mycompany.com/";
                PROFILE_SERVER_URL = "https://profile.dev.mycompany.com/";
                IMAGE_SERVER_URL = "https://image.dev.mycompany.com/";
            }
        }catch(Exception e){
            Log.wtf(TAG, "Couldn't set ENV variables. Setting to Production as default.");
        }
    }
    
    // and somewhere was defined the enum with the build types
    public enum ENV {
        DEVELOPMENT("DEVELOPMENT"),
        STAGING("STAGING"),
        STAGING_TWO("STAGING_TWO"),
        OPS_PREVIEW("OPS_PREVIEW"),
        PRODUCTION("PRODUCTION"), ;

        private final String name;

        private ENV(String s) {
            name = s;
        }

        public boolean equalsName(String otherName){
            return (otherName == null)? false:name.equals(otherName);
        }

        public String toString(){
            return name;
        }

    }

The code above is functional but when it comes to enhancement, maintainability and so on, then we can see some problems on that. We will discuss this later on. Let's now see how we could do it better by using the strategy pattern. First have a look at the uml diagram of it.



And now the little code of if:

   
  // this method is called somewhere in code
  setEnvironment(BuildConfig.ENVIRONMENT);
  
  // then we set the environment and use it without any change in code
  private Environment environment;
  public void setEnvironment(final Environment env){
   environment = env;
  }
  /**
   * In your android studio look for the tab: "Build Variants" (bottom left side)
   * and select "Build Variant > debug" while developing the application.
   * This will automatically instantiate this class and assign this value to BuildConfig.ENVIRONMENT
   * @author Ricardo Ferreira
   */
  public class DeveloperEnvironment extends Environment{
   private final static String CONTENT_SERVER_URL = "https://content.dev.mycompany.com/";
   private final static String PROFILE_SERVER_URL = "https://profile.dev.mycompany.com/";
   private final static String S3_AMAZON_SERVER_URL = "https://s3.amazon.dev.mycompany.com";
   private final static String GOOGLE_DEVELOPER_KEY = "123456";
   public DeveloperEnvironment() {
    super(CONTENT_SERVER_URL, PROFILE_SERVER_URL, S3_AMAZON_SERVER_URL, GOOGLE_DEVELOPER_KEY);
   }
  }
  /**
   * Build environment strategy - Every environment phase should extend from this strategy.
   * This way, you can access your app's environment configuration over the system class 
   * BuildConfig.ENVIRONMENT everywhere without the necessity of enumerations, switch cases 
   * or a bunch of if-else statements. If you create new environments, no change in your code
   * will be needed. 
   * @author Ricardo Ferreira
   */
  public abstract class Environment{
   private final String CONTENT_SERVER_URL;
   private final String PROFILE_SERVER_URL;
   private final String S3_AMAZON_SERVER_URL;
   private final String GOOGLE_DEVELOPER_KEY;
   // ... other environment variables ...
 public Environment(
   final String contentServerUrl, 
   final String profileServerUrl,
   final String s3amazonServerUrl, 
   final String googleDeveloperKey) {
  super();
  this.CONTENT_SERVER_URL = contentServerUrl;
  this.PROFILE_SERVER_URL = profileServerUrl;
  this.S3_AMAZON_SERVER_URL = s3amazonServerUrl;
  this.GOOGLE_DEVELOPER_KEY = googleDeveloperKey;
 }
 public String getContentServerUrl() {
  return CONTENT_SERVER_URL;
 }
 public String getProfileServerUrl() {
  return PROFILE_SERVER_URL;
 }
 public String getS3AmazonServerUrl() {
  return S3_AMAZON_SERVER_URL;
 }
 public String getGoogleDeveloperKey() {
  return GOOGLE_DEVELOPER_KEY;
 }
  }

Coming back to our initial discussion. Imagine now you need to add a new enviromnet. lets say staging. with the approach above you just have to say to someone: create a class who extends from environment, analogical DeveloperEnvironment, define it in your build.gradle, like you would do for the other approach also and done! It would work just fine without touching already written, functional code.The other way around you would have to change the enum, change the if-else statements.

Here is how you could define the strategies in your build.gradle. This will automatically assign the right value to the variable ENVIRONMENT depending on which environment you choose over Android Studio by selecting the tab Build Variants on the bottom left side of your IDE:

buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            debuggable true
            setApplicationIdSuffix("dev")
            versionNameSuffix " Dev"
            buildConfigField "com.treslines.learn.environment.Environment", "ENVIRONMENT", "new com.treslines.learn.environment.DeveloperEnvironment()"
            buildConfigField "boolean", "DEBUGLOG", "true"
            manifestPlaceholders = [
                    appName         : "MyApp Debug"
            ]
        }
        Staging {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.debug
            debuggable true
            setApplicationIdSuffix("staging")
            versionNameSuffix " Staging"
            buildConfigField "com.treslines.learn.environment.Environment", "ENVIRONMENT", "new com.treslines.learn.environment.StagingEnrivornment()"
            buildConfigField "boolean", "DEBUGLOG", "true"
            manifestPlaceholders = [
                    appName         : "MyApp Stg"
            ]
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
            zipAlignEnabled true

            buildConfigField "com.treslines.learn.environment.Environment", "ENVIRONMENT", "new com.treslines.learn.environment.ProductionEnrivornment()"
            buildConfigField "boolean", "DEBUGLOG", "false"
        }
    } 

That's all! hope you like it! :)