DEV Community

Cover image for Harnessing App Flavors and Shared Preferences in Android: A Ride-Sharing App Case Study
Audu Ephraim
Audu Ephraim

Posted on

Harnessing App Flavors and Shared Preferences in Android: A Ride-Sharing App Case Study

Introduction

In this article, we will explore the concept of app flavors in Android applications and how to customize these flavors. We will build a demo ride-sharing application with two flavors, one for the driver and one for passengers.

We will also be looking at saving a user's choice at the first launch with shared preferences so that, on the next launch, the user's choice will be remembered.

By the end of this article, you will have a solid foundational understanding of how to use app flavors in Android development and how to apply this knowledge to create more flexible applications.

Prerequisites

Basic knowledge of Android development, Gradle and Jetpack Compose is required to follow along with this article.

Understanding App Flavors

App flavors or build configurations are distinct ways to create distinct environments or “flavors” for your app, each representing a distinct variant of your application but from the same codebase.

App flavors are a way to implement different versions of an application with minor changes.

Common use cases of app flavors include:

  • Different versions of an application- you might have a free and premium version of your app. The free version could have ads and limited features, while the premium version has no ads and more features.

  • Geographical variations: if your app provides content or features based on geographical location, you can use app flavors.

  • Different UI for different user roles: in a ride-sharing app, like in our demo. You have one UI for drivers and one for passengers.

  • A/B Testing: you can use app flavors to create slightly different versions of your app for A/B testing. This can help you understand which features or designs users prefer.

Building different flavors for your app can be very useful, they also come with some level of complexity, so you need to think carefully about your needs before setting up a project to use app flavors.

If your separate apps require minor differences and theme changes but are still the same app, multiple flavors should be considered. However, if both apps require a lot of custom code differences you should rethink using app flavors.

Build Types

In the process of building Android apps, besides “flavors”, there’s another crucial concept known as “build types”.

A build type is responsible for defining settings related to the app’s build and packaging process, such as whether it’s debuggable and what signing keys to use. The standard build types are “debug” and “release”.

On the other hand, a flavor is used to specify features, devices and API prerequisites like custom code and layout, as well as minimum and target API levels, among other things.

The term “Build Variant” is used to describe the combination of build types and flavors.

For every combination of a defined flavor and build type, there exists a corresponding build variant. In the case of our ride-sharing application, we will have these corresponding build variants.

  • driverAppDebug

  • driverAppRelease

  • passengerAppDebug

  • passengerAppRelease

Therefore, it is important not to mix up the concept of app flavors, build types and build variants with each other as each has its unique role to play.

Let’s get started!!!

Creating the project

We do not need to do anything special to create our project. We’ll create a new regular Android project and call it myRideSharingApp and wait for the project to be done building.

Setting up Project Flavors

There are two ways to do this.
One way is to click on the build option in Android Studio and select “edit flavors”

screenshot of android studio showing edit flavor option

We can now create a flavor dimension, this is essential when creating multiple flavors as each flavor belongs to a dimension. We can also edit applicationID, add a signing config among other things.

Then click “Okay” A Gradle sync will occur and when it is done our flavors will be created.

Another method to create our flavors involves opening our module’s build.gradle file. Inside the Android block of this file, we can add the specifications for our flavors.

flavorDimensions += listOf("driver", "passenger")
productFlavors {
   create("driverApp") {
       dimension = "driver"
       applicationId = "com.example.myridesharingapp.driver"
       applicationIdSuffix = ".driver"
       versionCode = 1
       versionName = "1.0"
       versionNameSuffix = "-driver"
       targetSdk = 34
       minSdk = 24
   }

   create("passengerApp") {
       dimension = "passenger"
       applicationId = "com.example.myridesharingapp.passenger"
       applicationIdSuffix = ".passenger"
       versionCode = 1
       versionName = "1.0"
       versionNameSuffix = "-passenger"
       targetSdk = 34
       minSdk = 24
   }
}
Enter fullscreen mode Exit fullscreen mode

