Awesome Open Source
Awesome Open Source

Title: Android Development | Best Practices

Best Practices

Introduction

Android development continues to dominate the world of mobile development. Fun projects, great pay, and tons of job prospects are just some of the reasons developers are starting their journeys into the exciting world of the Android operating system. Some experts say that there has never been a better time to learn Android skills, especially since the recent updates, like the addition of Kotlin and improvements to Google’s policies.

It’s been five years now that I’ve been into Android development and there has been no single day I haven't learned something new. But with these passing years, what I have realized is:
Just writing the code is not enough, Writing in an efficient way is the real challenge.

Tips and not Tricks

  1. Choose your App Architecture wisely based on your need, not just what the trend is.

    The architecture defines where the application performs its core functionality and how that functionality interacts with things like the database and the user interface.
    We have many architectures like MVC, MVP, MVVM, MVI, Clean Architecture.
    If any of these architectures is fulfilling your project requirements and you are following the standard coding guidelines and keeping your code clean, no architecture is bad.

  2. Consider using SVGs or WebPs for your image drawables.

    Supporting multiple resolutions are a sometimes nightmare to developers. Including multiple images for different resolutions also increases the project size.
    The solution is to use Vector Graphics such as SVG images or use WebP which can make a big difference in solving the image size problem by compressing lossless images.

  3. Choose your layout wisely and separate out the reusable XML and add it using include tag.

    We have different layouts like ConstraintLayout, LinearLayout, RelativeLayout, FrameLayout, CoordinatorLayout. I did performance analysis for some of them and found out that one should use layout based on their scenario/requirement only.
    Also, If you have some part of your XML getting reused in different layouts, extract it in a separate layout and use tag to avoid replication of code in different layouts.

  4. Learn how to use Build types, Product Flavors and Build Variants and make most out of it for faster and easier development.

Build Type

Decides how our code will be compiled. For instance, If we want to sign our .apk with debug key, we put our debug configuration into debug build type. If we want to have obfuscated code when it is compiled and ready to release, we put that configuration on our release build type. If we want to log our HTTP request in debug mode but we want to disable it on release mode, we put that configuration on build types or call build types in library dependencies.

buildTypes
{
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            applicationIdSuffix ".debug"
        }
}

Here is the simplest example:
When you run your app in debug mode, your application package name will be packagename.debug and if you run your app in release mode, your application package name will be packagename.

Product Flavor

Let’s say we have are developing your app for your customer users. Everything is going fine for customer app. Then your product owner said that you need to develop that app for admin users. Admin user app should have all functionalities that customer app has. But also admin user can have access to statistics page and admin user should see the app in different colours and resources. And also your admin app’s analytics should not be mixed with customer app.What will you do? The answer is Product Flavor. Same app, different behaviour.

Edit your Gradle file:

android
 {
    ...
    defaultConfig {...}
    buildTypes {...}
    productFlavors {
        admin {
            ..
        }
        customer {
            ..
        }
    }
}

Build Variants

Combines your build types and product flavors. Sync your project after you update your build.gradle. Then you will see all your build variants.

Build Variants

  1. Learn & Use Android Debug Bridge (ADB) to debug your application.

    Android Debug Bridge (ADB) is a versatile command-line tool that lets you communicate with a device. It allows you to do things on an Android device that may not be suitable for everyday use, yet can greatly benefit your user or developer experience. For example, you can install apps outside of the Play Store, debug apps, access hidden features, and bring up a Unix shell so you can issue commands directly on the device. ADB provides you with more details than your Android Studio Logcat. Just try it once and you can thank me later :-)

  2. Configure your gradle.properties to increase your build speed.

    Long build times have always been a problem in the developer’s life.

org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
android.enableBuildCache=true
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching= true
android.useAndroidX=true
android.enableJetifier=true
kapt.incremental.apt=true
kapt.use.worker.api=true

Showing you a sample of enhancements I did. You can read more about speeding up here.

  1. Keep a check on structural problems in your App code through Lint.

    The lint tool helps find poorly structured code that can impact the reliability and efficiency of your Android apps. The command for MAC:
./gradlew lint

For Windows:

gradlew lint
  1. Log everything in DEBUG mode only.

    We use logs to display useful information, errors, workflows or even to debug something.
    But, Every information that we log can be a potential source of security issues! So make sure you remove before the code goes live.
    And If you really want to keep these logs, you can either use Timber library which can log your messages and gives you the control over the flow of logs or you can create your own custom class to print logs in debug mode.

  2. Never add whole third party library if it’s possible for you to extract specific methods or small no. of classes for your functionality.

    Just add those classes to your project and modify them accordingly.

  3. Detect and Fix memory leaks in Android App time to time.

    “A small leak will sink a great ship.” — Benjamin Franklin
    Use memory tools like Leak canary to detect the cause for the memory leak.

