Best Practice for Android Native Development

12-Factor App

The twelve-factor app is a methodology that we must follow.

https://12factor.net

IDE

The recommended IDE is Android Studio because it is developed and frequently updated by Google, has good support for gradle, contains a range of useful monitoring and analysis tools and is generally tailored for Android development.

Min SDK Requirement

It’s useful to make an early decision on the minimum sdk requirement that you want to support.

Knowing which OS version and, devices that you need to develop is important to test against and to determine what’s possible and what’s not.

Coding Style

Use Android Lint to scan the code for making sure that common coding convention and best practices are followed while developing the app.

  • Name – Descriptive and consistent naming makes app easier to read and understand
  • Code organization – Use java packages to organize your code properly (refer Java Packages)
  • Unused Code – Unused (dead) code, including the editor template code and placeholder comments should be removed.
  • Comments – when they are needed, use comments to explain why a particular piece of code does something. Comments must be up-to-date or deleted.

Java Packages

Use the java packages to:

  • to clearer feature dependency and interface boundaries
  • promot encapsulation
  • reduce the risk of unknowingly modifying unrelated or shared code.- simpler navigation, most related class will be in one package
  • easier to remove the feature
  • simplifies the transaction to module based build structure (better build times and Instant Apps support)

Library

  • Jackson is a Java library for JSON serialization and deserialization, supporting various ways of processing JSON: streaming, in-memory tree model, and traditional JSON- POJO data binding.
  • Gson is a another popular choice and being smaller library than Jackson. Prefer it to avoid 65k methods limit.
  • Recommend basing your stack around OkHttp for efficient http requests and using Retrofit to provide a typesafe layer.
  • Consider using Picasso for loading and caching images. Glide is another option for loading and caching images. Claims of better performance than Picasso, but also bigger method counts.
  • RxJava is a library for Reactive Programming (handling asynchronous events). It is a powerful paradigm, but it also has a steep learning curve.
  • Dagger is a dependency injection framework like any other DI framework, it can be used to manage dependency to build components which can be easily enhanced. Dagger is not required for simple applications. But it is always a good idea to use Dagger as functionality is always expected to change. That making functionality changes requires minimal effort. You should be able to make changes where it needs without breaking the entire app.
  • Try to use android extension libraries (AndroidX) which represents the new era for the Support Library
  • Consider using Kotlin over Java. Google said that write better Android Apps faster with Kotlin. Kotlin is modern statically typed programming language that will boost your productivity and increase the developers happiness.
  • LeakCanary is a memory leak detection library for Android and Java. A memory leak is the programming error that cause your application to keep a reference to an object that is no longer needed. As a result, the memory allocated for that object cannot be reclaimed, eventually leading to an OutOfMemoryError crash.

General

  • Latest IDE and SDK must be used for development and make sure all developer/reviewer of the project is using same version.
  • If no other special reason, follow the default project structure as this simplify your build script.
  • Don’t use the other scripting languages to build or to automate, if require create the tasks in Gradle
  • In your app’s build.gradle, you will need to define the signingConfigs for the release build. But avoid to store App credential details in signingConfigs. Use thegradle.properties file which should not be added to the version control system. That file is automatically imported by Gradle, and can use it in build.gradle.
  • Prefer Maven dependency resolution to import android library files or jar files. If you explicitly include jar files in your project, then they will be a specific frozen version. Downloading jars and handling updates is cumbersome and it is a problem that Maven already solves properly. Use Maven to resolve the dependencies.
  • Avoid Maven dynamic dependency resolution. Avoid the use of dynamic dependency versions (such as 2.1.+) as this may result in different and unstable builds or subtle, untracked differences in behavior between builds. The use of static versions (such as 2.1.1) helps create a more stable, predictable and repeatable development environment.
  • Use different package name for non-release builds. Use ‘ApplicationSuffix’ for debug build type to be able to install both debug and release apk on the same device. This will be especially valuable after your app has been published.
  • Project must be kept at SVN from day1 with proper versioning.
  • Avoid adding editor-specific configuration files, such as Android Studio’s .iml files, to the version control system as those often contain the local machine specific configurations
  • Application should be crash free.
  • Font & text size, text alignment, justification should be unique throughout
  • always in mind to handle special character, handle encoding/decoding (html, json, xml, url)
  • showing loading animation during API calls
  • checking the battery usage and internet usage of the application
  • Reducing Methods count as much as possible. (be mind 65K Methods Limit)
  • Remove of unnecessary images, files, resources which are not being used
  • Use of proper alert messages whenever necessary
  • Handling of internet reachability

Activity and Fragment

  • Avoid using nested fragment extensively, because matryoshka bugs can occur. Use nested fragment only when it makes sense.
  • Avoid putting too much code in activity. Whenever possible, keep them as lightweight containers, it should be exit in the app primarily for the lifecycle and other important Android-interfacing APIs.
  • Consider using single-fragment activity instead of plain activity. Put UI code in the activity’s fragment. This make it reusable in case if you need to use it in tabbed layout or multi-fragment tablet screen.