Let's try to understand what's going on:

  • flavourDimension+= listOf("driver", "passenger"): This line creates dimensions of your product flavors. Each dimension represents a different characteristic that a product flavor can modify. In this case we have two dimensions: driver and passenger.

  • productFlavors: In this block, you define your product flavors. Each product flavor is a variant of your app that you can build
    create("driverApp") { ... } and create("passengerApp") { ... } these blocks define the driverApp and passengerApp flavors respectively. Each block sets properties specific to that flavor

  • dimension: This property sets which flavor dimension this product flavor belongs to

  • applicationId: This property sets the unique ID for this version of the app. It’s used by the Android system to identify your app among all others. Two applications with the same applicationID cannot exist on the same device.

  • applicationIdSuffix: This property adds a suffix to the applicationId for this flavor. This allows you to install multiple flavors of the app on the same device.

  • versionCode: This property sets the version code for this flavor. The version code is an integer value that represents the version of the app.

  • versionName: This property sets the version name for this flavor. The version name is a string that represents the version of the app.

  • versionNameSuffix: This property adds a suffix to the versionName for this flavor.

  • targetSdk: This property sets the target SDK version for this flavor. The target SDK version is the latest Android version that your app has been tested with.

  • minSdk: This property sets the minimum SDK version for this flavor. The minimum SDK version is the lowest Android version that your app supports
    Once done, sync the project and we've created our flavors.
    Note: All these details and specifications can be entered using the ‘Edit Flavor’ dialog.

File Structure

Every flavor file structure should look exactly like the main directory file structure. In our case it should look like this:

screenshot of android studio showing file structure

Every flavor should have its res folder that should contain values specific to each flavors like drawables and the like.

Also, each flavor can have its manifest.xml file to specify settings and configurations particular to each flavor.

For example, you might have different services or permissions for the driverApp flavor compared to the passengerApp flavor.

These differences would be reflected in each flavor’s respective AndroidManifest.xml file.
It’s a powerful feature that adds a lot of flexibility to your app’s configuration.
Each flavor should contain its own MainActivity.kt file. In our case passengerActivity.kt and DriverActivity.kt serve as an entry point to each application (Flavor).

Setting Up Activities and Manifest Files

We create our driverActivity.kt in our driverApp directory which contains a simple composable screen that indicates that this is the driver screen.

class DriverActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DriverScreen()
        }
    }
}

@Composable
fun DriverScreen() {
    Box(contentAlignment = Alignment.Center) {
        Text(
            text = "This is the driver Screen",
            fontSize = 20.sp
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

In our passengerApp directory, we do the same: we create a passengerActivity and a composable to indicate that this is the passenger screen.

class PassengerActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PassengerScreen()
        }
    }
}

