Bloco

View Original

Android Starter Template Revised

A few years ago we developed an Android app template project. It had the latest APIs, the best practices, our base architecture and our most used libraries pre-configured, but time moved on and so did most of these concepts. Four years after the last commit we decided it was time to migrate the template to Kotlin and check what concepts needed reevaluation as well.

So let's start right off the bat and tackle the question you probably already wondered about: "Why should I start building an Android app using a template?"

Well if you think about it Android already has templates for you, maybe you moved through it so fast you never did really considered them as the same kind of "templates". Well, you are not entirely wrong 🙂

Android Studio Project Template

While the idea of the Android Studio templates is to simply provide the starting screens with a new app in order to be quicker, the idea behind our template is a bit more complex.


Our Android Template is meant to give more than a starting screen. It gives you initial *project structure* so you can keep your code organized to expand efficiently, and with pre-configured *features* that we consider essential for modern Android Development. It's the template we use when starting every new project.

Project Structure

There are many approaches to build complex systems with a good architecture, we try to follow the principals behind a Clean Architecture. Being a "theoretical" solution, it needs to be adapted to Android.

Clean Architecture Evolution

Layers

Our adaptation results in a package structure like this:

Android Studio Folder Structure

The UI Layer

This layer contains the User Interface-related code. We are using the MVVM pattern in this layer. Note that it doesn’t matter which pattern you use for this layer and you are free to use what suits your needs best, be it MVP, MVI or something else.

The Domain Layer

The Domain layer contains all the models and business rules of your app. It also serves as a bridge between the UI Layer and the Data Layer.

In small projects sharing a domain model between all the layers is normal. But in bigger and more complex applications it's a good practice to separate models between layers.

The Data Layer

This layer provides abstract definitions for accessing data sources like a database or the internet. Note that it doesn’t matter which pattern you use for this layer and you are free to use what suits your needs best, be it Repository pattern, Store or something else.

Advantages of this separation

Let's take our template. It comes with a simple counter that makes use of the system preferences to keep the data persistent between launches.

class GetAndSetCounter
  @Inject constructor(
    private val preferences: CounterRepository
  ) {

  fun get() = preferences.getValue()

  suspend fun editCounter(counter: Counter) : Result<Counter> {
    if (counter.value < 0) {
      Timber.w(IncrementError(), "Value given was ${counter.value}")
      return Result.failure(IncrementError())
    }
    preferences.setValue(counter)
    return Result.success(counter)
  }

  class IncrementError :
    Exception("Value given to .editCounter() is negative. Negative numbers are not allowed")
  }

Template app Screenshot

GetAndSetCounter fits in the Domain Layer, it is the class responsible for handling the business logic of the counter. (In our example this logic is a simple validation of positive numbers)

The project comes with both instrumentation and unit tests and if we check the following unit test you can get a better sense of why this architecture can be so powerful. Let's take a look:

class GetAndSetCounterTest {
  val mockRepo = mock<CounterRepository>()
  val getAndSetCounter = GetAndSetCounter(mockRepo)

  @Test fun editCounter() = runBlocking {
    val expected = Counter(5)
    val result = getAndSetCounter.editCounter(expected)
    verify(mockRepo).setValue(expected)
    assert(result.isSuccess)
    assertEquals(expected, result.getOrNull())
  }
        ...
 }

We simply want to check that the edition of the number is allowed with a positive number, we don't care if it's stored on a server or locally, that validation is not related with the intended business logic, so all storage related code can be "faked"/"mocked", we do so with the aid of mockito.

Here we can clearly see the distinction between Domain Layer (validation of the number) and Data Layer (local storage). It is this distinction of responsibilities that allows for an easy way to test and be ready for future changes.

Imagine that in the future we no longer wish to store the data in the preferences but instead in a server, or after that we want to store it in a different server. With this approach it's all the same, we simply need to change the logic in the CounterRepository (Data Layer) and we can keep the business logic test intact, and since most of the critical parts of an application happens on the domain layer, this is the layer you want to keep most protected without having to worry about changes on what kind of storage you are using.

Features

This project takes advantage of many popular libraries, plugins and tools of the Android ecosystem such as:

  • Jetpack - major improvement to the original Android Support Library, which is no longer maintained
  • Android KTX - provides concise, idiomatic Kotlin to Jetpack and Android platform APIs.
  • Material Design - the default design library, where you can find Material Components
  • Gesture Navigation - Beginning with Android 10, the Android system supports fully gesture-based navigation
  • ViewModel - designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.
  • Coroutines - managing asynchronous operations with simpler code and reducing needs for callbacks
  • Flow - reactive programming based on coroutines
    • Flow Preferences - Flow adapter for SharedPreferences
  • Hilt - new way to incorporate Dagger dependency injection into an Android application.
  • Timber - a logger with a small, extensible API which provides utility on top of Android's normal Log class
  • Espresso - to write concise, beautiful, and reliable Android UI tests
  • Mockito - most popular Mocking framework for unit tests written in Java.

Most of the libraries are in the stable version but as of this writing the Coroutines/Flow are still experimental, meaning their API can suffer changes. They are however ready for production. Our version of AndroidX Core & Hilt dependencies are in alpha.

Also worth noting that we normally use tools like Jacoco or code coverage. You can find its integration with CodeCov in the template. It is commented out in the Github workflow, meaning that by default its CI integration is turned off.