Its knowledge of the internals of the Android Framework gives it a unique ability to narrow down the cause of each leak, helping developers dramatically reduce OutOfMemoryError crashes.

  1. Handle Configuration changes for your App.

    Sometimes handling the orientation changes for your Activity, Fragment or AsyncTasks becomes most frustrating things to deal. If orientation changes are not handled properly then it results in unexpected behaviour of the application. When such changes occur, Android restarts the running Activity means it destroys and again created.
    There are different options to handle the orientation changes:

    1. Lock screen orientation
    2. Prevent Activity to recreated
    3. Save basic state
    4. Save complex objects
  2. Perform validations in screens like input form on Client end only.

    Do we really need to hit backend for the validations like Is users email valid or Is user’s contact number is of the required length?
    Consider this kind of cases and write your logic accordingly.

  3. Don’t create references to activities that will prevent them from being garbage collected when they are done.

    Consider a very simple scenario — you need to register a local broadcast receiver in your activity. If you don’t unregister the broadcast receiver, then it still holds a reference to the activity, even if you close the activity.

How to solve this?
Always remember to call unregister receiver in onStop() of the activity.

Like this, Find more scenarios and fix them.

  1. Use App Chooser for your implicit intents and always handle NoActivityFound Exception.

    If multiple apps can respond to the intent and the user might want to use a different app each time, you should explicitly show a chooser dialog.
    Also, if for some reason no app is available, your app shouldn’t crash. Please handle it through try /catch with showing some toast message to the user.

  2. Put all your sensitive information in gradle.properties and never push it to your version control system.

    Don’t do this. This would appear in the version control system.

signingConfigs
 {
    release {
        // DON'T DO THIS!!
        storeFile file("myapp.keystore")
        storePassword "password123"
        keyAlias "thekey"
        keyPassword "password789"
    }
}

Instead, make a gradle.properties file :

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

That file is automatically imported by Gradle, so you can use it in build.gradle as such:

signingConfigs
{
    release
    {
        try
         {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}
  1. Implement SSL certificate pinning to prevent Man-in-the-middle Attack (MITM).

    To intercept any request, we mostly use a proxy tool. The proxy tool installs its own certificate on the device and application trust that certificate as a valid certificate and allow proxy tool to intercept application traffic. This way we can help hackers to tamper or know our data stuff.
    With SSL Pinning implementation, the application does not trust custom certificates and does not allow proxy tools to intercept the traffic.
    It’s a method that depends on server certificate verification on the client-side. Read more here.

  2. Use SafetyNet Attestation API to help determine whether your servers are interacting with your genuine app running on a genuine Android device.

    The API verifies the following:

  3. Whether the device is rooted or not.

  4. Whether the device is monitored.

  5. Whether the device has recognized hardware parameters.

  6. Whether the software is Android compatible.

  7. Whether the device is free form malicious apps.
    But before implementing it, please check the Dos and Don’ts.

  8. Use EncryptedSharedPreferences instead of SharedPreferences for storing sensitive information like your auth tokens etc.

  9. Use the Android Keystore system to store and retrieve sensitive information from your storage like databases etc.

The Android keystore provides a secure system level credential storage. With the keystore, an app can create a new Private/Public key pair, and use this to encrypt application secrets before saving it in the private storage folders. While developing AarogyaSetu, I learnt about using Keystore for encrypting and decrypting highly sensitive information. You can check the implementation here.

Note: We have an Android Backup mechanism which is turned on by default. Android preserves app data by uploading it to the user’s Google Drive — where it’s protected by the user’s Google Account credentials and can be easily downloaded for the same credentials on other devices. But, When you have encrypted something with Android Keystore, you won’t be able to restore it because the cypher key is only for that specific device. We don’t have a sync mechanism for Keystore like iOS and their keychain. A better way to backup your users’ data is to store them on your backend.

  1. Call Google Play services methods to ensure that your app is running on a device that has the latest updates to protect against known vulnerabilities found in the default security provider.

    For example, a vulnerability was discovered in OpenSSL (CVE-2014–0224) that can leave apps open to a “man-in-the-middle” attack that decrypts secure traffic without either side knowing. With Google Play services version 5.0, a fix is available, but apps must ensure that this fix is installed. By using the Google Play services methods, your app can ensure that it’s running on a device that’s secured against that attack.
    To protect against these vulnerabilities, Google Play services provides a way to automatically update a device’s security provider to protect against known exploits. Read more here.

  2. Implement reCAPTCHA to ensure your app is not automated i.e handled by a robot.

    reCAPTCHA is a free service that uses an advanced risk analysis engine to protect your app from spam and other abusive actions. If the service suspects that the user interacting with your app might be a bot instead of a human, it serves a CAPTCHA that a human must solve before your app can continue executing.

  3. Write Unit tests for your feature.

    Listing out some benefits:

  • Unit tests help to fix bugs early in the development cycle and save costs.
  • It helps the developers to understand the code base and enables them to make changes quickly
  • Good unit tests serve as project documentation.
  • You become more confident about the stuff you are writing.
    There are many more benefits.:-)
  1. Make security decisions on the server-side whenever possible.

    Don’t trust your application’s client-side. Hacker can easily tamper or hack your application’s codebase and can manipulate your code. So, it's better to have checks on the backend side whenever possible.

  2. Learn how to use Proguard to the maximum for code Obfuscation and Optimization.

    It is quite easy to reverse engineer Android applications, so if you want to prevent this from happening, you should use ProGuard for its main function: obfuscation, a process of creating source code in a form that is hard for a human to understand(changing the name of classes and members).
    ProGuard has also two other important functions: shrinking which eliminates unused code and is obviously highly useful and also optimization.
    Optimization operates with Java bytecode, though, and since Android runs on Dalvik bytecode which is converted from Java bytecode, some optimizations won’t work so well. So you should be careful there.

  3. Use Network security configuration to improve your app’s network security.

    Security is more about layers of protection than a single iron wall. The Android Network Security Configuration feature provides a simple layer to protect apps from unintentionally transmitting sensitive data in unencrypted cleartext.
    If you don’t know what “unencrypted communications” means, think of it this way — let’s say your office has the policy to send all shipments via UPS. A new intern joins the office and is tasked with shipping equipment to an office across the country.
    Oblivious to the policy and with all the best intentions, the intern sets up all shipments to be sent through an unknown, less expensive service.
    The Android Network Security Configuration feature is like the shipping/receiving manager who examines all inbound and outbound shipments and stops the shipment before the equipment gets into the hands of an unvetted delivery system. It can be used to prevent the accidental use of untrusted, unencrypted connections.
    Read more here.

  4. Use In-app review API to give users the ability to leave a review from within the app, without heading back to the App Details page. For many developers, ratings and reviews are an important touchpoint with users. Millions of reviews are left on Google Play every day, offering developers valuable insight on what users love and what they want to be improved. Users also rely on ratings and reviews to help them decide which apps and games are right for them.

Over the past two years, Google Play has launched various features to make it easier for users to leave reviews, as well as for developers to interact and respond to them. For example, users are now able to leave reviews from the Google Play homepage. We also launched the Reviews page under My Apps & Games, which gives users a centralized place to leave and manage reviews.The API lets developers choose when to prompt users to write reviews within the app experience. We believe the best time to prompt your users is when they have used the app enough to be able to provide thorough and useful feedback. However, be sure not to interrupt them in the middle of a task or when their attention is needed, as the review flow will take over the action on the screen.

  1. Introduce Modularization in app.

Split your app module into different small modules, and give those modules as dependency to required modules like implementation project(":network-module") , you will get benefit of faster builds while developing your app and reusable code. Later you can extend this to provide dynamic delivery module.

Build Variants


Read more here.

  1. Avoid using too many base classes. Using base class everywhere creates a tight web in your code, which later makes it hard to refactor things. If still it's a necessity, create and use a standalone function (kotline file). Lets understand this with an example : You have 2 fragments namely ProfileFragment and HomeFragment, which extends from BaseFragment. BaseFragment has a function fetchPosts() in the onCreate() method, Now if in future you decide that ProfileFragment should not fetch posts when it is created, rather it should first show a dialogBox if the user is not logged-in. If the codebase is huge, you may have hardtime refactoring it. The other way around is to create a kotlin file as fun fetchPosts() and then use this function in your fragments in the onCreate() method or in a swipeRefresh() method (depending on your use case). Also one must note that a class can extend only one abstract base class.

Read more here, here, here.

  1. If you are using FirebaseFireStore in your application then don't directly write 'true' on the read and write rules to decrease security issues as it can help hackers to penetrate through your application.To clear this out lets see the default rules written in fireStore -
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }
If we directly change false --> true it will no doubt work fine but its not secure because here acc to the rules everyperson has the permission to read and write the database so instead we use -->
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth!=null; } } }
This will allow only logged users to write over db. For more about writing rules check this out : https://firebase.google.com/docs/rules

Read more here and here.

  1. Create sourceSets for your main layout folder by adding following snippet in app level build.gradle file as follows
android {
    ...

    sourceSets {
        main {
           res.srcDirs = [
              'src/main/res',
              'src/main/res/layouts',
              file('src/main/res/layouts').listFiles()
           ]
        }
    }
}

Now you can separate out activity layouts, fragment layout and custom layouts in their respective folder as shown

Build Variants

This will make navigating for layout files a lot easier and keep resources segregated.

The Critics principle

When you’re reviewing code of your teammates don’t be a friend, Be their arch enemy, don’t let them make mistakes that you might have to clean someday. Cleaning other’s shit will only make your hand dirty. Enforce good practices in code reviews.

Please feel free to contribute by raising a PR to add more. I will be happy to learn and share :-)

You can check the article here which is originally published here on my personal website.


Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Android (40,977
Tips (291
Tips And Tricks (176
Related Projects