@Composable
fun PassengerScreen() {
    Box(contentAlignment = Alignment.Center) {
        Text(
            text = "This is the passenger Screen",
            fontSize = 20.sp
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

After we are done setting up our passengerActivity and driverActivity, we need to declare each activity in its manifest file so that the Android systems know about it. The passenger manifest file will look like this:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <activity android:name=".PassengerActivity"
            android:exported="true">
        </activity>
    </application>

</manifest>
Enter fullscreen mode Exit fullscreen mode

and the driver's manifest file will look like this:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <activity android:name=".DriverActivity"
            android:exported="true">
        </activity>
    </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

The system must know that this activity is part of your app so it can start the activity when requested to do so through an intent.

Navigating to Individual Flavors

In our mainActivity.kt file in our main directory we are going to create a simple composable with two buttons to navigate to our desired application (Flavor).

@Composable
fun SharedComponents(
) {
   val context = LocalContext.current
   Box(
       contentAlignment = Alignment.Center,
       modifier = Modifier.fillMaxSize()
   ){
       Column(
           modifier = Modifier.fillMaxSize(),
           horizontalAlignment = Alignment.CenterHorizontally,
           verticalArrangement = Arrangement.Center
       ) {
           Text(
               text = "This is the shared module",
               fontSize = 20.sp
           )
           Spacer(modifier = Modifier.height(30.dp))
           Button(onClick = {
               val intent = Intent(context, DriverActivity::class.java
               )
               context.startActivity(intent)}
           ) {
               Text(text = "driver")
           }
           Spacer(modifier = Modifier.height(30.dp))
           Button(onClick = {
               val intent = Intent(context, PassengerActivity::class.java)
               context.startActivity(intent)
           }) {
               Text(text = "passenger")
           }
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

Upon running the application, you should be able to navigate to any of the flavors by clicking a button. If you close the application and reopen it, you’ll notice that it will return to the screen of the shared module.

This current setup is not ideal. We would prefer our application to remember our selection. That way, the next time we open our application, we will be directed to the flavor of our initial choice.

To achieve this, we will be using shared preferences.

Setting up Shared Preferences

We are going to create an object called preference in our main module and add the following.

object Preference {

   private const val PREFERENCE_NAME = "app_preference"

   private const val USER_CHOICE = "user_choice"

   fun saveUserChoice(context: Context, choice: String){
       val sharedPref = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
       sharedPref.edit().putString(USER_CHOICE, choice).apply()
   }

   fun getChoice(context: Context): String?{
       val sharedPref = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
       return sharedPref.getString(USER_CHOICE, null)
   }
}
Enter fullscreen mode Exit fullscreen mode

We create two functions to save and get our user’s choice.
In our mainActivity class we retrieve a user’s choice from sharedPreference and launch the appropriate activity when the app starts.

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val userChoice = Preference.getChoice(this)
           MyRideSharingAppTheme {
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colorScheme.background
               ) {
                   if (userChoice == "driver"){
                       val intent = Intent(this, DriverActivity::class.java)
                       startActivity(intent)
                       finish()
                   }else if(userChoice == "passenger"){
                       val intent = Intent(this, PassengerActivity::class.java)
                       startActivity(intent)
                       finish()
                   }else{
                       setContent {
                           MyRideSharingAppTheme {
                               Surface(
                                   modifier = Modifier.fillMaxSize(),
                                   color = MaterialTheme.colorScheme.background
                               ) {
                                   SharedComponents()
                               }

                           }

                       }

                   }
               }
           }
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

We also modify our onClick lambda to be able to save a user's choice at the click of the button.

Button(onClick = {
   **Preference.saveUserChoice(context, "driver")**
   val intent = Intent(context, DriverActivity::class.java
   )
   context.startActivity(intent)}
) {
   Text(text = "driver")
}
Spacer(modifier = Modifier.height(30.dp))
Button(onClick = {
   **Preference.saveUserChoice(context, "passenger")**
   val intent = Intent(context, PassengerActivity::class.java)
   context.startActivity(intent)
}) {
   Text(text = "passenger")
}
Enter fullscreen mode Exit fullscreen mode

Run the application and make a selection. After closing and reopening the application, you’ll find that it remembers your initial choice, just as intended!

Conclusion

So far, we’ve discussed app flavors and their use cases. We’ve examined how to set up app flavors and customize each one to meet specific requirements.

We’ve also explored how to establish file structures and navigate to a particular app flavor.

Finally, we’ve looked at how to save a user’s preferred flavor with shared preferences for subsequent runs.

In conclusion, the concept of app flavors in Android development offers a powerful and flexible way to manage different versions of an application.

Through the example of a ride-sharing app, we’ve seen how to create and customize ‘driver’ and ‘passenger’ flavors, each tailored to meet specific user needs.

We’ve also explored how to use shared preferences to enhance the user experience by remembering their preferred flavor.

This not only streamlines the development process but also provides a more personalized user experience. Happy Coding!

Questions and suggestions would be highly appreciated.

You can find the link to the whole project Here

Top comments (0)