How to Build and Structure a Modern Android App using MVVM and Kotlin (includes source code) Part 1

Introduction

This article shows how to build a clean Android app using MVVM, Kotlin, LiveData interfaced with RxJava and Retrofit, Data Repository, Room, Navigation, Data Binding, Notifications, Deep Links, SMS, Sharing, Preferences, and Permissions. It includes Kotlin Unit Tests built using MockK, Espresso, Mock Web Server and Robolectric.

Building a professional Android app can be challenging, even for experienced software engineers. For your app to provide a great user experience and run without crashing, the recommended approach is to use proven design patterns and tooling. This article discusses implementing the MVVM pattern using Google’s Architecture Components (JetPack) with Retrofit and RxJava libraries. Included is a fully working example, with unit tests, to help you get started. The sample is complete, maintainable, compact, efficient and easily adapted to your own ideas. It shows manual dependency inversion and MVVM best coding practices. Download the example app source code here: github.com/LeeHounshell/Dogs

MVVM is a layered architecture. Imagine your data as a ‘river’ flowing from the Network and Database into your Data Repository and from there into a ‘View Model’ (containing business logic), and finally onto the device screen’s Activity and Fragment display. From there interactions by a user cause data to flow back the way it came, from the Activity or Fragment to the ViewModel; from the ViewModel into the Data Repository and from there back to the Network and Database. The purpose of such a design is clean separation of code. By dividing code into sections that perform a single task well and communicate results, your app becomes both more stable and more maintainable. By using dependency inversion properly, your app becomes maintainable and testable.

Dependency Inversion simply means constructing the objects used by a class from outside of that class, and then passing (or injecting) those previously constructed objects into the class when using it. Decoupling this way is important because the technique allows for easy object reuse and extension. Your app becomes more maintainable. DI also allows for easier testing because object dependencies can be “mocked” so that specific functionality is targeted for testing. For example, if you have a unit test that uses a Network or Database, that test must be physically decoupled from the real Network and Database. Imagine a test that is not decoupled from the Network: One day it works fine, but the next day Internet is down and the test fails. That is unacceptable. Tests must be fashioned to validate functionality without being dependent on lower levels of abstraction. Using Dependency Inversion properly allows for that. However, it is my opinion that overuse of tools like Dagger2 and shiny-newer tools for automatic dependency injection can lead to violation of the Single Responsibility Principle. I believe it is better to structure your code well and use DI in a more targeted and limited manner. This demo shows how that can be accomplished, including creation of meaningful Unit Tests. Also seriously consider using the Service Locator Pattern. At some point you will likely want to extend your app’s functionality with new Services and Analytics. Using a Service Locator to obtain these resources and their dependencies is straight-forward and understandable for almost all software engineers. Ask yourself: Are the benefits of using dependency injection tools worth added complexity, risk and training expense? Does the Service Locator Pattern add equivalent functionality with reduced cost and risk?

Let’s Get Started

One of the first and most important tasks that you encounter when building an app is creating the build configuration. Getting your project and app ‘build.gradle‘ files correct from the beginning helps avoid problems. It is important to know what libraries are compatible with others. For example, you should never mix ‘androidx’ with the old ‘support’ libraries. Problems with incompatible build.gradle files are one main reason for app failure. Start with using a known stable build, like from this configuration, treating it as a template when building apps for Android API level 30:

The project build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    def gradle_version = '4.0.0'

    //FIXME: these are duplicated in app build.gradle. global variables are broken in buildscript

    def kotlin_version = '1.3.72'
    def nav_version = '2.2.2'

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

The app build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.harlie.dogs"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        // The following argument makes the Android Test Orchestrator run its
        // "pm clear" command after each test invocation. This command ensures
        // that the app's state is completely cleared between tests.
        testInstrumentationRunnerArguments clearPackageData: 'true'
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    buildFeatures{
        dataBinding = true
        // for view binding:
        // viewBinding = true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

    testOptions {
        execution 'ANDROIDX_TEST_ORCHESTRATOR'
        unitTests {
            includeAndroidResources = true
            unitTests.returnDefaultValues = true
        }
    }

    packagingOptions {
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/license.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/notice.txt'
        exclude 'META-INF/AL2.0'
        exclude 'META-INF/LGPL2.1'
        exclude("META-INF/*.kotlin_module")
    }
}