Resources

  • one attribute per line, indented by 4 space.
  • android:id should be first attribute always
  • android:layout_xxx should be at the top
  • style attribute at the bottom
  • Tag closer /> on its own line
  • Don’t hard code android:text, if you want to see sample use designtime attribute
  • Split a large file into other files. You don’t need to have a single styles.xml file. (you can have multiple)
  • colors.xml is a color palette. There should be nothing else in your colors.xml than just a mapping from a color name to an RGBA value.
  • Name your strings with keys that resemble namespaces, and don’t be afraid of repeating a value for two or more keys.
  • Don’t write string in all uppercase. Just use normal text (e.g, capitalize first character) . Need to display in all caps then can use the textAllCaps attribute.
  • Avoid a deep hierarchy of views.
  • Beware of problems related to WebViews. When need to display a web-page, (e.g., news articles), avoid doing client-side procession to clean the HTML, rather ask for the pure HTML from the backend. WebView can also leak memory when they keep the reference to their activity instead of being bound to application context.

Data Storage

Shared Preference

If you only need to persist simple value and the application is running in a single process then SharedPreferences is enough.It is a good default option.

But there’re some situations where SharedPreferences are not suitable:

  • Performance – the data is complex or data volume is huge
  • Multiple process accessing the data – You have widgets or remote services that run in their own processes and require synchronized data.
  • You want to save the relational data
  • You can store the complex objects by serializing them to JSON and deserializing them when retrieve. But it may not be particularly performant, nor maintainable.

Content Providers

In case SharedPreferences are not enough, you should use platform standard ContentProviders, which are fast and process safe.

The single problem with ContentProviders is boilerplate code that is needed to set them up. You can use the library such as Schematic, which significantly reduces the effort. But still need to write some parsing code to read the data from Sqlite and vice versa.

Memory Management

Sharing Memory

Each app process is forked from an existing process call Zygote. The Zygote process starts when the system boots and loads common framework code and resources. To start the new app process, the system forks the Zygote process then load and run the app’s code in the new process. This approach allows most of the RAM pages allocated for framework code and resources to be shared across all app processes.

Most static data is mapped into a process. This technique allows data to be shared between processes, and also allows to be paged out when needed.

In may places, Android shares the same DRAM across processes using explicitly allocated shared memory regions. Specifically close resources, don’t rely on garbage collection. Properly free allocated memory upon the completion of functions and at all exit points.

Allocation

The Dalvik heap is constrained to a single virtual memory range for each app process. This defines the logical heap size, which can grow as it needs to but only up to a limit that the system defines for each app.

The logical size of the heap is not same as the amount of physical used by the heap. When inspecting your app’s heap, Android computes a value called PSS (Proportional Set Size).

Reclaiming

The Dalvik heap does not compact the logical size of the heap, meanings Android does not fragment the heap to close up space. Android can only shrink the logical heap size when there is unused space at the end of the heap. However, the system can still reduce physical memory used by the heap. After garbage collection, Dalvik walks the heap and find unused pages, then returns those pages to the kernel.

Security

  • Use secure http protocol “Https (TLS 1.1 or 1.2)
  • If no other concern, applications should not show the background snapshot and screen capture (disclosing confidential information) by using theLayoutParams.FLAG_SECURE
  • To prevent repeated and continuous submit, consider to implement the CAPTCHA(such as Google reCAPTCHA) or set the rate limit (e.g., allow one request per 2 minutes).
  • The SharedPreferences XML file should be cleared upon logout.
  • Set android:debuggable to false.
  • Should not allow the app to be able to install in rooted Android device
  • Should applied certificate pinner (SSL pinning) to make sure that application is talking the right back end.
  • Only use the permissions necessary for the app to work. Pay attention to permissions required by libraries.

Testing

As your app expends, app need to fetch data from a server, interact with device sensors, access local storage, or render complex user interface. The versatility of your app demands a comprehensive testing strategy.

Automated Testing

The Testing Pyramid illustrates how your app should include the three categories of tests: small, medium, and large.

  • Small tests [70%] are the unit tests that can run in isolation from production systems. Thy typically mock every major component and should run quickly on your machine.
  • Medium tests [20%] are integration
  • tests. They integrate several components, and they run on emulators or real devices.
  • Large tests [10%] are integration and UI tests that run by completing a UI workflow. They ensure that key end-user tasks work as expected on emulators or real devices.

Manual Test

  • Ensure app compiles to target API without errors or warnings
  • Ensure that app is able to handle for cases where the user rejects permission requests, and prompts the user for permissions.
  • Handles Doze with expected results and no errors.
  • Ensure that app is able to handle device state change (layout, language/region, font,connection)
  • handling of interruptions (like incoming call) during running application
  • UI/UX flow test, app redirection (move from one screen to another) is working properly, able to go back to correct screen, able to show the data properly (test with max length)

Error Handling and Logging

  • Do not expose sensitive information in the error responses, including system details, account information, or session id.
  • Use error handler that do not display debugging or stack trace information.
  • Implement generic error message and use custom error pages.
  • Properly free allocated memory when error occur
  • Do not store sensitive information in logs
  • Log all system exceptions.