Speaking of Github workflow and CI integration the template also is pre-configured to run unit lint and unit tests. Like Jacoco reports on CodeCov to run instrumentation testing you also need a service. You can find the template with a commented configuration to run them on Firebase Test Labs.

The majority of libraries and tools used should be familiar to most android developers, nevertheless, there are a few worth exploring in more detail.

Hilt

For those not familiar with dependency injection it's a simple concept that you can check at Edureka

If you are familiar with dependency injection (DI) you have probably already heard of Dagger(2). It is the go-to in dependency injection for Android, however it was not designed to be specifically for Android and therefore you can end up with a boilerplate code.

One thing to keep in mind with DI are the scopes. The most familiar scope you probably know is "Singleton", a Singleton has the scope of the entire app. In Android Development is perfectly normal to want the scope of an object associated with an Activity. While Dagger allows for custom scopes they need to be setup manually and can differ between projects. Hilt comes to solve all of this in a standard way that incorporates Dagger Dependency Injection into an Android application.

In Dagger-Android, we have to create a component class with a builder/factory, includes every module and we should inject the application context in the Application class after building our project. Lets take the following snippet with Dagger(2)

@Singleton
@Component(
  modules = [
    AppModule::class,
  ]
)
interface AppComponent {
  fun activityComponent(): ActivityComponent
  fun inject(app: App)
}


open class App : Application() {
  open val component: AppComponent by lazy {
    DaggerAppComponent.builder()
      .appModule(AppModule(this))
      .build()
  }

  override fun onCreate() {
    super.onCreate()
    component.inject(this)
  }
}

Now let's take a look at the same thing but with Hilt. With Hilt we don’t need to create a component, include every module, and build for generating DaggerAppComponent class all we have to do is to annotate the @HiltAndroidApp annotation.

@HiltAndroidApp
open class App : Application()

That’s all! Just by annotating the @HiltAndroidApp annotation, Hilt will do everything itself!

If you want a more in depth look I would advise you to check the official Google Developers video and documentation.

Coroutines & Flow

We previously worked with RxJava, we did so to achieve reactive programming and:

  • avoid “callback hell”
  • simple async / threaded work
  • simple composition of streams of data
  • clean and readable code base
  • easy to implement back-pressure

RxJava was the undisputed leader for reactive solutions and although reactive programming is not directly related to threading, concurrency and parallelism are very important to avoid unwanted issues. This is were Coroutines/Kotlin Flow takes the win, it is just simpler and more efficient!

This does not mean it's all wins, Kotlin Flow is still experimental. Although we are using it, we still recommend you to take a deep analysis of the pros and cons of each of them, and decide for yourself what is the best approach for you and your project.

Vasya Drobushkov has a great post about RxJava and Kotlin Flow we recommend a reading about it here and for an even more in depth comparison you can also check this.

Kotlin Coroutines and Flow are still recent, but they are already wining traction for their simplicity and improvements over RxJava. As a result, new libraries and tools are appearing. One such library is Flow Preferences that we use to read SharedPreferences reactively.

Gesture Navigation & AndroidX

Beginning with Android 10 (API level 29), the Android system supports fully gesture-based navigation. There are two things that app developers should do to ensure their apps are compatible with this feature:

  • Handle conflicting app gestures
  • Extend app content from edge to edge

We don't have conflicting app gestures in the template, but you should take them into consideration in your development.

Extending the app content from edge to edge it's a lot easier than it looks, thanks to AndroidX (androidx.core:core-ktx:1.5.0-alpha02) all you really need to do is:

WindowCompat.setDecorFitsSystemWindows(window, true)

Yes, we do realize it's still in alpha, but this feature is also something "new" so it is to be expected at the moment.

Besides these changes there are some design changes to take into consideration, we do recommend you to take a deeper dive with the official documentation and an amazing set of articles from Chris Banes that you can find here. For the Android Template these changes can be found in the theme.xml.

Template

You can easily see that we have two `theme.xml`files. Why? Because it allows to specify rules for specific API's levels, in this case API 29.


Let's consider the following code

<style name="Base.AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
  <item name="android:navigationBarColor">@android:color/transparent</item>
  <item name="android:windowTranslucentNavigation">true</item>
  <item name="android:statusBarColor">@color/bloco_green</item>
</style>

the item windowTranslucentNavigation produces a contrast in the navigation bar so it's "always" visible.

But this only makes sense in previous API levels, so for the most recent API levels there's no need to include the item windowTranslucentNavigation. We do that by having the second theme.xml files one that is specific to API29+.

Material Design

In order to keep with the best practices we follow the Material Design Android Guidelines. You can find them at https://material.io/blog/android-material-theme-color.

Material Design Color Pallet

Conclusion

In order to keep this Template alive and in good health it requires updates, and while we will be updating with our new learning experiences and best practices, it is open source so you can help contribute in creating a great starting point for everyone.

If you are new to the open source community don't worry, and don't be afraid to contribute even if with a question. Ask for a feature that you believe it may help people in the future and we can discuss it.

We recommend you to take a look at our Open Source App Development blog post and a look at our github repository.

Update: Since the revision to Kotlin we also added a few changes that we found useful while using the Template in production apps. You can check the update here.


📲 We are a studio specialised in designing and developing Android products. Keep up with us through Twitter, Facebook, and Instagram.