def kotlin_version = '1.3.72'
def multidex_version = '2.0.1'
def appCompat_version = '1.1.0'
def ktx_version = '1.3.0'
def constraint_version = '1.1.3'
def lifecycle_version = '2.2.0'
def room_version = '2.2.5'
def legacySupport_version = '1.0.0'
def coroutines_version = '1.3.7'
def nav_version = '2.2.2'
def material_version = '1.1.0'
def retrofit_version = '2.9.0'
def rxJava_version = '2.2.19'
def rxAndroid_version = '2.1.1'
def rxRetrofit_version = '2.9.0'
def glide_version = '4.11.0'
def palette_version = '1.0.0'
def preferences_version = '1.1.1'
def timberkt_version = '1.5.1'
def greenrobot_version = '3.2.0'
def antlr_version = '4.7.1'
def junit_version = '4.13'
def junitTest_version = '1.2.0'
def espresso_version = '3.2.0'
def mockk_version = '1.9.3'
def livedataTest_version = '1.1.2'
def okhttp_version = '4.7.2'
def robolectric_version = '4.2.1'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation "androidx.appcompat:appcompat:$appCompat_version"
    implementation "androidx.core:core-ktx:$ktx_version"
    implementation "androidx.constraintlayout:constraintlayout:$constraint_version"

    // Multidex
    implementation "androidx.multidex:multidex:$multidex_version"

    // Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

    // Room
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.legacy:legacy-support-v4:$legacySupport_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    // Navigation
    implementation "android.arch.navigation:navigation-common-ktx:$nav_version"
    implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
    implementation "android.arch.navigation:navigation-runtime-ktx:$nav_version"
    implementation "android.arch.navigation:navigation-ui-ktx:$nav_version"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"

    // RxJava
    implementation "io.reactivex.rxjava2:rxjava:$rxJava_version"
    implementation "io.reactivex.rxjava2:rxandroid:$rxAndroid_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$rxRetrofit_version"
    implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"

    // Glide
    implementation "com.github.bumptech.glide:glide:$glide_version"
    implementation "androidx.palette:palette:$palette_version"

    // Misc
    implementation "androidx.legacy:legacy-support-v4:$legacySupport_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "com.google.android.material:material:$material_version"
    implementation "androidx.preference:preference:$preferences_version"
    implementation "com.github.ajalt:timberkt:$timberkt_version"
    implementation "org.greenrobot:eventbus:$greenrobot_version"

    // Unit Tests - MockK
    testImplementation "junit:junit:$junit_version"
    testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testImplementation "io.mockk:mockk:$mockk_version"
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
    testImplementation "com.jraska.livedata:testing-ktx:$livedataTest_version"

    // Unit Tests - Robolectric
    testImplementation "org.robolectric:robolectric:$robolectric_version"
    testImplementation "androidx.test:core:$junitTest_version"

    // Instrumented Tests - espresso
    androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
    androidTestImplementation "androidx.test:rules:$junitTest_version"

    // Instrumented Tests - MockK
    androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    androidTestImplementation "io.mockk:mockk-android:$mockk_version"
    androidTestImplementation "androidx.test:runner:$junitTest_version"
    androidTestUtil "androidx.test:orchestrator:$junitTest_version"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
    androidTestImplementation "com.jraska.livedata:testing-ktx:$livedataTest_version"

    // Mock Test Web Server
    testImplementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"

}

configurations.all {
    resolutionStrategy {
        force("org.antlr:antlr4-runtime:$antlr_version")
        force("org.antlr:antlr4-tool:$antlr_version")
    }
}

App Overview

This demo app has one Activity and two Fragments. One Fragment displays a master list and the other shows detail information. Master/Detail is a common pattern that occurs in many apps; implementing it properly sometimes is challenging.

In this app, we initially load a local database with JSON default data. Then, if network is available, we update that data using a Retrofit REST API and RxJava. Network data overlays default database information with the latest. Note that Retrofit and RxJava are proven and powerful tools for accessing REST APIs; but because Google’s Jetpack libraries use ‘LiveData‘ for the data binding display, we need to convert RxJava ‘Flowables‘ into ‘LiveData’. Our Data Repository will handle that job using the converter MyLiveDataReactiveStreams. This app loads the network data from this dogs.json

The app screens look like this:

Initial Navigation and Display

Android uses the ‘AndroidManifest.xml‘ to determine the starting Activity, marked as the LAUNCHER Activity. For this demo, there is only a single activity: ‘MainActivity‘, to load. When that runs, a call to ‘setContentView‘ causes the main activity layout to inflate. Note that inside this layout is a fragment (or more properly a FragmentContainerView) element identified with android:name = “androidx.navigation.fragment.NavHostFragment”. That is what triggers Navigation to initialize our ListFragment. It works like this:

JetPack’s Navigation library uses ‘res/navigation/navigation.xml‘ to determine the starting transition, and all future screen transitions and transition arguments. Since ‘listFragment‘ is the id marked as the starting navigation fragment destination (see ‘app:startDestination = “@id/listFragment” ‘), the ‘listFragment’ navigation fragment loads, which in turn loads and runs the actual ListFragment Kotlin code. Finally, the ListFragment’s generated display content is displayed in the ‘NavHostFragment’ section of our ‘activity_main.xml‘. The next section shows how ListFragment loads the display content:

Create a ViewModel with DataRepository

In MVVM, the ViewModel’s purpose is to hold business logic and data for attached Activities and Fragments. It is decoupled from Activities and Fragments and has no dependencies on UI components. Any data stored in a ViewModel will survive ‘Configuration Changes‘, reducing (but not always eliminating) the need to override ‘onSaveInstanceState‘ and ‘onRestoreInstanceState‘. It is the ViewModel’s responsibility to communicate with a DataRepository when requesting or updating data to/from the Network or Database. That data is also presented to Activities and Fragments by having them observe changes to the ViewModel’s LiveData. The ViewModel should implement ‘LifeCycleOwner‘ to make it lifecycle aware so that it is not necessary to release observers when a UI component gets destroyed.

When the ListFragment loads, it needs to create (or attach to) a DogsListViewModel. Also the DogsListViewModel needs to use a DogsListDataRepository to manage the data flows. And the DogsListDataRepository requires both a DogsApiService and a SharedPreferencesHelper to do that data management. We need to use Dependency Inversion to properly setup those relationships. To accomplish that we construct DogsListDataRepository passing in the network URL, the DogsApiService and a SharedPreferencesHelper. Then we create the DogsListViewModel with the new DogsListDataRepository as a constructor parameter. In Android, to create any ViewModel with constructor parameters we need to use a ‘ViewModelFactory‘. The class MyViewModelFactory does just that. Note that the factory constructor takes a ‘DataRepository’ as a parameter and that this is our repository base class type. This lets us use different DataRepository classes mixed with different ViewModel implementations. Here is how to set it all up from inside the ListFragment’s ‘onViewCreated‘ method using Kotlin:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        Timber.tag(_tag).d("onViewCreated")
        super.onViewCreated(view, savedInstanceState)
        val repositoryURL: String = DogsApiService.BASE_URL
        val apiService = DogsApiService()
        val prefHelper = SharedPreferencesHelper()
        val viewModelFactory = MyViewModelFactory(DogsListDataRepository(repositoryURL, apiService, prefHelper))
        dogListViewModel = ViewModelProvider(this, viewModelFactory).get(DogsListViewModel::class.java)
        ...
    }

ViewModel Initialization

When the DogsListViewModel is created, it runs a Kotlin init block to check if the database is created, and to initialize that if it does not exist yet. The MutableLiveDatadogsMutableLoading‘ flag is observed by the ListFragment. It is initially set false so that a loading spinner displays. During first time init, the ViewModel’s ‘initializeData‘ method background loads the starting database from a local JSON asset file. Then the database is marked as ‘created’ using a SharedPrefrencesHelper. Now the ListFragment UI Thread is concurrently inside ‘onViewCreated‘ and it calls ‘dogListViewModel.checkRefresh‘ to load the dog list data, and start the ‘refresh‘ loop.

    init {
        Timber.tag(_tag).d("init")
        dogsRepository = repository
        viewModelScope.launch {
            dogsMutableLoading.postValue(false)
            initializeData(MyApplication.applicationContext())
        }
    }

The ‘refresh’ Loop

The idea here is to always have something valid to display. Because network may not be available, the initial database comes from local JSON. But if we do have access to a network, then we want to get the most up-to-date information from a remote server. If we load that, then we need to overlay our initial database with the new DogBreed values. Note that when the DogsListViewModel calls ‘dogsRepository.storeDogsLocally(dogsList)‘ the list is stored asynchronously and so won’t exist in the database immediately. That means we don’t initially have each DogBreed’s ‘Uuid‘ field set with an index value. The index is needed because when one of the list items is clicked we load that item from our database. Without an index, we don’t know if the clicked DogBreed info is actually written to the local database yet, meaning a database read of the clicked item may fail. Only after a bulk insert completes, do we learn the assigned ‘Uuid‘ of each dog. A fetch and display refresh loop runs from the ListFragment to ensure the database writes are flushed to disk. Also, when network data replaces existing database content, we need to loop to keep running until everything stabilizes and we have proper ‘Uuid‘ values for each replaced DogBreed list item.

So when the DogsListViewModel posts new list data, that is observed by the ListFragment, which in turn triggers a call to ‘showCurrentDogs‘. That completes the refresh loop by checking for valid ‘Uuid‘ indexes. If those are not set yet, then the loop runs again, until we have proper ‘Uuid‘ indexes for each list item. An additional check happens in ListFragment’s ‘observeViewModel‘ method when ‘dogListViewModel.checkIfLoadingIsComplete‘ checks to see if we have loaded from the network. If not and network is available, then ‘refresh‘ loops again, this time with Retrofit and RxJava loading the latest JSON.

    fun showCurrentDogs() {
        haveUuids = (currentDogs.isNotEmpty() && currentDogs[0].uuid != 0)
        Timber.tag(_tag).d("--------- showCurrentDogs: haveUuids=${haveUuids}")
        if (! haveUuids) {
            refresh()
        }
        else if (currentDogs.isNotEmpty()) {
            Timber.tag(_tag).d("showCurrentDogs: update the RecyclerView")
            // this forces the RecyclerView to redraw images (to fix an Android rotation bug)
            val myAdapter = dogsList.adapter
            dogsList.adapter = myAdapter
            // set adapter data as current dogs list
            dogListAdapter.updateDogList(currentDogs)
            dogsList.visibility = View.VISIBLE
            dogsList.layoutManager?.scrollToPosition(dogListViewModel.lastClickedDogListIndex)
        }
    }

    private fun refresh() {
        Timber.tag(_tag).d("refresh")
        Timer("refresh", false).schedule(500) {
            uiScope.launch(Dispatchers.IO) {
                if (haveUuids && currentDogs.isNotEmpty() && ! isNetworkAvailable()) {
                    Timber.tag(_tag).d("refresh: NO NETWORK, SHOW EXISTING DATA")
                }
                else {
                    Timber.tag(_tag).d("refresh: do the refresh")
                    dogListViewModel.refresh()
                }
            }
        }
    }

Room Database

Room‘ is a wrapper implementation around SQLite by Google. It allows easy implementation of object persistence by defining data models and abstract data access interfaces, and linking those using an abstract Database, like in this example. The real power here is that you don’t have to write low level data access code. Defining a ‘Data Access Object‘ interface and referencing that in the abstract database is enough to allow operations on the underlying SQLite database. Room works really well with LiveData and RxJava. Annotations in the DAO interface instruct Room how to interact with SQLite. Here is an example DAO for our demo app showing CRUD database operations for ‘DogBreed‘ models:

package com.harlie.dogs.room

import androidx.room.*
import com.harlie.dogs.model.DogBreed

@Dao
interface DogDao {
    @Insert
    suspend fun insertAll(vararg dogs: DogBreed): List<Long>

    @Query("SELECT * FROM dogbreed")
    suspend fun getAllDogs(): List<DogBreed>

    @Query("SELECT * FROM dogbreed WHERE breed_id = :dogId")
    suspend fun getDog(dogId: String): DogBreed

    @Query("DELETE FROM dogbreed")
    suspend fun deleteAllDogs()

    @Update
    suspend fun updateDog(dog: DogBreed)

    @Delete
    suspend fun deleteDog(dog: DogBreed)
}

And here is an example of using that Dao from our DogsListDataRepository to replace the DogBreed items with a new list:

        databaseScope.launch {
            val context: Context = MyApplication.applicationContext()
            val dao = DogDatabase.getInstance(context)?.dogDao()
            dao?.deleteAllDogs() // since we are replacing the cache, delete old data first
            val result = dao?.insertAll(*dogsList.toTypedArray())
            result.let {
                var i = 0
                while (i < dogsList.size) {
                    dogsList[i].uuid = it?.get(i)?.toInt() ?: 0
                    ++i
                }
                prefHelper.markDatabaseCreated()
            }
        }

Retrofit and RxJava

RxJava is an amazing library for reactive programming in Android. It allows you to do chained operations on data streamed from the Network or a Database in an intuitive manner that is extensible, maintainable and Thread safe. In RxJava, Observables and Flowables emit data items that are later iterated over by Observers. Flowables are different than Observables in that they can handle back-pressure for large data sets. In Flowables, back-pressure can be handled using a BackpressureStrategy like: MISSING, ERROR, BUFFER, DROP, or LATEST. In this example, RxJava and Retrofit work together with a Flowable to load and process JSON data from the Network. No BackPressureStrategy is defined, so it falls back to default which buffers up to 128 items in the queue. Because our ViewModels use LiveData for data-binding to the UI, we must convert the emitted data into LiveData objects. To make RxJava work together with LiveData we use LiveDataReactiveStreams. Here is an example of using RxJava together with Retrofit to load and convert REST objects from remote JSON data:

    fun fetchFromRemote(): LiveData<List<DogBreed>> {
        Timber.tag(_tag).d("fetchFromRemote")
        return MyLiveDataReactiveStreams.fromPublisher(
            apiService.getRequestApi()
                .getFlowableDogs()
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.computation())
        )
    }

Notice that we are using MyLiveDataReactiveStreams to do the conversion from Flowable to LiveData. This is a modified version of Google’s LiveDataReactiveStreams. It is modified because Google’s code does not handle error states very well. In this demo, the original code is changed to trap errors and send them via GreenRobot’s EventBus for handling. Then any errors that happen inside MyLiveDataReactiveStreams are easily handled with this code:

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onRxErrorEvent(rxError_event: RxErrorEvent) {
        Timber.tag(_tag).e("onRxErrorEvent: $rxError_event")
        Toast.makeText(this, rxError_event.description, Toast.LENGTH_LONG).show()
        recover()
    }

Data Binding with LiveData

Data Binding is a library for binding XML UI components to data sources in the app. The app’s data sources get updated via observed changes in a ViewModel’s LiveData. In this example we use data-binding for the DogBreed RecyclerView when showing the ListFragment, and also in the DetailFragment when displaying a single DogBreed list item. Additionally data-binding helps with a popup dialog when sending a SMS.

The ListFragment contains a RecyclerView and a display adapter. All of the RecyclerView XML binding happens in the adapter DogsListAdapter. Note the adapter’s ‘onCreateViewHolder‘ method attaches to item_dogs.xml using DataBindingUtil like this:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DogViewHolder {
        //Timber.tag(_tag).d("onCreateViewHolder")
        val inflater = LayoutInflater.from(parent.context)
        val view = DataBindingUtil.inflate<ItemDogsBinding>(inflater, R.layout.item_dogs, parent, false)
        return DogViewHolder(view)
    }

    // Also in the DogsListAdapter we define a ViewHolder with a data binding parameter
    class DogViewHolder(var view: ItemDogsBinding): RecyclerView.ViewHolder(view.root)

The class ‘ItemDogsBinding‘ is generated from the XML ‘data‘ section inside ‘item_dogs.xml‘. The binding class’s generated name comes from the XML file name. So ‘item_dogs.xml’ generates an ‘ItemDogsBinding’ class. In ‘item_dogs.xml’ the ‘data‘ section holds XML variable mappings. Here we map the XML variable ‘dog‘ to a DogBreed and the variable ‘listener’ to a ‘DogClickListener‘. For our RecyclerView items, those mappings look like this:

    <data>
        <variable
            name="dog"
            type="com.harlie.dogs.model.DogBreed">
        </variable>
        <variable
            name="listener"
            type="com.harlie.dogs.view.DogClickListener">
        </variable>
    </data>

Later in the ‘item_dogs.xml’ we use those variables. The list item click listeners are set using ‘android:onClick=”@{listener::onDogClicked}”‘ in the LinearLayout container. And text values are set for the breed name and lifespan inside their respective TextViews with code like this: ‘android:text=”@{dog.breedName}”‘ and ‘android:text=”@{dog.breedLifespan}”‘. A third invisible TextView holds ‘android:text=”@{dog.breedId}”‘. The invisible widget exists solely as a data holder so we can easily pass the dog’s breedId to a DetailFragment when a list item is clicked. Lastly, the ‘CenterBottomImageView‘ is set with XML code ‘android:imageUrl=”@{dog.breedImageUrl}”‘. But to make the image display we also need to add an image BindingAdapter to our UtilityFunctions. The BindingAdapter binds to the name ‘android:imageUrl‘ and looks like this:

@BindingAdapter("android:imageUrl")
fun loadImage(view: ImageView, url: String?) {
    //Timber.tag(utag).d("loadImage binding")
    GlideWrapper().loadImage(view, url, getProgressDrawable(view.context))
}

We have another (slightly different) BindingAdapter for the DetailFragment to load from our Glide image cache:

@BindingAdapter("bind:image_url")
fun loadCachedImage(view: ImageView, url: String?) {
    //Timber.tag(utag).d("loadCachedImage binding")
    GlideWrapper().loadCachedImage(view, url)
}

The DetailFragment uses the alternate image BindingAdapter for the CenterBottomImageView defined in ‘fragment_detail.xml‘. Note the ‘data’ section needed for defining the ‘dog‘ and color ‘palette‘ XML variables. The detail image is set from the image cache with this code: ‘app:image_url=”@{dog.breedImageUrl}”‘. Be aware that fragment_detail.xml binds to the alternate image BindingAdapter with the name ‘bind:image_url‘. This alternate BindingAdapter definition makes use of the Glide image cache.

From our DetailFragment, we attach to the fragment_detail.xml inside ‘onCreateView’ like this:

    private lateinit var dataBinding: FragmentDetailBinding

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        Timber.tag(_tag).d("onCreateView")
        setHasOptionsMenu(true)
        dataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_detail, container, false)
        return dataBinding.root
    }

Here the class ‘FragmentDetailBinding’ is generated from the ‘data‘ section of fragment_detail.xml and the XML’s file name, just like before. So the name ‘fragment_detail.xml’ generates a ‘FragmentDetailBinding’ class.

The ListFragment and DetailFragment both receive UI updates from their respective ViewModels. When changes happen, these are propagated to the UI XML display using the ‘binding adapters’ discussed earlier. These generated binding adapters are the mechanism used to change the UI’s display.

This is how the ListFragment observes and propagates LiveData changes from its attached DogsListViewModel into the ItemDogsBinding (in DogsListAdapter):

    private fun observeViewModel() {
        Timber.tag(_tag).d("observeViewModel")
        dogListViewModel.dogsList.observe(viewLifecycleOwner, Observer { dogs ->
            Timber.tag(_tag).d("observeViewModel: observe dogsLiveList size=${dogs?.size}")
            dogs?.let {
                currentDogs = dogs
                showCurrentDogs()
                if (dogs.isNotEmpty()) {
                    dogListViewModel.checkIfLoadingIsComplete()
                }
            }
        })
        dogListViewModel.dogsLoading.observe(viewLifecycleOwner, Observer { isLoading ->
            Timber.tag(_tag).d("observe dogsLoading=${isLoading}")
            isLoading?.let {
                dogsLoadingProgress.visibility = if (it) View.VISIBLE else View.INVISIBLE
                if (it) {
                    dogsList.visibility = View.INVISIBLE
                }
            }
        })
    }

And here is the code that observes and propagates detail data into the FragmentDetailBinding from the DetailFragment using LiveData from an attached DogDetailViewModel:

    private fun observeViewModel() {
        Timber.tag(_tag).d("observeViewModel")
        dogDetailViewModel.dog.observe(viewLifecycleOwner, Observer { dog ->
            if (dog != null) {
                currentDog = dog
                Timber.tag(_tag).d("observeViewModel: observe dog_icon dog_icon=${dog}")
                dataBinding.dog = dog
                dog.breedImageUrl?.let {
                    GlideWrapper().setBackgroundColor(this, dataBinding, it)
                }
            }
            else {
                Timber.tag(_tag).d("observeViewModel: need to refresh()")
                refresh()
            }
        })
    }

This is the end of part 1. The second half of this article will include:

 

  • Notifications and Deep Links
  • SMS, Sharing and Permissions
  • User Preferences
  • Building Unit Tests

 

Note 9 SPen Volume App

The SPen Volume control app works with SPen Remote and the Samsung Galaxy Note 9 to let you easily mute or unmute sound and change volume levels with the SPen. To use the app, you must enable SPen Volume in Settings by first setting the default SPen app to be SPen Volume. Additionally, you must enable the SPen Volume pen control buttons. Here’s how:

From Settings, select “Advanced features -> SPen -> SPen Remote -> HOLD DOWN PEN BUTTON TO” and set the feature ON then also set the SPen Volume app as the default SPen application under SUGGESTED APPS. Those settings should look like this screenshot:

You also must enable the SPen Volume pen buttons to use them for volume control. From Settings, select “Advanced features -> SPen -> SPen Remote”. Then under APP ACTIONS enable the SPEN VOLUME app to control volume up and down. That setting should look like this screenshot:

After installing and configuring the app, you can use it by long-pressing the SPen remote button. When you long-press, the SPen Volume app will open and toggle volume MUTE. The screen will briefly display “MUTE” or “UNMUTE”, depending on the new volume state. While “UNMUTE” is displayed, you can single-press the SPen button to increase sound volume or double-press it to decrease volume. Note that the app will only be active for a few seconds and will “beep” to let you know it is finished. Once the app beeps, it terminates itself and SPen Volume stops controlling sound, until the next long-press of the Spen button.

Be aware that when this app starts, any app currently in the foreground might minimize itself, for example ‘YouTube’ will minimize; however, other apps like ‘HULU’, ‘AMC’, ‘FXNOW’, ‘NBC’, ‘SYFY’, ‘TCM’, ‘TNT’, ‘The CW’ and ‘TuneIn Radio’ do not currently minimize, letting you easily ignore commercials. The SPen Volume app has no control over the behavior of other apps and can only adjust sound while it is active from a long-press of the SPen button.

This app collects no information and does not have Internet permission.

Using Gradle for processing Android flavor and buildType with #IFDEF in Java code

If you are a ‘C’ or ‘C++’ programmer you are likely familiar with the #IFDEF syntax used for pre-processing source code. Unfortunately, Java has no pre-processor for managing conditional code like this. When building Android apps, a developer will typically handle dependencies on ‘flavor’ and ‘buildType’ by placing modified versions of source under specially named directories. For example, handling code differences between a ‘paid’ vs. ‘free’ app may lead to 4 versions: paidRelease, freeRelease, paidDebug and freeDebug. Code duplication can become even more severe when more than 2 flavors or more than 2 build types are used. That means app maintenance complexity increases exponentially; the developer must remember to change code similarly in all versions of a duplicated class.

Gradle build rules can be employed to allow a Java source file to contain conditional source for processing all flavor and build type combinations. This post shows you how to do that. I assume you are already familiar with using Gradle and Android Studio for app development.

First create a new file: ‘preprocessor.gradle‘ under your project’s root directory (The directory containing the ‘app’ folder). That file should contain the following code (click to expand):

    // -------------------------------------------------------------------------------------------------
    // Android seems to want to duplicate code using 'buildTypes' and 'flavors' but when minor differences exist
    // it is inefficient to maintain mostly duplicate copies of code this way.  The code below allows java source code
    // to be edited in place. Source sections are commented or uncommented based on specially crafted comments:
    //
    //#IFDEF 'configuration'
    //    java code for the specified 'configuration'
    //#ELSE
    //    java code for NOT the specified 'configuration'
    //#ENDIF
    //
    // The 'configuration' specified above can be a BUILD_TYPE or FLAVOR or BUILD_TYPE+FLAVOR or FLAVOR+BUILD_TYPE
    // For example: 'debug' or 'release' or 'paid' or 'free'
    //              or 'debugpaid' or 'debugfree 'or 'releasepaid' or 'releasefree'
    //              or 'paiddebug' or 'freedebug' or 'paidrelease' or 'freerelease'..
    //              these are all valid 'configuration' entries and will be processed by #IFDEF depending on buildType and flavor.
    // Note that nested #IFDEF statements are not supported (and there is no actual need to nest).
    // Also the 'configuration' is case independent
    //
    // To use this preprocessor, add the following line to your app/build.gradle:
    //     apply from: '../preprocessor.gradle'
    //
    // Then in your java source with build dependencies, do something like this:
    //
    //#IFDEF 'paidRelease'
    //Log.v(TAG, "example of #IFDEF 'paidRelease'");
    //#ELSE
    //Log.v(TAG, "example of NOT #IFDEF 'paidRelease'");
    //#ENDIF
    //
    // Now during a gradle build, the appropriate lines of java code will be commented and uncommented as required.
    //
    // Author: Lee Hounshell - lee.hounshell@gmail.com - Jan 11, 2016
    // See: http://harlie.com/?p=38
    
     
    String sourceDirectory = 'src'
    FileTree javaFiles = fileTree(sourceDirectory) {
        include '**/*.java'
    }
     
    // auto comment and uncomment source lines between #IFDEF 'configuration' and #ELSE or #ENDIF
    // each matching java source file is edited in-place
    class PreProcessor {
     
        public enum IfdefState {
            NONE,
            IFDEF,
            ELSE
        }
     
        public static void preProcessSourceCode (FileTree javaFiles, String buildType, String flavor) {
            buildType = buildType.toLowerCase()
            flavor = flavor.toLowerCase()
            println("---> preProcessSourceCode BUILD_TYPE="+buildType+" FLAVOR="+flavor)
            String buildTypeAndFlavor = buildType + flavor
            String flavorAndBuildType = flavor + buildType
            String ifdefRegex = '^([ ]*)(\\/\\/)#IFDEF \'(.*)\'$'
            String elseRegex = '^([ ]*)(\\/\\/)#ELSE$'
            String endifRegex = '^([ ]*)(\\/\\/)#ENDIF$'
            String lineRegex = '^([ ]*)([^ ][^ ])(.*)$'
            String singleCharLineRegex = '^([ ]*)([^ ])$'
            String comment = "//"
            String newline = System.getProperty("line.separator")
     
            javaFiles.each { File javaFile ->
                println "checking for '$ifdefRegex' in $javaFile.name"
                String content = javaFile.getText()
                StringBuilder newContent = new StringBuilder()
                IfdefState match = IfdefState.NONE
                boolean changed = false;
                String buildTypeAndOrFlavor = "<undefined>"
                content.eachLine { line, index ->
                    // process #IFDEF
                    if (line.matches(ifdefRegex)) {
                        buildTypeAndOrFlavor = (line.split('\'')[1]).toLowerCase()
                        println("--> #IFDEF on line $index for $buildTypeAndOrFlavor")
                        if (buildTypeAndOrFlavor.equals(buildType)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR BUILD_TYPE $buildType")
                        }
                        else if (buildTypeAndOrFlavor.equals(flavor)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR FLAVOR $flavor")
                        }
                        else if (buildTypeAndOrFlavor.equals(buildTypeAndFlavor)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR COMBO BUILD_TYPE PLUS FLAVOR $buildTypeAndFlavor")
                        }
                        else if (buildTypeAndOrFlavor.equals(flavorAndBuildType)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR COMBO FLAVOR PLUS BUILD_TYPE $flavorAndBuildType")
                        }
                        else {
                            match = IfdefState.ELSE
                            println("--> $buildTypeAndOrFlavor IS NOT A MATCH FOR BUILD_TYPE $buildType OR FLAVOR $flavor OR COMBO $buildTypeAndFlavor OR COMBO $flavorAndBuildType")
                        }
                    }
                    // process #ELSE
                    else if (line.matches(elseRegex)) {
                        println("--> #ELSE on line $index for $buildTypeAndOrFlavor")
                        if (match != IfdefState.ELSE) {
                            match = IfdefState.ELSE
                            println("--> $buildTypeAndOrFlavor IS NOT A MATCH FOR #ELSE")
                        }
                        else {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR #ELSE")
                        }
                    }
                    // process #ENDIF
                    else if (line.matches(endifRegex)) {
                        println("--> #ENDIF on line $index for $buildTypeAndOrFlavor")
                        match = IfdefState.NONE
                    }
                    // comment or uncomment code or leave it unchanged
                    else {
                        if (match == IfdefState.IFDEF) { // ifdef: uncomment lines up to #ELSE or #ENDIF, as needed
                            if (line.matches(lineRegex)) {
                                def matcher = line =~ lineRegex
                                if (matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + matcher[0][3]
                                    changed = true
                                    println(line)
                                }
                            }
                        } else if (match == IfdefState.ELSE) { // else: comment-out lines to #ELSE or #ENDIF, as needed
                            if (line.matches(lineRegex)) {
                                def matcher = line =~ lineRegex
                                if (!matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + comment + matcher[0][2] + matcher[0][3]
                                    changed = true
                                    println(line)
                                }
                            }
                            else if (line.matches(singleCharLineRegex)) {
                                def matcher = line =~ singleCharLineRegex
                                if (!matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + comment + matcher[0][2]
                                    changed = true
                                    println(line)
                                }
                            }
                        }
                    }
                    newContent.append(line + newline)
                }
                // save the file if was edited
                if (changed) {
                    println("==> EDITING THE FILE <==")
                    javaFile.setText(newContent.toString())
                }
            }
        }
     
    }
     
    task preProcessSourceCodeDebugFree << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'free')")
        description("preprocess free code after //#IFDEF 'debug' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'free')
    }
     
    task preProcessSourceCodeDebugPaid << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'paid')")
        description("preprocess paid code after //#IFDEF 'debug' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'paid')
    }
     
    task preProcessSourceCodeReleaseFree << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'release', 'free')")
        description("preprocess free code after //#IFDEF 'release' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'release', 'free')
    }
     
    task preProcessSourceCodeReleasePaid << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'release', 'paid')")
        description("preprocess paid code after //#IFDEF 'release' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'release', 'paid')
    }
     
    tasks.whenTaskAdded { task ->
        if (task.name == 'compileFreeDebugJavaWithJavac') {
            logger.quiet('---> compileFreeDebugJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        if (task.name == 'compilePaidDebugJavaWithJavac') {
            logger.quiet('---> compilePaidDebugJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeDebugUnitTestJavaWithJavac') {
            logger.quiet('---> compileFreeDebugUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseUnitTestJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidDebugUnitTestJavaWithJavac') {
            logger.quiet('---> compilePaidDebugUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseUnitTestJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeDebugAndroidTestJavaWithJavac') {
            logger.quiet('---> compileFreeDebugAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseAndroidTestJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidDebugAndroidTestJavaWithJavac') {
            logger.quiet('---> compilePaidDebugAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseAndroidTestJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
    }

The code above works by allowing gradle to modify your source code, in place, when a build is run. Source code for your app will be dynamically edited to reflect the current settings for flavor and buildType. Now modify your ‘app/build.gradle‘ and add the line:

apply from: '../preprocessor.gradle'

so that your build includes the new preprocessor.gradle rules. Below is an example showing what the ‘app/build.gradle‘ file might look like if using free and paid builds. Important: Only the second line in the example shown below is needed to pull in the new build rules. This example shows a full build configuration only for completeness:

    apply plugin: 'com.android.application'
    apply from: '../preprocessor.gradle'
    
    android {
    
        ext.addDependency = {
            task, flavor, dependency ->
                println('task='+(String)task+'flavor='+(String)flavor+'dependency='+(String)dependency)
        }
    
        if (project.hasProperty("MyProject.properties")
                && new File(project.property("MyProject.properties") as String).exists()) {
    
            Properties props = new Properties()
            props.load(new FileInputStream(file(project.property("MyProject.properties"))))
    
            signingConfigs {
                release {
                    keyAlias props['keystore.alias']
                    keyPassword props['keystore.password']
                    storeFile file(props['keystore'])
                    storePassword props['keystore.password']
                }
                debug {
                    keyAlias props['keystore.alias']
                    keyPassword props['keystore.password']
                    storeFile file(props['keystore'])
                    storePassword props['keystore.password']
                }
            }
        }
    
        compileSdkVersion 'Google Inc.:Google APIs:23'
        buildToolsVersion "23.0.2"
    
        defaultConfig {
            applicationId "com.example.builditbigger"
            minSdkVersion 14
            targetSdkVersion 23
            versionCode 1
            versionName "1.0"
        }
    
        buildTypes {
            debug {
                debuggable true
            }
            release {
                minifyEnabled true
                shrinkResources true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                signingConfig signingConfigs.release
            }
        }
    
        productFlavors {
            paid {
                applicationId "com.example.builditbigger.paid"
                versionName "1.0-Paid"
            }
            free {
                applicationId "com.example.builditbigger.free"
                versionName "1.0-Free"
            }
        }
    
    }
    
    repositories {
        mavenCentral()
        jcenter()
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.1.1'
        compile 'com.android.support:design:23.1.1'
        // Added for AdMob
        freeCompile 'com.google.android.gms:play-services:8.4.0'
        freeCompile 'com.google.android.gms:play-services-ads:8.4.0'
    }
    

Now your app is ready to use #IFDEF #ELSE and #ENDIF when compiling Java code. Because Java does not recognize these new keywords, we need to prefix them with ‘//‘ so they are marked as comments. Here is an example ‘CheckPlayStore.java‘ class showing how that is done. In this example, the ImageView upgrade_paid will be null for release builds and non-null for free builds (assuming of course that your free layout.xml contains an ImageView with id ‘upgrade_to_paid‘).  This means the block of code after

if (upgrade_paid != null)

will only execute for free builds. The included Logging examples also show how to conditionally check for both flavor and buildType together. Note that the #IFDEF ‘configuration’ directive is case-independent:

    package com.example.builditbigger.util;
    
    import android.app.Activity;
    import android.content.ActivityNotFoundException;
    import android.content.Context;
    import android.content.Intent;
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.net.Uri;
    import android.util.Log;
    import android.view.View;
    import android.widget.ImageView;
    
    import com.example.builditbigger.R;
    
    public class CheckPlayStore {
        private final static String TAG = "EXAMPLE: <" + CheckPlayStore.class.getSimpleName() + ">";
    
        public static void upgradeToPaid(final Activity activity) {
            @SuppressWarnings("UnusedAssignment") ImageView upgrade_paid = null;
    
            //---------------------------------------------------------------------------------------------------------
            // IMPORTANT NOTE: the following #IFDEF #ELSE and #ENDIF directives are processed in build.gradle prior to javac
            //                 CODE IN THIS BLOCK DEPENDS ON 'BUILD_TYPE' AND/OR 'FLAVOR' AND IS DYNAMICALLY EDITED BY GRADLE
            //---------------------------------------------------------------------------------------------------------
            //#IFDEF 'free'
            //upgrade_paid = (ImageView) activity.findViewById(R.id.upgrade_to_paid);
            //#ENDIF
    
            // combo BUILD_TYPE+FLAVOR and FLAVOR+BUILD_TYPE examples..
    
            //#IFDEF 'freeDebug'
            //Log.v(TAG, "example of #IFDEF 'freeDebug'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'freeDebug'");
            //#ENDIF
    
            //#IFDEF 'releaseFree'
            //Log.v(TAG, "example of #IFDEF 'releaseFree'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'releaseFree'");
            //#ENDIF
    
            //#IFDEF 'DEBUGPAID'
            //Log.v(TAG, "example of #IFDEF 'DEBUGPAID'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'DEBUGPAID'");
            //#ENDIF
    
            //#IFDEF 'paidRelease'
            //Log.v(TAG, "example of #IFDEF 'paidRelease'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'paidRelease'");
            //#ENDIF
            //---------------------------------------------------------------------------------------------------------
    
            //noinspection ConstantConditions
            if (upgrade_paid != null) {
                Log.v(TAG, "using FREE version.");
                upgrade_paid.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        String packageId = activity.getApplicationContext().getPackageName();
                        packageId = packageId.replace(".free", ".paid");
                        Log.v(TAG, "packageId="+packageId);
                        try {
                            // from: http://stackoverflow.com/questions/3239478/how-to-link-to-android-market-app
                            String upgradeLink = "http://market.android.com/details?id=" + packageId;
                            if (CheckPlayStore.isGooglePlayInstalled(activity.getApplicationContext())) {
                                upgradeLink = "market://details?id=" + packageId;
                            }
                            Log.v(TAG, "upgradeLink=" + upgradeLink);
                            activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(upgradeLink)));
                        }
                        catch (ActivityNotFoundException e) {
                            Log.e(TAG, "APP id='"+packageId+"' NOT FOUND ON PLAYSTORE!");
                        }
                    }
                });
            }
            else {
                Log.v(TAG, "using PAID version.");
            }
        }
    
        // from: http://stackoverflow.com/questions/15401748/how-to-detect-if-google-play-is-installed-not-market
        private static boolean isGooglePlayInstalled(Context context) {
            PackageManager pm = context.getPackageManager();
            boolean app_installed;
            try
            {
                PackageInfo info = pm.getPackageInfo("com.android.vending", PackageManager.GET_ACTIVITIES);
                String label = (String) info.applicationInfo.loadLabel(pm);
                app_installed = (label != null && ! label.equals("Market"));
            }
            catch (PackageManager.NameNotFoundException e)
            {
                app_installed = false;
            }
            Log.v(TAG, "isGooglePlayInstalled=" + app_installed);
            return app_installed;
        }
    
    }

enjoy.

Your support for my work is greatly appreciated!