Android Articles / Blogs / Perficient https://blogs.perficient.com/tag/android/ Expert Digital Insights Mon, 02 Jun 2025 14:07:24 +0000 en-US hourly 1 https://blogs.perficient.com/files/favicon-194x194-1-150x150.png Android Articles / Blogs / Perficient https://blogs.perficient.com/tag/android/ 32 32 30508587 Over The Air Updates for React Native Apps https://blogs.perficient.com/2025/06/02/over-the-air-ota-deployment-process-for-mobile-app/ https://blogs.perficient.com/2025/06/02/over-the-air-ota-deployment-process-for-mobile-app/#respond Mon, 02 Jun 2025 14:07:24 +0000 https://blogs.perficient.com/?p=349211

Mobile App development is rapidly growing and so is the expectation of robust support. “Mobile first” is the set paradigm for many application development teams. Unlike web deployment, an app release has to go through the review process via App Store Connect and Google Play. Minor or major releases follow the app review same process, which can take 1-4 days. Hot fixes or critical security patches are also bound by the review cycle restrictions.  This may lead to service disruptions, negative app and customer reviews.

Let’s say that the latest version of an app is version 1.2. However, a critical bug was identified in version 1.1. The app developers may release version 1.3, but the challenge would be that it may take a while to release the new version (unless a forced update mechanism is implemented for the app). Another potential challenge would be the fact that there is no guarantee that the user would have auto updates on.

Luckily, “Over The Air” updates comes to the rescue in such situations.

The Over The Air (OTA), deployment process for mobile apps allows developers to push updates without going through the traditional review process. The OTA update process enables faster delivery for any hot fix or patch.

While this is very exciting, it does come with a few limitations:

  • This feature is not intended for major updates or large feature launches.
  • OTA primarily works with JavaScript bundlers so native feature changes cannot be deployed via OTA deployment.

Mobile OTA Deployment

React Native consists of JavaScript and Native code. When the app gets compiled, it creates the JSbundles for Android and iOS apps along with the native builds. OTA also relies on the JavaScript bundles and hence React Native apps are great candidates to take advantage of the OTA update technology.

One of our client’s app has an OTA deployment process implemented using App Center. However, Microsoft has decided to retire App Center as of March 31, 2025. Hence, we started exploring the alternatives. One of the alternate solutions on the the table was provided by App Center and the other was to find a similar PAAS solution from another provider. Since back-end stack was AWS, we chose to go with EAS Update.

EAS Update

EAS Update is a hosted service that serves updates for projects using expo-updates library. Once the EAS Update is configured correctly, the app will be listening for any targeted version of the app on the EAS dev cloud server. Expo provides a great documentation on setup and configuration.

How Does It Work?

In a nutshell;

  1. Integrate “EAS Updates” in the app project.
  2. The user has the app installed on their device.
  3. The development team made a bug fix/patch and generated JSbundle for the targeted app version and uploaded to the Expo.dev cloud server.
  4. Next time the user opens the app (frequencies can be configurable, we can set on app resume/start), the app will check if any bundle is available to be installed. If there is an update available, the newer version of the app from Expo will be installed on user’s device.
Over The Air Update process flow

OTA deployment process

Additional details can be found at https://docs.expo.dev/eas-update/how-it-works/.

Implementation Details:

If you are new to React Native app development, this article may help Ramp Up On React/React Native In Less Than a Month. And if you are transitioning from React to React Native, you may find this React Native – A Web Developer’s Perspective on Pivoting to Mobile useful.

I am using my existing React-Native 0.73.7 app. However, one can start a fresh React Native App for your test.

Project configuration requires us to setup expo-modules. The Expo installation guide provides an installer which handles configuration.  Our project needed an SDK 50 version of the installer.

  • Using npx install-expo-modules@0.8.1, I installed Expo, SDK-50, in alignment with our current React native version 0.73.7, which added the following dependencies.
"@expo/vector-icons": "^14.0.0",
"expo-asset": "~9.0.2",
"expo-file-system": "~16.0.9",
"expo-font": "~11.10.3",
"expo-keep-awake": "~12.8.2",
"expo-modules-autolinking": "1.10.3",
"expo-modules-core": "1.11.14",
"fbemitter": "^3.0.0",
"whatwg-url-without-unicode": "8.0.0-3"
  • Installed Expo-updates v0.24.14 package which added the following dependencies.
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "~8.5.0",
"@expo/config-plugins": "~7.9.0",
"arg": "4.1.0",
"chalk": "^4.1.2",
"expo-eas-client": "~0.11.0",
"expo-manifests": "~0.13.0",
"expo-structured-headers": "~3.7.0",
"expo-updates-interface": "~0.15.1",
"fbemitter": "^3.0.0",
"resolve-from": "^5.0.0"
  • Created expo account at https://expo.dev/signup
  • To setup the account execute, eas configure
  • This generated the project id and other account details.
  • Following channels were created: staging, uat, and production.
  • Added relevant project values to app.json, added Expo.plist, and updated same in AndroidManifest.xml.
  • Scripts block of package.json has been updated to use npx expo to launch the app.
  • AppDelegate.swift was refactored as part of the change.
  • App Center and CodePush assets and references were removed.
  • Created custom component to display a modal prompt when new update is found.

OTA Deployment:

  • Execute the command via terminal:
EAS_CHANNEL=staging RUNTIME_VERSION="7.13" eas update --message "build:[QA] - 7.13.841 - 25.5.9.4 - OTA Test2 commit"
  • Once the package is published, I can see my update available in expo.dev as shown in the image below.
EAS update OTA deployment

EAS update screen once OTA deployment is successful.

Test:

  1. Unlike App center, Expo provides the same package for iOS and Android targets.
  2. The targeted version package is available on the expo server.
  3. App restart or resume will display the popup (custom implementation) informing “A new update is available.”.
  4. When a user hits “OK” button in the popup, the update will be installed and content within the App will restart.
  5. If the app successfully restarts, the update is successfully installed.

Considerations:

  • In metro.config.js – the @rnx-kit/metro-serializer had to be commented out due to compatibility issue with EAS Update bundle process.
  • @expo/vector-icons package causes Android release build to crash on app startup. This package can be removed but if package-lock.json is removed the package will reinstall as an expo dependency and again, cause the app to crash. The issue is described in the comments here: https://github.com/expo/expo/issues/26521. There is no solution available at the moment. The expo vector icons package isn’t being handled correctly during the build process. It is caused by the react-native-elements package. When removed, the files are no longer added to app.manifest and the app builds and runs as expected.
  • Somehow the font require statements in node_modules/react-native-elements/dist/helpers/getIconType.js are being picked up during the expo-updates generation of app.manifest even though the files are not used our app. The current solution is to go ahead and include the fonts in the package but this is not optimal. Better solution is to filter those fonts from expo-updates process.

Deployment Troubleshooting:

  • Error fetching latest Expo update: Error: “channel-name” is not allowed to be empty.

The headers “expo-runtime-version”, “expo-channel-name”, and “expo-platform” are required. They can also be set with the query parameters “runtime-version”, “channel-name”, and “platform”. Learn more: https://github.com/expo/fyi/blob/main/eas-update-missing-headers.md

The configuration values for iOS app are maintained in Supporting/Expo.plist. The above error indicates that the EXUpdatesRequestHeadersblock in the plist might be missing.

OTA deployment is very useful when large number of customers are using the app and any urgent hot fix or patch needs to be released. You can set this for your lower environments as well as the production.

In my experience, it is very reliable and the expo team is doing great job on maintaining it.

So take advantage of this amazing service and Happy coding!

 

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

]]>
https://blogs.perficient.com/2025/06/02/over-the-air-ota-deployment-process-for-mobile-app/feed/ 0 349211
Android Development Codelab: Mastering Advanced Concepts https://blogs.perficient.com/2025/04/10/android-development-codelab-mastering-advanced-concepts/ https://blogs.perficient.com/2025/04/10/android-development-codelab-mastering-advanced-concepts/#respond Thu, 10 Apr 2025 22:28:06 +0000 https://blogs.perficient.com/?p=379698

 

This guide will walk you through building a small application step-by-step, focusing on integrating several powerful tools and concepts essential for modern Android development.

What We’ll Cover:

  • Jetpack Compose: Building the UI declaratively.
  • NoSQL Database (Firestore): Storing and retrieving data in the cloud.
  • WorkManager: Running reliable background tasks.
  • Build Flavors: Creating different versions of the app (e.g., dev vs. prod).
  • Proguard/R8: Shrinking and obfuscating your code for release.
  • Firebase App Distribution: Distributing test builds easily.
  • CI/CD (GitHub Actions): Automating the build and distribution process.

The Goal: Build a “Task Reporter” app. Users can add simple task descriptions. These tasks are saved to Firestore. A background worker will periodically “report” (log a message or update a counter in Firestore) that the app is active. We’ll have dev and prod flavors pointing to different Firestore collections/data and distribute the dev build for testing.

Prerequisites:

  • Android Studio (latest stable version recommended).
  • Basic understanding of Kotlin and Android development fundamentals.
  • Familiarity with Jetpack Compose basics (Composable functions, State).
  • A Google account to use Firebase.
  • A GitHub account (for CI/CD).

Let’s get started!


Step 0: Project Setup

  1. Create New Project: Open Android Studio -> New Project -> Empty Activity (choose Compose).
  2. Name: AdvancedConceptsApp (or your choice).
  3. Package Name: Your preferred package name (e.g., com.yourcompany.advancedconceptsapp).
  4. Language: Kotlin.
  5. Minimum SDK: API 24 or higher.
  6. Build Configuration Language: Kotlin DSL (build.gradle.kts).
  7. Click Finish.

Step 1: Firebase Integration (Firestore & App Distribution)

  1. Connect to Firebase: In Android Studio: Tools -> Firebase.
    • In the Assistant panel, find Firestore. Click “Get Started with Cloud Firestore”. Click “Connect to Firebase”. Follow the prompts to create a new Firebase project or connect to an existing one.
    • Click “Add Cloud Firestore to your app”. Accept changes to your build.gradle.kts (or build.gradle) files. This adds the necessary dependencies.
    • Go back to the Firebase Assistant, find App Distribution. Click “Get Started”. Add the App Distribution Gradle plugin by clicking the button. Accept changes.
  2. Enable Services in Firebase Console:
    • Go to the Firebase Console and select your project.
    • Enable Firestore Database (start in Test mode).
    • In the left menu, go to Build -> Firestore Database. Click “Create database”.
      • Start in Test mode for easier initial development (we’ll secure it later if needed). Choose a location close to your users. Click “Enable”.
    • Ensure App Distribution is accessible (no setup needed here yet).
  3. Download Initial google-services.json:
    • In Firebase Console -> Project Settings (gear icon) -> Your apps.
    • Ensure your Android app (using the base package name like com.yourcompany.advancedconceptsapp) is registered. If not, add it.
    • Download the google-services.json file.
    • Switch Android Studio to the Project view and place the file inside the app/ directory.
    • Note: We will likely replace this file in Step 4 after configuring build flavors.

Step 2: Building the Basic UI with Compose

Let’s create a simple UI to add and display tasks.

  1. Dependencies: Ensure necessary dependencies for Compose, ViewModel, Firestore, and WorkManager are in app/build.gradle.kts.
    app/build.gradle.kts

    
    dependencies {
        // Core & Lifecycle & Activity
        implementation("androidx.core:core-ktx:1.13.1") // Use latest versions
        implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
        implementation("androidx.activity:activity-compose:1.9.0")
        // Compose
        implementation(platform("androidx.compose:compose-bom:2024.04.01")) // Check latest BOM
        implementation("androidx.compose.ui:ui")
        implementation("androidx.compose.ui:ui-graphics")
        implementation("androidx.compose.ui:ui-tooling-preview")
        implementation("androidx.compose.material3:material3")
        implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1")
        // Firebase
        implementation(platform("com.google.firebase:firebase-bom:33.0.0")) // Check latest BOM
        implementation("com.google.firebase:firebase-firestore-ktx")
        // WorkManager
        implementation("androidx.work:work-runtime-ktx:2.9.0") // Check latest version
    }
                    

    Sync Gradle files.

  2. Task Data Class: Create data/Task.kt.
    data/Task.kt

    
    package com.yourcompany.advancedconceptsapp.data
    
    import com.google.firebase.firestore.DocumentId
    
    data class Task(
        @DocumentId
        val id: String = "",
        val description: String = "",
        val timestamp: Long = System.currentTimeMillis()
    ) {
        constructor() : this("", "", 0L) // Firestore requires a no-arg constructor
    }
                    
  3. ViewModel: Create ui/TaskViewModel.kt. (We’ll update the collection name later).
    ui/TaskViewModel.kt

    
    package com.yourcompany.advancedconceptsapp.ui
    
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.google.firebase.firestore.ktx.firestore
    import com.google.firebase.firestore.ktx.toObjects
    import com.google.firebase.ktx.Firebase
    import com.yourcompany.advancedconceptsapp.data.Task
    // Import BuildConfig later when needed
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.StateFlow
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.tasks.await
    
    // Temporary placeholder - will be replaced by BuildConfig field
    const val TEMPORARY_TASKS_COLLECTION = "tasks"
    
    class TaskViewModel : ViewModel() {
        private val db = Firebase.firestore
        // Use temporary constant for now
        private val tasksCollection = db.collection(TEMPORARY_TASKS_COLLECTION)
    
        private val _tasks = MutableStateFlow<List<Task>>(emptyList())
        val tasks: StateFlow<List<Task>> = _tasks
    
        private val _error = MutableStateFlow<String?>(null)
        val error: StateFlow<String?> = _error
    
        init {
            loadTasks()
        }
    
        fun loadTasks() {
            viewModelScope.launch {
                try {
                     tasksCollection.orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING)
                        .addSnapshotListener { snapshots, e ->
                            if (e != null) {
                                _error.value = "Error listening: ${e.localizedMessage}"
                                return@addSnapshotListener
                            }
                            _tasks.value = snapshots?.toObjects<Task>() ?: emptyList()
                            _error.value = null
                        }
                } catch (e: Exception) {
                    _error.value = "Error loading: ${e.localizedMessage}"
                }
            }
        }
    
         fun addTask(description: String) {
            if (description.isBlank()) {
                _error.value = "Task description cannot be empty."
                return
            }
            viewModelScope.launch {
                 try {
                     val task = Task(description = description, timestamp = System.currentTimeMillis())
                     tasksCollection.add(task).await()
                     _error.value = null
                 } catch (e: Exception) {
                    _error.value = "Error adding: ${e.localizedMessage}"
                }
            }
        }
    }
                    
  4. Main Screen Composable: Create ui/TaskScreen.kt.
    ui/TaskScreen.kt

    
    package com.yourcompany.advancedconceptsapp.ui
    
    // Imports: androidx.compose.*, androidx.lifecycle.viewmodel.compose.viewModel, java.text.SimpleDateFormat, etc.
    import androidx.compose.foundation.layout.*
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material3.*
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.yourcompany.advancedconceptsapp.data.Task
    import java.text.SimpleDateFormat
    import java.util.Date
    import java.util.Locale
    import androidx.compose.ui.res.stringResource
    import com.yourcompany.advancedconceptsapp.R // Import R class
    
    @OptIn(ExperimentalMaterial3Api::class) // For TopAppBar
    @Composable
    fun TaskScreen(taskViewModel: TaskViewModel = viewModel()) {
        val tasks by taskViewModel.tasks.collectAsState()
        val errorMessage by taskViewModel.error.collectAsState()
        var taskDescription by remember { mutableStateOf("") }
    
        Scaffold(
            topBar = {
                TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }) // Use resource for flavor changes
            }
        ) { paddingValues ->
            Column(modifier = Modifier.padding(paddingValues).padding(16.dp).fillMaxSize()) {
                // Input Row
                Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
                    OutlinedTextField(
                        value = taskDescription,
                        onValueChange = { taskDescription = it },
                        label = { Text("New Task Description") },
                        modifier = Modifier.weight(1f),
                        singleLine = true
                    )
                    Spacer(modifier = Modifier.width(8.dp))
                    Button(onClick = {
                        taskViewModel.addTask(taskDescription)
                        taskDescription = ""
                    }) { Text("Add") }
                }
                Spacer(modifier = Modifier.height(16.dp))
                // Error Message
                errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp)) }
                // Task List
                if (tasks.isEmpty() && errorMessage == null) {
                    Text("No tasks yet. Add one!")
                } else {
                    LazyColumn(modifier = Modifier.weight(1f)) {
                        items(tasks, key = { it.id }) { task ->
                            TaskItem(task)
                            Divider()
                        }
                    }
                }
            }
        }
    }
    
    @Composable
    fun TaskItem(task: Task) {
        val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) }
        Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
            Column(modifier = Modifier.weight(1f)) {
                Text(task.description, style = MaterialTheme.typography.bodyLarge)
                Text("Added: ${dateFormat.format(Date(task.timestamp))}", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
                    
  5. Update MainActivity.kt: Set the content to TaskScreen.
    MainActivity.kt

    
    package com.yourcompany.advancedconceptsapp
    
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.ui.Modifier
    import com.yourcompany.advancedconceptsapp.ui.TaskScreen
    import com.yourcompany.advancedconceptsapp.ui.theme.AdvancedConceptsAppTheme
    // Imports for WorkManager scheduling will be added in Step 3
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                AdvancedConceptsAppTheme {
                    Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                        TaskScreen()
                    }
                }
            }
            // TODO: Schedule WorkManager job in Step 3
        }
    }
                    
  6. Run the App: Test basic functionality. Tasks should appear and persist in Firestore’s `tasks` collection (initially).

Step 3: WorkManager Implementation

Create a background worker for periodic reporting.

  1. Create the Worker: Create worker/ReportingWorker.kt. (Collection name will be updated later).
    worker/ReportingWorker.kt

    
    package com.yourcompany.advancedconceptsapp.worker
    
    import android.content.Context
    import android.util.Log
    import androidx.work.CoroutineWorker
    import androidx.work.WorkerParameters
    import com.google.firebase.firestore.ktx.firestore
    import com.google.firebase.ktx.Firebase
    // Import BuildConfig later when needed
    import kotlinx.coroutines.tasks.await
    
    // Temporary placeholder - will be replaced by BuildConfig field
    const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs"
    
    class ReportingWorker(appContext: Context, workerParams: WorkerParameters) :
        CoroutineWorker(appContext, workerParams) {
    
        companion object { const val TAG = "ReportingWorker" }
        private val db = Firebase.firestore
    
        override suspend fun doWork(): Result {
            Log.d(TAG, "Worker started: Reporting usage.")
            return try {
                val logEntry = hashMapOf(
                    "timestamp" to System.currentTimeMillis(),
                    "message" to "App usage report.",
                    "worker_run_id" to id.toString()
                )
                // Use temporary constant for now
                db.collection(TEMPORARY_USAGE_LOG_COLLECTION).add(logEntry).await()
                Log.d(TAG, "Worker finished successfully.")
                Result.success()
            } catch (e: Exception) {
                Log.e(TAG, "Worker failed", e)
                Result.failure()
            }
        }
    }
                    
  2. Schedule the Worker: Update MainActivity.kt‘s onCreate method.
    MainActivity.kt additions

    
    // Add these imports to MainActivity.kt
    import android.content.Context
    import android.util.Log
    import androidx.work.*
    import com.yourcompany.advancedconceptsapp.worker.ReportingWorker
    import java.util.concurrent.TimeUnit
    
    // Inside MainActivity class, after setContent { ... } block in onCreate
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // ... existing code ...
        }
        // Schedule the worker
        schedulePeriodicUsageReport(this)
    }
    
    // Add this function to MainActivity class
    private fun schedulePeriodicUsageReport(context: Context) {
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    
        val reportingWorkRequest = PeriodicWorkRequestBuilder<ReportingWorker>(
                1, TimeUnit.HOURS // ~ every hour
             )
            .setConstraints(constraints)
            .addTag(ReportingWorker.TAG)
            .build()
    
        WorkManager.getInstance(context).enqueueUniquePeriodicWork(
            ReportingWorker.TAG,
            ExistingPeriodicWorkPolicy.KEEP,
            reportingWorkRequest
        )
        Log.d("MainActivity", "Periodic reporting work scheduled.")
    }
                    
  3. Test WorkManager:
    • Run the app. Check Logcat for messages from ReportingWorker and MainActivity about scheduling.
    • WorkManager tasks don’t run immediately, especially periodic ones. You can use ADB commands to force execution for testing:
      • Find your package name: com.yourcompany.advancedconceptsapp
      • Force run jobs: adb shell cmd jobscheduler run -f com.yourcompany.advancedconceptsapp 999 (The 999 is usually sufficient, it’s a job ID).
      • Or use Android Studio’s App Inspection tab -> Background Task Inspector to view and trigger workers.
    • Check your Firestore Console for the usage_logs collection.

Step 4: Build Flavors (dev vs. prod)

Create dev and prod flavors for different environments.

  1. Configure app/build.gradle.kts:
    app/build.gradle.kts

    
    android {
        // ... namespace, compileSdk, defaultConfig ...
    
        // ****** Enable BuildConfig generation ******
        buildFeatures {
            buildConfig = true
        }
        // *******************************************
    
        flavorDimensions += "environment"
    
        productFlavors {
            create("dev") {
                dimension = "environment"
                applicationIdSuffix = ".dev" // CRITICAL: Changes package name for dev builds
                versionNameSuffix = "-dev"
                resValue("string", "app_name", "Task Reporter (Dev)")
                buildConfigField("String", "TASKS_COLLECTION", "\"tasks_dev\"")
                buildConfigField("String", "USAGE_LOG_COLLECTION", "\"usage_logs_dev\"")
            }
            create("prod") {
                dimension = "environment"
                resValue("string", "app_name", "Task Reporter")
                buildConfigField("String", "TASKS_COLLECTION", "\"tasks\"")
                buildConfigField("String", "USAGE_LOG_COLLECTION", "\"usage_logs\"")
            }
        }
    
        // ... buildTypes, compileOptions, etc ...
    }
                    

    Sync Gradle files.

    Important: We added applicationIdSuffix = ".dev". This means the actual package name for your development builds will become something like com.yourcompany.advancedconceptsapp.dev. This requires an update to your Firebase project setup, explained next. Also note the buildFeatures { buildConfig = true } block which is required to use buildConfigField.
  2. Handling Firebase for Suffixed Application IDs

    Because the `dev` flavor now has a different application ID (`…advancedconceptsapp.dev`), the original `google-services.json` file (downloaded in Step 1) will not work for `dev` builds, causing a “No matching client found” error during build.

    You must add this new Application ID to your Firebase project:

    1. Go to Firebase Console: Open your project settings (gear icon).
    2. Your apps: Scroll down to the “Your apps” card.
    3. Add app: Click “Add app” and select the Android icon (</>).
    4. Register dev app:
      • Package name: Enter the exact suffixed ID: com.yourcompany.advancedconceptsapp.dev (replace `com.yourcompany.advancedconceptsapp` with your actual base package name).
      • Nickname (Optional): “Task Reporter Dev”.
      • SHA-1 (Optional but Recommended): Add the debug SHA-1 key from `./gradlew signingReport`.
    5. Register and Download: Click “Register app”. Crucially, download the new google-services.json file offered. This file now contains configurations for BOTH your base ID and the `.dev` suffixed ID.
    6. Replace File: In Android Studio (Project view), delete the old google-services.json from the app/ directory and replace it with the **newly downloaded** one.
    7. Skip SDK steps: You can skip the remaining steps in the Firebase console for adding the SDK.
    8. Clean & Rebuild: Back in Android Studio, perform a Build -> Clean Project and then Build -> Rebuild Project.
    Now your project is correctly configured in Firebase for both `dev` (with the `.dev` suffix) and `prod` (base package name) variants using a single `google-services.json`.
  3. Create Flavor-Specific Source Sets:
    • Switch to Project view in Android Studio.
    • Right-click on app/src -> New -> Directory. Name it dev.
    • Inside dev, create res/values/ directories.
    • Right-click on app/src -> New -> Directory. Name it prod.
    • Inside prod, create res/values/ directories.
    • (Optional but good practice): You can now move the default app_name string definition from app/src/main/res/values/strings.xml into both app/src/dev/res/values/strings.xml and app/src/prod/res/values/strings.xml. Or, you can rely solely on the resValue definitions in Gradle (as done above). Using resValue is often simpler for single strings like app_name. If you had many different resources (layouts, drawables), you’d put them in the respective dev/res or prod/res folders.
  4. Use Build Config Fields in Code:
      • Update TaskViewModel.kt and ReportingWorker.kt to use BuildConfig instead of temporary constants.

    TaskViewModel.kt change

    
    // Add this import
    import com.yourcompany.advancedconceptsapp.BuildConfig
    
    // Replace the temporary constant usage
    // const val TEMPORARY_TASKS_COLLECTION = "tasks" // Remove this line
    private val tasksCollection = db.collection(BuildConfig.TASKS_COLLECTION) // Use build config field
                        

    ReportingWorker.kt change

    
    // Add this import
    import com.yourcompany.advancedconceptsapp.BuildConfig
    
    // Replace the temporary constant usage
    // const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs" // Remove this line
    
    // ... inside doWork() ...
    db.collection(BuildConfig.USAGE_LOG_COLLECTION).add(logEntry).await() // Use build config field
                        

    Modify TaskScreen.kt to potentially use the flavor-specific app name (though resValue handles this automatically if you referenced @string/app_name correctly, which TopAppBar usually does). If you set the title directly, you would load it from resources:

     // In TaskScreen.kt (if needed)
    import androidx.compose.ui.res.stringResource
    import com.yourcompany.advancedconceptsapp.R // Import R class
    // Inside Scaffold -> topBar

    TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }) // Use string resource

  5. Select Build Variant & Test:
    • In Android Studio, go to Build -> Select Build Variant… (or use the “Build Variants” panel usually docked on the left).
    • You can now choose between devDebug, devRelease, prodDebug, and prodRelease.
    • Select devDebug. Run the app. The title should say “Task Reporter (Dev)”. Data should go to tasks_dev and usage_logs_dev in Firestore.
    • Select prodDebug. Run the app. The title should be “Task Reporter”. Data should go to tasks and usage_logs.

Step 5: Proguard/R8 Configuration (for Release Builds)

R8 is the default code shrinker and obfuscator in Android Studio (successor to Proguard). It’s enabled by default for release build types. We need to ensure it doesn’t break our app, especially Firestore data mapping.

    1. Review app/build.gradle.kts Release Build Type:
      app/build.gradle.kts

      
      android {
          // ...
          buildTypes {
              release {
                  isMinifyEnabled = true // Should be true by default for release
                  isShrinkResources = true // R8 handles both
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro" // Our custom rules file
                  )
              }
              debug {
                  isMinifyEnabled = false // Usually false for debug
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro"
                  )
              }
              // ... debug build type ...
          }
          // ...
      }
                 

      isMinifyEnabled = true enables R8 for the release build type.

    2. Configure app/proguard-rules.pro:
      • Firestore uses reflection to serialize/deserialize data classes. R8 might remove or rename classes/fields needed for this process. We need to add “keep” rules.
      • Open (or create) the app/proguard-rules.pro file. Add the following:
      
      # Keep Task data class and its members for Firestore serialization
      -keep class com.yourcompany.advancedconceptsapp.data.Task { (...); *; }
      # Keep any other data classes used with Firestore similarly
      # -keep class com.yourcompany.advancedconceptsapp.data.AnotherFirestoreModel { (...); *; }
      
      # Keep Coroutine builders and intrinsics (often needed, though AGP/R8 handle some automatically)
      -keepnames class kotlinx.coroutines.intrinsics.** { *; }
      
      # Keep companion objects for Workers if needed (sometimes R8 removes them)
      -keepclassmembers class * extends androidx.work.Worker {
          public static ** Companion;
      }
      
      # Keep specific fields/methods if using reflection elsewhere
      # -keepclassmembers class com.example.SomeClass {
      #    private java.lang.String someField;
      #    public void someMethod();
      # }
      
      # Add rules for any other libraries that require them (e.g., Retrofit, Gson, etc.)
      # Consult library documentation for necessary Proguard/R8 rules.
    • Explanation:
      • -keep class ... { <init>(...); *; }: Keeps the Task class, its constructors (<init>), and all its fields/methods (*) from being removed or renamed. This is crucial for Firestore.
      • -keepnames: Prevents renaming but allows removal if unused.
      • -keepclassmembers: Keeps specific members within a class.

3. Test the Release Build:

    • Select the prodRelease build variant.
    • Go to Build -> Generate Signed Bundle / APK…. Choose APK.
    • Create a new keystore or use an existing one (follow the prompts). Remember the passwords!
    • Select prodRelease as the variant. Click Finish.
    • Android Studio will build the release APK. Find it (usually in app/prod/release/).
    • Install this APK manually on a device: adb install app-prod-release.apk.
    • Test thoroughly. Can you add tasks? Do they appear? Does the background worker still log to Firestore (check usage_logs)? If it crashes or data doesn’t save/load correctly, R8 likely removed something important. Check Logcat for errors (often ClassNotFoundException or NoSuchMethodError) and adjust your proguard-rules.pro file accordingly.

 


 

Step 6: Firebase App Distribution (for Dev Builds)

Configure Gradle to upload development builds to testers via Firebase App Distribution.

  1. Download private key: on Firebase console go to Project Overview  at left top corner -> Service accounts -> Firebase Admin SDK -> Click on “Generate new private key” button ->
    api-project-xxx-yyy.json move this file to root project at the same level of app folder *Ensure that this file be in your local app, do not push it to the remote repository because it contains sensible data and will be rejected later
  2. Configure App Distribution Plugin in app/build.gradle.kts:
    app/build.gradle.kts

    
    // Apply the plugin at the top
    plugins {
        // ... other plugins id("com.android.application"), id("kotlin-android"), etc.
        alias(libs.plugins.google.firebase.appdistribution)
    }
    
    android {
        // ... buildFeatures, flavorDimensions, productFlavors ...
    
        buildTypes {
            getByName("release") {
                isMinifyEnabled = true // Should be true by default for release
                isShrinkResources = true // R8 handles both
                proguardFiles(
                    getDefaultProguardFile("proguard-android-optimize.txt"),
                    "proguard-rules.pro" // Our custom rules file
                )
            }
            getByName("debug") {
                isMinifyEnabled = false // Usually false for debug
                proguardFiles(
                    getDefaultProguardFile("proguard-android-optimize.txt"),
                    "proguard-rules.pro"
                )
            }
            firebaseAppDistribution {
                artifactType = "APK"
                releaseNotes = "Latest build with fixes/features"
                testers = "briew@example.com, bri@example.com, cal@example.com"
                serviceCredentialsFile="$rootDir/api-project-xxx-yyy.json"//do not push this line to the remote repository or stablish as local variable } } } 

    Add library version to libs.version.toml

    
    [versions]
    googleFirebaseAppdistribution = "5.1.1"
    [plugins]
    google-firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "googleFirebaseAppdistribution" }
    
    Ensure the plugin classpath is in the 

    project-level

     build.gradle.kts: 

    project build.gradle.kts

    
    plugins {
        // ...
        alias(libs.plugins.google.firebase.appdistribution) apply false
    }
                    

    Sync Gradle files.

  3. Upload a Build Manually:
    • Select the desired variant (e.g., devDebugdevRelease, prodDebug , prodRelease).
    • In Android Studio Terminal  run  each commmand to generate apk version for each environment:
      • ./gradlew assembleRelease appDistributionUploadProdRelease
      • ./gradlew assembleRelease appDistributionUploadDevRelease
      • ./gradlew assembleDebug appDistributionUploadProdDebug
      • ./gradlew assembleDebug appDistributionUploadDevDebug
    • Check Firebase Console -> App Distribution -> Select .dev project . Add testers or use the configured group (`android-testers`).

Step 7: CI/CD with GitHub Actions

Automate building and distributing the `dev` build on push to a specific branch.

  1. Create GitHub Repository. Create a new repository on GitHub and push your project code to it.
    1. Generate FIREBASE_APP_ID:
      • on Firebase App Distribution go to Project Overview -> General -> App ID for com.yourcompany.advancedconceptsapp.dev environment (1:xxxxxxxxx:android:yyyyyyyyyy)
      • In GitHub repository go to Settings -> Secrets and variables -> Actions -> New repository secret
      • Set the name: FIREBASE_APP_ID and value: paste the App ID generated
    2. Add FIREBASE_SERVICE_ACCOUNT_KEY_JSON:
      • open api-project-xxx-yyy.json located at root project and copy the content
      • In GitHub repository go to Settings -> Secrets and variables -> Actions -> New repository secret
      • Set the name: FIREBASE_SERVICE_ACCOUNT_KEY_JSON and value: paste the json content
    3. Create GitHub Actions Workflow File:
      • In your project root, create the directories .github/workflows/.
      • Inside .github/workflows/, create a new file named android_build_distribute.yml.
      • Paste the following content:
    4. 
      name: Android CI 
      
      on: 
        push: 
          branches: [ "main" ] 
        pull_request: 
          branches: [ "main" ] 
      jobs: 
        build: 
          runs-on: ubuntu-latest 
          steps: 
          - uses: actions/checkout@v3
          - name: set up JDK 17 
            uses: actions/setup-java@v3 
            with: 
              java-version: '17' 
              distribution: 'temurin' 
              cache: gradle 
          - name: Grant execute permission for gradlew 
            run: chmod +x ./gradlew 
          - name: Build devRelease APK 
            run: ./gradlew assembleRelease 
          - name: upload artifact to Firebase App Distribution
            uses: wzieba/Firebase-Distribution-Github-Action@v1
            with:
              appId: ${{ secrets.FIREBASE_APP_ID }}
              serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY_JSON }}
              groups: testers
              file: app/build/outputs/apk/dev/release/app-dev-release-unsigned.apk
      
    1. Commit and Push: Commit the .github/workflows/android_build_distribute.yml file and push it to your main branch on GitHub.
    1. Verify: Go to the “Actions” tab in your GitHub repository. You should see the workflow running. If it succeeds, check Firebase App Distribution for the new build. Your testers should get notified.

 


 

Step 8: Testing and Verification Summary

    • Flavors: Switch between devDebug and prodDebug in Android Studio. Verify the app name changes and data goes to the correct Firestore collections (tasks_dev/tasks, usage_logs_dev/usage_logs).
    • WorkManager: Use the App Inspection -> Background Task Inspector or ADB commands to verify the ReportingWorker runs periodically and logs data to the correct Firestore collection based on the selected flavor.
    • R8/Proguard: Install and test the prodRelease APK manually. Ensure all features work, especially adding/viewing tasks (Firestore interaction). Check Logcat for crashes related to missing classes/methods.
    • App Distribution: Make sure testers receive invites for the devDebug (or devRelease) builds uploaded manually or via CI/CD. Ensure they can install and run the app.
    • CI/CD: Check the GitHub Actions logs for successful builds and uploads after pushing to the develop branch. Verify the build appears in Firebase App Distribution.

 

Conclusion

Congratulations! You’ve navigated complex Android topics including Firestore, WorkManager, Compose, Flavors (with correct Firebase setup), R8, App Distribution, and CI/CD.

This project provides a solid foundation. From here, you can explore:

    • More complex WorkManager chains or constraints.
    • Deeper R8/Proguard rule optimization.
    • More sophisticated CI/CD pipelines (deploy signed apks/bundles, running tests, deploying to Google Play).
    • Using different NoSQL databases or local caching with Room.
    • Advanced Compose UI patterns and state management.
    • Firebase Authentication, Cloud Functions, etc.

If you want to have access to the full code in my GitHub repository, contact me in the comments.


 

Project Folder Structure (Conceptual)


AdvancedConceptsApp/
├── .git/
├── .github/workflows/android_build_distribute.yml
├── .gradle/
├── app/
│   ├── build/
│   ├── libs/
│   ├── src/
│   │   ├── main/           # Common code, res, AndroidManifest.xml
│   │   │   └── java/com/yourcompany/advancedconceptsapp/
│   │   │       ├── data/Task.kt
│   │   │       ├── ui/TaskScreen.kt, TaskViewModel.kt, theme/
│   │   │       ├── worker/ReportingWorker.kt
│   │   │       └── MainActivity.kt
│   │   ├── dev/            # Dev flavor source set (optional overrides)
│   │   ├── prod/           # Prod flavor source set (optional overrides)
│   │   ├── test/           # Unit tests
│   │   └── androidTest/    # Instrumentation tests
│   ├── google-services.json # *** IMPORTANT: Contains configs for BOTH package names ***
│   ├── build.gradle.kts    # App-level build script
│   └── proguard-rules.pro # R8/Proguard rules
├── api-project-xxx-yyy.json # Firebase service account key json
├── gradle/wrapper/
├── build.gradle.kts      # Project-level build script
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
        

 

]]>
https://blogs.perficient.com/2025/04/10/android-development-codelab-mastering-advanced-concepts/feed/ 0 379698
Comparing Figma-to-Compose Conversion Methods for Android Development https://blogs.perficient.com/2025/03/31/comparing-figma-to-compose-conversion-methods-for-android-development/ https://blogs.perficient.com/2025/03/31/comparing-figma-to-compose-conversion-methods-for-android-development/#comments Tue, 01 Apr 2025 01:55:39 +0000 https://blogs.perficient.com/?p=379466

The modern Android development landscape increasingly relies on two powerful tools: Figma for collaborative UI/UX design and Jetpack Compose for building native UIs declaratively. A crucial step in the development workflow is translating the polished designs from Figma into functional Compose code. But what’s the most effective way to do this?

Several approaches exist, each with its own strengths and weaknesses, impacting development speed, code quality, and flexibility. Let’s compare the primary methods available as of early 2025: Manual Conversion, Assisted Conversion (like the soon-to-be-sunset Relay), and AI Assistance.

1. Manual Conversion: The Foundation

This is the most traditional and widely practiced method. Developers meticulously examine the Figma design specifications (layouts, spacing, typography, colors, components) and manually write the corresponding Jetpack Compose code (Column, Row, Text, Image, custom composables, etc.). They implement state management, interaction logic, and navigation using standard Compose APIs and libraries like Navigation Compose.

Advantages:

  • Maximum Control & Flexibility: Developers have complete control over the generated code, ensuring it meets specific architectural patterns, performance requirements, and accessibility standards.
  • Optimal Code Quality: Manual implementation allows for clean, idiomatic, and maintainable Compose code tailored to the project’s needs.
  • Deep Understanding: This process forces developers to deeply understand both the design and Compose principles, leading to better overall skills.
  • Best for Complexity: Handles complex layouts, custom interactions, intricate state management, and dynamic UIs effectively.

Disadvantages:

  • Time-Consuming: Can be the slowest method, especially for complex screens or large applications.
  • Prone to Human Error: Manual translation can introduce inconsistencies or visual discrepancies compared to the Figma design if not done carefully.
  • Requires Strong Compose Skills: Developers need a solid understanding of Jetpack Compose and its best practices.
  • Effectiveness for Flows & Navigation: Navigation and complex user flows are implemented entirely by the developer using libraries like Navigation Compose. This ensures the navigation logic is robust and integrated correctly with the app’s architecture.

2. Assisted Conversion: Tools Aiming for Automation (Caution: Relay Sunset)

Tools in this category aim to automate parts of the conversion process, often via plugins or specific workflows. Relay was a prominent example from Google. It allowed designers to annotate Figma components and developers to import them as Compose code packages.

Relay (Sunsetting April 30, 2025):

Original Intent: Relay aimed to streamline the designer-developer handoff for UI components, translating design system elements into reusable Compose code.

Limitations (Even Before Sunset): Often best suited for simpler, static components. Handling complex state, interactions, or highly dynamic layouts usually required significant manual code modification after import. The generated code sometimes wasn’t as clean or idiomatic as manually written code. It introduced its own workflow and potential build complexities.

Current Status (Crucial): Relay is being sunset on April 30, 2025. This means it will no longer be supported or updated, making it not a viable option for new projects and a risk for existing ones. Teams relying on it need to migrate away.

Other Potential Tools (Less Common/Mature for Compose):

While other design-to-code tools exist for different frameworks, mature, widely adopted, and robust Figma-to-Compose converters (beyond Relay) haven’t fully materialized or gained significant traction in the Android community for generating production-ready code.

Advantages (Historically/Conceptually):

  • Potential for faster initial component generation, better consistency if the tool’s workflow is strictly followed.

Disadvantages:

  • Relay’s sunset makes it obsolete. Other tools are often immature for Compose, generate subpar code, lack flexibility, struggle with complexity, and can lead to vendor lock-in or specific workflow dependencies.
  • Effectiveness for Flows & Navigation: These tools typically focus on individual components or screens, not application-wide navigation logic. Navigation still requires manual implementation.

3. AI Assistance: The Intelligent Helper

Developers leverage Large Language Models (LLMs) and AI coding assistants (like Gemini, GitHub Copilot powered by OpenAI’s models, ChatGPT, DeepSeek Coder, etc.) to aid in the conversion process. This can take several forms:

  • Pasting descriptions or screenshots of Figma elements and asking the AI to generate corresponding Compose code snippets.
  • Asking the AI to refactor existing code or implement specific Compose patterns.
  • Using AI code completion features within the IDE (like Studio Bot powered by Gemini in Android Studio).
  • Feeding simplified representations of layouts (e.g., textual descriptions of structure and attributes) to the AI.

Determining the single best AI is difficult and context-dependent and it is important to take in account that currently are raising new AI assistant models that overcome others in less than months

  • Gemini (especially integrated into Android Studio as Studio Bot): Strong potential due to Google’s focus on Android. Designed to understand Android development contexts, including Compose. Good for generating boilerplate, explaining code, and answering Android-specific questions.
  • GitHub Copilot (OpenAI): Widely used, excellent code completion and suggestion capabilities across many languages, including Kotlin/Compose. Learns from the context of your project.
  • ChatGPT (OpenAI): Versatile for generating code snippets from descriptions, explaining concepts, and brainstorming approaches. Less integrated into the IDE workflow compared to Copilot or Studio Bot.
  • DeepSeek Coder: Specifically trained on vast amounts of code, potentially offering strong code generation capabilities, though perhaps less context-aware of specific Android/Compose nuances compared to Gemini/Studio Bot unless specifically prompted.

For Android development specifically, Gemini (via Studio Bot) has a strategic advantage due to its integration and Google’s focus. GitHub Copilot is a very strong general-purpose contender. The best choice often depends on individual workflow preference, specific task complexity, and current model performance (which evolves rapidly). Experimentation is key.

Strengths:

  • Speed Boost: Can significantly accelerate the generation of boilerplate code and simple components.
  • Learning Aid: Helps developers understand how to implement certain UI elements in Compose.
  • Idea Generation: Can suggest different implementation approaches.
  • Reduces Tedium: Automates the writing of repetitive code patterns.

Weaknesses:

  • Code Quality Varies: AI-generated code requires careful review and often refactoring to ensure it meets quality standards, follows best practices, and handles state correctly.
  • Limited Context/Complexity: AI often struggles with complex layouts, intricate state dependencies, accessibility nuances, and the overall application architecture without very specific guidance. It typically cannot “read” a Figma file directly and understand all its implications.
  • Not a Full Solution: AI is an assistant, not a replacement for a developer. It rarely produces production-ready screens directly from a design prompt without significant developer intervention.
  • Requires Verification: Always needs human oversight to catch errors, ensure visual fidelity, and implement correct logic.
  • Effectiveness for Flows & Navigation: AI can generate boilerplate code for Navigation Compose (e.g., setting up a NavHost), but it cannot design or implement the complex logic of your app’s navigation graph based solely on a Figma design. This still requires manual developer effort and architectural understanding.

Conversion Method Comparison

Feature Manual Conversion Assisted (Relay, plugins) AI Assistance
UI Accuracy High (Developer Controlled) Medium (Tool Dependent) Medium (Requires Refinement)
Code Quality High (Developer Controlled) Low-Medium (Often Needs Refactor) Variable (Requires Review)
Flexibility Very High Low Medium (Assists, doesn’t dictate)
Speed Slow Medium (Initial Setup) – Fast (Plugins) Fast (for Boilerplate/Snippets)
Complexity Handling Excellent Poor Poor-Medium (Best for simple parts)
Navigation/Flows Full Manual Implementation Not Handled Boilerplate Help Only
Learning Curve High (Requires Compose Skills) Medium (Tool Specific) – Low (Plugins) Low-Medium (Prompting/Review)
Current Viability High (Standard) None (Sunset Relay) – Low (Plugins) High (As an Assistant)

Conclusion: The Hybrid Approach

As of March 2025, with Relay sunsetting, there is no magic bullet for instantly converting complex Figma designs into perfect, production-ready Jetpack Compose applications.

  • Manual Conversion remains the most reliable method for achieving high-quality, flexible, and maintainable UI code, especially for complex screens and ensuring correct logic and state management. It’s essential for implementing navigation flows.
  • AI Assistance (using tools like Gemini/Studio Bot, Copilot, ChatGPT) is rapidly becoming an indispensable helper. It excels at accelerating development by generating boilerplate, suggesting implementations for simpler components, and reducing repetitive tasks. However, it requires significant developer oversight, review, and refinement.

The most effective strategy today is often a hybrid one:

  1. Analyze the Figma design thoroughly.
  2. Manually structure the core screen layouts, navigation, and state management logic in Compose.
  3. Leverage AI to generate code for simpler, repetitive UI elements (buttons, text fields, basic layouts) or to get initial boilerplate.
  4. Critically review and refactor any AI-generated code, integrating it into your manual structure.
  5. Manually implement complex components, interactions, and precise styling details.

Ultimately, developer skill, a deep understanding of Jetpack Compose, and careful attention to detail are paramount, regardless of the tools used. AI can augment this process, but it doesn’t replace the need for expert human developers to bridge the final gap between design and functional, high-quality code.

]]>
https://blogs.perficient.com/2025/03/31/comparing-figma-to-compose-conversion-methods-for-android-development/feed/ 1 379466
Kotlin Multiplatform vs. React Native vs. Flutter: Building Your First App https://blogs.perficient.com/2025/02/26/kotlin-multiplatform-vs-react-native-vs-flutter-building-your-first-app/ https://blogs.perficient.com/2025/02/26/kotlin-multiplatform-vs-react-native-vs-flutter-building-your-first-app/#respond Wed, 26 Feb 2025 21:50:16 +0000 https://blogs.perficient.com/?p=377508

Choosing the right framework for your first cross-platform app can be challenging, especially with so many great options available. To help you decide, let’s compare Kotlin Multiplatform (KMP), React Native, and Flutter by building a simple “Hello World” app with each framework. We’ll also evaluate them across key aspects like setup, UI development, code sharing, performance, community, and developer experience. By the end, you’ll have a clear understanding of which framework is best suited for your first app.

Building a “Hello World” App

1. Kotlin Multiplatform (KMP)

Kotlin Multiplatform allows you to share business logic across platforms while using native UI components. Here’s how to build a “Hello World” app:

Steps:

  1. Set Up the Project:
    • Install Android Studio and the Kotlin Multiplatform Mobile plugin.
    • Create a new KMP project using the “Mobile Library” template.
  2. Shared Code:In the shared module, create a Greeting class with a function to return “Hello World”.
    // shared/src/commonMain/kotlin/Greeting.kt
    class Greeting {
        fun greet(): String {
            return "Hello, World!"
        }
    }
  3. Platform-Specific UIs:For Android, use Jetpack Compose or XML layouts in the androidApp module. For iOS, use SwiftUI or UIKit in the iosApp module.Android (Jetpack Compose):
    // androidApp/src/main/java/com/example/androidApp/MainActivity.kt
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                Text(text = Greeting().greet())
            }
        }
    }

    iOS (SwiftUI):

    // iosApp/iosApp/ContentView.swift
    struct ContentView: View {
        var body: some View {
            Text(Greeting().greet())
        }
    }
  4. Run the App:Build and run the app on Android and iOS simulators/emulators.

Pros and Cons:

Pros:

  • Native performance and look.
  • Shared business logic reduces code duplication.

Cons:

  • Requires knowledge of platform-specific UIs (Jetpack Compose for Android, SwiftUI/UIKit for iOS).
  • Initial setup can be complex.

2. React Native

React Native allows you to build cross-platform apps using JavaScript and React. Here’s how to build a “Hello World” app:

Steps:

  1. Set Up the Project:
    • Install Node.js and the React Native CLI.
    • Create a new project:
      npx react-native init HelloWorldApp
  2. Write the Code:Open App.js and replace the content with the following:
    import React from 'react';
    import { Text, View } from 'react-native';
    
    const App = () => {
        return (
            <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
                <Text>Hello, World!</Text>
            </View>
        );
    };
    
    export default App;
  3. Run the App:Start the Metro bundler:
    npx react-native start

    Run the app on Android or iOS:

    npx react-native run-android
    npx react-native run-ios

Pros and Cons:

Pros:

  • Easy setup and quick development.
  • Hot reload for instant updates.

Cons:

  • Performance may suffer for complex apps due to the JavaScript bridge.
  • Limited native look and feel.

3. Flutter

Flutter is a UI toolkit for building natively compiled apps for mobile, web, and desktop using Dart. Here’s how to build a “Hello World” app:

Steps:

  1. Set Up the Project:
    • Install Flutter SDK and Android Studio/VS Code.
    • Create a new project:
      flutter create hello_world_app
  2. Write the Code:Open lib/main.dart and replace the content with the following:
    import 'package:flutter/material.dart';
    
    void main() {
        runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                home: Scaffold(
                    appBar: AppBar(title: Text('Hello World App')),
                    body: Center(child: Text('Hello, World!')),
                ),
            );
        }
    }
  3. Run the App:Run the app on Android or iOS:
    flutter run

Pros and Cons:

Pros:

  • Single codebase for UI and business logic.
  • Excellent performance and rich UI components.

Cons:

  • Larger app size compared to native apps.
  • Requires learning Dart.

Comparing the Frameworks

1. Initial Setup

  • KMP: Moderate setup complexity, especially for iOS. Requires configuring Gradle files and platform-specific dependencies.
  • React Native: Easy setup with tools like Expo and React Native CLI.
  • Flutter: Smoothest setup with the Flutter CLI and flutter doctor command.

Best option: Flutter (for ease of initial setup).

2. UI Development

  • KMP: Platform-specific UIs (Jetpack Compose for Android, SwiftUI/UIKit for iOS). Offers native flexibility but requires separate UI code.
  • React Native: Declarative UI with JSX. Powerful but can feel like a middle ground between native and custom rendering.
  • Flutter: Widget-based system for consistent cross-platform UIs. Highly customizable but requires learning Dart.

Best option: A tie between KMP (for native UI flexibility) and Flutter (for cross-platform consistency).

3. Code Sharing

  • KMP: Excels at sharing business logic while allowing native UIs.
  • React Native: High code sharing but may require platform-specific code for advanced features.
  • Flutter: High code sharing for both UI and business logic but requires Dart.

Best option: Kotlin Multiplatform (for its focus on sharing business logic).

4. Performance

  • KMP: Native performance due to native UIs and compiled shared code.
  • React Native: Good performance but can struggle with complex UIs due to the JavaScript bridge.
  • Flutter: Excellent performance, often close to native, but may not match native performance in all scenarios.

Winner: Kotlin Multiplatform (for native performance).

5. Community and Ecosystem

  • KMP: Growing community backed by JetBrains. Kotlin ecosystem is mature.
  • React Native: Large and active community with a rich ecosystem.
  • Flutter: Thriving community with strong Google support.

Best option: React Native (for its large and mature community), but Flutter is a close contender.

6. Developer Experience

  • KMP: Gentle learning curve for Kotlin developers but requires platform-specific UI knowledge.
  • React Native: Familiar for JavaScript/React developers but may require native mobile knowledge.
  • Flutter: Excellent developer experience with hot reload and comprehensive documentation.

Best option: Flutter (for its excellent developer experience and tooling).

7. AI-Assisted Development Speed

With the rise of AI tools like GitHub Copilot, ChatGPT, Gemini, Claude, etc.. Developers can significantly speed up app development. Let’s evaluate how each framework benefits from AI assistance:

  • KMP: AI tools can help generate Kotlin code for shared logic and even platform-specific UIs. However, the need for platform-specific knowledge may limit the speed gains.
  • React Native: JavaScript is widely supported by AI tools, making it easy to generate boilerplate code, components, and even entire screens. The large ecosystem also means AI can suggest relevant libraries and solutions.
  • Flutter: Dart is less commonly supported by AI tools compared to JavaScript, but Flutter’s widget-based system is highly structured, making it easier for AI to generate consistent and functional code.

Best option: React Native (due to JavaScript’s widespread support in AI tools).

The resolution:

There’s no one-size-fits-all answer. The best choice depends on your priorities:

    • Prioritize Performance and Native UI: Choose Kotlin Multiplatform.
    • Prioritize Speed of Development and a Large Community: Choose React Native.
    • Prioritize Ease of Use, Cross-Platform Consistency, and Fast Development: Choose Flutter.

For Your First App:

  • Simple App, Fast Development: Flutter is an excellent choice. Its ease of setup, hot reload, and comprehensive widget system will get you up and running quickly.
  • Existing Kotlin/Android Skills, Focus on Shared Logic: Kotlin Multiplatform allows you to leverage your existing knowledge while sharing a significant portion of your codebase.
  • Web Developer, Familiar with React: React Native is a natural fit, allowing you to utilize your web development skills for mobile development.

Conclusion

Each framework has its strengths and weaknesses, and the best choice depends on your team’s expertise, project requirements, and long-term goals. For your first app, consider starting with Flutter for its ease of use and fast development, React Native if you’re a web developer, or Kotlin Multiplatform if you’re focused on performance and native UIs.

Try building a simple app with each framework to see which one aligns best with your preferences and project requirements.

References

  1. Kotlin Multiplatform Documentation: https://kotlinlang.org/docs/multiplatform.html
  2. React Native Documentation: https://reactnative.dev/docs/getting-started
  3. Flutter Documentation: https://flutter.dev/docs
  4. JetBrains Blog on KMP: https://blog.jetbrains.com/kotlin/
  5. React Native Community: https://github.com/react-native-community
  6. Flutter Community: https://flutter.dev/community

 

]]>
https://blogs.perficient.com/2025/02/26/kotlin-multiplatform-vs-react-native-vs-flutter-building-your-first-app/feed/ 0 377508
Migrating from MVP to Jetpack Compose: A Step-by-Step Guide for Android Developers https://blogs.perficient.com/2025/02/03/migrating-from-mvp-to-jetpack-compose-a-step-by-step-guide-for-android-developers/ https://blogs.perficient.com/2025/02/03/migrating-from-mvp-to-jetpack-compose-a-step-by-step-guide-for-android-developers/#comments Mon, 03 Feb 2025 15:30:02 +0000 https://blogs.perficient.com/?p=376701

Migrating an Android App from MVP to Jetpack Compose: A Step-by-Step Guide

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development by using a declarative approach, which is a significant shift from the traditional imperative XML-based layouts. If you have an existing Android app written in Kotlin using the MVP (Model-View-Presenter) pattern with XML layouts, fragments, and activities, migrating to Jetpack Compose can bring numerous benefits, including improved developer productivity, reduced boilerplate code, and a more modern UI architecture.

In this article, we’ll walk through the steps to migrate an Android app from MVP with XML layouts to Jetpack Compose. We’ll use a basic News App to explain in detail how to migrate all layers of the app. The app has two screens:

  1. A News List Fragment to display a list of news items.
  2. A News Detail Fragment to show the details of a selected news item.

We’ll start by showing the original MVP implementation, including the Presenters, and then migrate the app to Jetpack Compose step by step. We’ll also add error handling, loading states, and use Kotlin Flow instead of LiveData for a more modern and reactive approach.

1. Understand the Key Differences

Before diving into the migration, it’s essential to understand the key differences between the two approaches:

  • Imperative vs. Declarative UI: XML layouts are imperative, meaning you define the UI structure and then manipulate it programmatically. Jetpack Compose is declarative, meaning you describe what the UI should look like for any given state, and Compose handles the rendering.
  • MVP vs. Compose Architecture: MVP separates the UI logic into Presenters and Views. Jetpack Compose encourages a more reactive and state-driven architecture, often using ViewModel and State Hoisting.
  • Fragments and Activities: In traditional Android development, Fragments and Activities are used to manage UI components. In Jetpack Compose, you can replace most Fragments and Activities with composable functions.

2. Plan the Migration

Migrating an entire app to Jetpack Compose can be a significant undertaking. Here’s a suggested approach:

  1. Start Small: Begin by migrating a single screen or component to Jetpack Compose. This will help you understand the process and identify potential challenges.
  2. Incremental Migration: Jetpack Compose is designed to work alongside traditional Views, so you can migrate your app incrementally. Use ComposeView in XML layouts or AndroidView in Compose to bridge the gap.
  3. Refactor MVP to MVVM: Jetpack Compose works well with the MVVM (Model-View-ViewModel) pattern. Consider refactoring your Presenters into ViewModels.
  4. Replace Fragments with Composable Functions: Fragments can be replaced with composable functions, simplifying navigation and UI management.
  5. Add Error Handling and Loading States: Ensure your app handles errors gracefully and displays loading states during data fetching.
  6. Use Kotlin Flow: Replace LiveData with Kotlin Flow for a more modern and reactive approach.

3. Set Up Jetpack Compose

Before starting the migration, ensure your project is set up for Jetpack Compose:

  1. Update Gradle Dependencies:
    Add the necessary Compose dependencies to your build.gradle file:

    android {
        ...
        buildFeatures {
            compose true
        }
        composeOptions {
            kotlinCompilerExtensionVersion '1.5.3'
        }
    }
    
    dependencies {
        implementation 'androidx.activity:activity-compose:1.8.0'
        implementation 'androidx.compose.ui:ui:1.5.4'
        implementation 'androidx.compose.material:material:1.5.4'
        implementation 'androidx.compose.ui:ui-tooling-preview:1.5.4'
        implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
        implementation 'androidx.navigation:navigation-compose:2.7.4' // For navigation
        implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' // For Flow
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' // For Flow
    }
  2. Enable Compose in Your Project:
    Ensure your project is using the correct Kotlin and Android Gradle plugin versions.

4. Original MVP Implementation

a. News List Fragment and Presenter

The NewsListFragment displays a list of news items. The NewsListPresenter fetches the data and updates the view.

NewsListFragment.kt

class NewsListFragment : Fragment(), NewsListView {

    private lateinit var presenter: NewsListPresenter
    private lateinit var adapter: NewsListAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_news_list, container, false)
        val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
        adapter = NewsListAdapter { newsItem -> presenter.onNewsItemClicked(newsItem) }
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(context)
        presenter = NewsListPresenter(this)
        presenter.loadNews()
        return view
    }

    override fun showNews(news: List<NewsItem>) {
        adapter.submitList(news)
    }

    override fun showLoading() {
        // Show loading indicator
    }

    override fun showError(error: String) {
        // Show error message
    }
}

NewsListPresenter.kt

class NewsListPresenter(private val view: NewsListView) {

    fun loadNews() {
        view.showLoading()
        // Simulate fetching news from a data source (e.g., API or local database)
        try {
            val newsList = listOf(
                NewsItem(id = 1, title = "News 1", summary = "Summary 1"),
                NewsItem(id = 2, title = "News 2", summary = "Summary 2")
            )
            view.showNews(newsList)
        } catch (e: Exception) {
            view.showError(e.message ?: "An error occurred")
        }
    }

    fun onNewsItemClicked(newsItem: NewsItem) {
        // Navigate to the news detail screen
        val intent = Intent(context, NewsDetailActivity::class.java).apply {
            putExtra("newsId", newsItem.id)
        }
        startActivity(intent)
    }
}

NewsListView.kt

interface NewsListView {
    fun showNews(news: List<NewsItem>)
    fun showLoading()
    fun showError(error: String)
}

b. News Detail Fragment and Presenter

The NewsDetailFragment displays the details of a selected news item. The NewsDetailPresenter fetches the details and updates the view.

NewsDetailFragment.kt

class NewsDetailFragment : Fragment(), NewsDetailView {

    private lateinit var presenter: NewsDetailPresenter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_news_detail, container, false)
        presenter = NewsDetailPresenter(this)
        val newsId = arguments?.getInt("newsId") ?: 0
        presenter.loadNewsDetail(newsId)
        return view
    }

    override fun showNewsDetail(newsItem: NewsItem) {
        view?.findViewById<TextView>(R.id.title)?.text = newsItem.title
        view?.findViewById<TextView>(R.id.summary)?.text = newsItem.summary
    }

    override fun showLoading() {
        // Show loading indicator
    }

    override fun showError(error: String) {
        // Show error message
    }
}

NewsDetailPresenter.kt

class NewsDetailPresenter(private val view: NewsDetailView) {

    fun loadNewsDetail(newsId: Int) {
        view.showLoading()
        // Simulate fetching news detail from a data source (e.g., API or local database)
        try {
            val newsItem = NewsItem(id = newsId, title = "News $newsId", summary = "Summary $newsId")
            view.showNewsDetail(newsItem)
        } catch (e: Exception) {
            view.showError(e.message ?: "An error occurred")
        }
    }
}

NewsDetailView.kt

interface NewsDetailView {
    fun showNewsDetail(newsItem: NewsItem)
    fun showLoading()
    fun showError(error: String)
}

5. Migrate to Jetpack Compose

a. Migrate the News List Fragment

Replace the NewsListFragment with a composable function. The NewsListPresenter will be refactored into a NewsListViewModel.

NewsListScreen.kt

@Composable
fun NewsListScreen(viewModel: NewsListViewModel, onItemClick: (NewsItem) -> Unit) {
    val newsState by viewModel.newsState.collectAsState()

    when (newsState) {
        is NewsState.Loading -> {
            // Show loading indicator
            CircularProgressIndicator()
        }
        is NewsState.Success -> {
            val news = (newsState as NewsState.Success).news
            LazyColumn {
                items(news) { newsItem ->
                    NewsListItem(newsItem = newsItem, onClick = { onItemClick(newsItem) })
                }
            }
        }
        is NewsState.Error -> {
            // Show error message
            val error = (newsState as NewsState.Error).error
            Text(text = error, color = Color.Red)
        }
    }
}

@Composable
fun NewsListItem(newsItem: NewsItem, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onClick() }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = newsItem.title, style = MaterialTheme.typography.h6)
            Text(text = newsItem.summary, style = MaterialTheme.typography.body1)
        }
    }
}

NewsListViewModel.kt

class NewsListViewModel : ViewModel() {

    private val _newsState = MutableStateFlow<NewsState>(NewsState.Loading)
    val newsState: StateFlow<NewsState> get() = _newsState

    init {
        loadNews()
    }

    private fun loadNews() {
        viewModelScope.launch {
            _newsState.value = NewsState.Loading
            try {
                // Simulate fetching news from a data source (e.g., API or local database)
                val newsList = listOf(
                    NewsItem(id = 1, title = "News 1", summary = "Summary 1"),
                    NewsItem(id = 2, title = "News 2", summary = "Summary 2")
                )
                _newsState.value = NewsState.Success(newsList)
            } catch (e: Exception) {
                _newsState.value = NewsState.Error(e.message ?: "An error occurred")
            }
        }
    }
}

sealed class NewsState {
    object Loading : NewsState()
    data class Success(val news: List<NewsItem>) : NewsState()
    data class Error(val error: String) : NewsState()
}

b. Migrate the News Detail Fragment

Replace the NewsDetailFragment with a composable function. The NewsDetailPresenter will be refactored into a NewsDetailViewModel.

NewsDetailScreen.kt

@Composable
fun NewsDetailScreen(viewModel: NewsDetailViewModel) {
    val newsState by viewModel.newsState.collectAsState()

    when (newsState) {
        is NewsState.Loading -> {
            // Show loading indicator
            CircularProgressIndicator()
        }
        is NewsState.Success -> {
            val newsItem = (newsState as NewsState.Success).news
            Column(modifier = Modifier.padding(16.dp)) {
                Text(text = newsItem.title, style = MaterialTheme.typography.h4)
                Text(text = newsItem.summary, style = MaterialTheme.typography.body1)
            }
        }
        is NewsState.Error -> {
            // Show error message
            val error = (newsState as NewsState.Error).error
            Text(text = error, color = Color.Red)
        }
    }
}

NewsDetailViewModel.kt

class NewsDetailViewModel : ViewModel() {

    private val _newsState = MutableStateFlow<NewsState>(NewsState.Loading)
    val newsState: StateFlow<NewsState> get() = _newsState

    fun loadNewsDetail(newsId: Int) {
        viewModelScope.launch {
            _newsState.value = NewsState.Loading
            try {
                // Simulate fetching news detail from a data source (e.g., API or local database)
                val newsItem = NewsItem(id = newsId, title = "News $newsId", summary = "Summary $newsId")
                _newsState.value = NewsState.Success(newsItem)
            } catch (e: Exception) {
                _newsState.value = NewsState.Error(e.message ?: "An error occurred")
            }
        }
    }
}

sealed class NewsState {
    object Loading : NewsState()
    data class Success(val news: NewsItem) : NewsState()
    data class Error(val error: String) : NewsState()
}

6. Set Up Navigation

Replace Fragment-based navigation with Compose navigation:

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

@Composable
fun NewsApp() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "newsList") {
        composable("newsList") {
            val viewModel: NewsListViewModel = viewModel()
            NewsListScreen(viewModel = viewModel) { newsItem ->
                navController.navigate("newsDetail/${newsItem.id}")
            }
        }
        composable("newsDetail/{newsId}") { backStackEntry ->
            val viewModel: NewsDetailViewModel = viewModel()
            val newsId = backStackEntry.arguments?.getString("newsId")?.toIntOrNull() ?: 0
            viewModel.loadNewsDetail(newsId)
            NewsDetailScreen(viewModel = viewModel)
        }
    }
}

7. Test and Iterate

After migrating the screens, thoroughly test the app to ensure it behaves as expected. Use Compose’s preview functionality to visualize your UI:

@Preview(showBackground = true)
@Composable
fun PreviewNewsListScreen() {
    NewsListScreen(viewModel = NewsListViewModel(), onItemClick = {})
}

@Preview(showBackground = true)
@Composable
fun PreviewNewsDetailScreen() {
    NewsDetailScreen(viewModel = NewsDetailViewModel())
}

8. Gradually Migrate the Entire App

Once you’re comfortable with the migration process, continue migrating the rest of your app incrementally. Use ComposeView and AndroidView to integrate Compose with existing XML

]]>
https://blogs.perficient.com/2025/02/03/migrating-from-mvp-to-jetpack-compose-a-step-by-step-guide-for-android-developers/feed/ 1 376701
Unit Testing in Android Apps: A Deep Dive into MVVM https://blogs.perficient.com/2024/11/26/unit-testing-in-android-apps-a-deep-dive-into-mvvm/ https://blogs.perficient.com/2024/11/26/unit-testing-in-android-apps-a-deep-dive-into-mvvm/#respond Tue, 26 Nov 2024 19:56:40 +0000 https://blogs.perficient.com/?p=372567

Understanding Unit Testing

Unit testing is a crucial aspect of software development, especially in complex applications like Android apps. It involves testing individual units of code, such as methods or classes, in isolation. This ensures the correctness of each component, leading to a more robust and reliable application.

Why Unit Testing in MVVM?

The Model-View-ViewModel (MVVM) architectural pattern is widely adopted in Android app development. It separates the application into three distinct layers:

  • Model: Handles data logic and interacts with data sources.
  • View: Responsible for the UI and user interactions.
  • ViewModel: Acts as a bridge between the View and Model, providing data and handling UI logic.

Unit testing each layer in an MVVM architecture offers numerous benefits:

  • Early Bug Detection: Identify and fix issues before they propagate to other parts of the app.
  • Improved Code Quality: Write cleaner, more concise, and maintainable code.
  • Accelerated Development: Refactor code and add new features with confidence.
  • Enhanced Collaboration: Maintain consistent code quality across the team.

Setting Up the Environment

  1. Android Studio: Ensure you have the latest version installed.
  2. Testing Framework: Add the necessary testing framework to your app/build.gradle file:

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
  3. Testing Library: Consider using a testing library like Mockito or MockK to create mock objects for testing dependencies.

Unit Testing ViewModels

  1. Create a Test Class: Create a separate test class for each ViewModel you want to test.
  2. Set Up Test Dependencies: Use dependency injection frameworks like Dagger Hilt or Koin to inject dependencies into your ViewModel. For testing, use mock objects to simulate the behavior of these dependencies.
  3. Write Test Cases: Write comprehensive test cases covering various scenarios:
  • Input Validation: Test how the ViewModel handles invalid input.
  • Data Transformation: Test how the ViewModel transforms data from the Model.
  • UI Updates: Test how the ViewModel updates the UI through LiveData or StateFlow.
  • Error Handling: Test how the ViewModel handles errors and exceptions.

Example:

@RunWith(AndroidJUnit4::class)
class MyViewModelTest {

    @Test
    fun `should update the UI when data is fetched successfully`() {
        // ... (Arrange)
        val viewModel = MyViewModel(mockRepository)

        // ... (Act)
        viewModel.fetchData()

        // ... (Assert)
        viewModel.uiState.observeForever { uiState ->
            assertThat(uiState.isLoading).isFalse()
            assertThat(uiState.error).isNull()
            assertThat(uiState.data).isEqualTo(expectedData)
        }
    }
}

Unit Testing Repositories

  1. Create Test Classes: Create separate test classes for each Repository class.
  2. Set Up Test Dependencies: Use dependency injection to inject dependencies into your Repository. For testing, use mock objects to simulate the behavior of data sources like databases or network APIs.
  3. Write Test Cases: Write test cases to cover:
  • Data Fetching: Test how the Repository fetches data from remote or local sources.
  • Data Storage: Test how the Repository stores and retrieves data.
  • Data Manipulation: Test how the Repository processes and transforms data.
  • Error Handling: Test how the Repository handles errors and exceptions.

Example:

@RunWith(AndroidJUnit4::class)
class MyRepositoryTest {

    @Test
    fun `should fetch data from remote source successfully`() {
        // ... (Arrange)
        val mockApi = mock(MyApi::class.java)
        val repository = MyRepository(mockApi)

        // ... (Act)
        repository.fetchData()

        // ... (Assert)
        verify(mockApi).fetchData()
    }
}

Implementing SonarQube

SonarQube is a powerful tool for code quality and security analysis. Here’s a detailed guide on how to integrate SonarQube with your Android project:

  1. Set Up SonarQube Server:
  • Install SonarQube Server: Download and install the SonarQube server on your machine or a server.
  • Configure SonarQube: Configure the server with database settings, user authentication, and other necessary parameters.
  • Start SonarQube Server: Start the SonarQube server.
  1. Configure SonarQube Scanner:
  • Install SonarQube Scanner: Download and install the SonarQube Scanner.
  • Configure Scanner Properties: Create a sonar-scanner.properties file in your project’s root directory and configure the following properties:

    sonar.host.url=http://localhost:9000
    sonar.login=your_sonar_login
    sonar.password=your_sonar_password
    sonar.projectKey=my-android-project
    sonar.projectName=My Android Project
    sonar.sources=src/main/java
    sonar.java.binaries=build/intermediates/javac/release/classes
  1. Integrate SonarQube with Your Build Process:
  • Gradle: Add the SonarQube Gradle plugin to your build.gradle file:

    plugins {
        id 'org.sonarsource.scanner-gradle' version '3.3'
    }

    Configure the plugin with your SonarQube server URL and authentication token.

  • Maven: Add the SonarQube Maven plugin to your pom.xml file. Configure the plugin with your SonarQube server URL and authentication token.
  1. Run SonarQube Analysis:
  • Execute the SonarQube analysis using the SonarQube Scanner. This can be done manually or integrated into your CI/CD pipeline.
  1. Analyze the Results:
  • Once the analysis is complete, you can view the results on the SonarQube dashboard. The dashboard provides insights into code quality, security vulnerabilities, and potential improvements.

Implementing Test Coverage with Bitrise

Test coverage measures the percentage of your code that is covered by tests. It’s a crucial metric to assess the quality of your test suite. Here’s how to measure test coverage with Bitrise:

  1. Configure Code Coverage Tool: Add a code coverage tool like JaCoCo to your project. Configure it to generate coverage reports in a suitable format (e.g., XML).
  2. Add Code Coverage Step to Bitrise Workflow: Add a step to your Bitrise Workflow to generate the code coverage report. This step should execute your tests and generate the report.
  3. Upload Coverage Report to SonarQube: Add a step to upload the generated code coverage report to SonarQube. This will allow SonarQube to analyze the report and display the coverage metrics.

Best Practices for Unit Testing

  • Write Clear and Concise Tests: Use descriptive names for test methods and variables.
  • Test Edge Cases: Consider testing scenarios with invalid input, empty data, or network errors.
  • Use a Testing Framework: A testing framework like JUnit provides a structured way to write and run tests.
  • Leverage Mocking: Use mocking frameworks like Mockito or MockK to isolate units of code and control their behavior.
  • Automate Testing: Integrate unit tests into your CI/CD pipeline to ensure code quality.
  • Review and Refactor Tests: Regularly review and refactor your tests to keep them up-to-date and maintainable.

By following these guidelines and incorporating unit testing into your development process, you can significantly improve the quality and reliability of your Android apps.

]]>
https://blogs.perficient.com/2024/11/26/unit-testing-in-android-apps-a-deep-dive-into-mvvm/feed/ 0 372567
Set Your API Performance on Fire With BlazeMeter https://blogs.perficient.com/2024/05/20/set-your-api-performance-on-fire-with-blazemeter/ https://blogs.perficient.com/2024/05/20/set-your-api-performance-on-fire-with-blazemeter/#respond Mon, 20 May 2024 15:45:43 +0000 https://blogs.perficient.com/?p=358370

BlazeMeter, continuous testing platform,  is a perfect solution for your performance needs. BlazeMeter is an open-source tool that supports Web, Mobile and API implementations. You can perform large scale load and performance testing with the ability to tweak parameters to suit your needs.

We will learn step by step process on using BlazeMeter for API testing.

Register for BlazeMeter

Enter your information on the BlazeMeter site to register and get started

Configure Your First Scenario

The first time you login, you will be taken to default view of BlazeMeter with default workspace and project. Let us start configuring a new scenario.

Create a New Project

  1. Select Projects -> Create new project
  2. Name project
  3. Select Create Test
  4. Select Performance Test
  5. Now you are taken to configuration tab

 

Update Your Scenario

  1. The left section here has your test specifications
  2. Tap on Edit link and start updating your project name, let it be “FirstLoadTest”
  3. You can define scenario and test data in Scenario Definition section
  4. For this Demo we will configure API endPoint, tap on Enter URL/API calls (see picture below)
  5. In Scenario Definition enter “https://api.demoblaze.com/entries“. So we are load testing this endpoint with GET call
  6. Lets Name this scenario “DemoWithoutParameters”
  7. Tap on three dots next to Scenario definition and duplicate the scenario
  8. Name this as “DemoWithParameters”

Test Specifications

Create TestData

Create New Csvfile

  1. Next to Scenario Definition we have TestData section, tap on it
  2. You can choose from options available, for this demo we will go with “Create New Data Entity”
  3. Lets name it “DemoTestData” and Add it
  4. Tap on + icon next to entity created for parameterization options
  5. In this example we will select New CSV File
  6. You will be taken to a data table. Rename “variableName1” to “Parameter1” and “variableName2” to “Parameter2″(our variable names are “Parameter1” and “Parameter”)
  7. Enter values as “Value1” and “Value2” and Save
  8. Configure these parameters in Query Parameters section (See picture below)
  9. Now we have successfully completed building a scenario with two endpoints, you can configure one or more endpoints in one scenario

Scenariodefinition

Configure Your First Test Run

  1. Scroll down the scenario definition window to see Load Configuration section
  2. Enter Total Users, Duration, Ramp up Time. For now we can just test with 2 users, Duration: 1minute, RampupTime: 0
  3. Once you update these details observe the graphical representation of how your Load Test is going to be in the graph displayed in this section.
  4. We can also limit Requests Per Second(RPS) by enabling the toggle button for “Limit RPS” and select requests you need to limit per second
  5. We can also change number of users at run time, but this is available with only Enterprise Plan.
  6. Lets configure LoadDistribution now in “Load Distribution” section which is right below the “Load Configuration” section
  7. Select the location from where you need the requests to trigger.
  8. We can select multiple locations and distribute load across different locations, but again this feature is available with only enterprise plan
  9. For now, lets proceed by selecting one location

Load Configuration

Failure Criteria

  1. Failure Criteria is the best approach to immediately know your LoadTest Results
  2. Do you have your failure criteria defined? If yes, you can configure that in this section. This is optional, you can skip if you don’t have failure criteria defined.
  3. You can configure multiple failure criteria as well
  4. Enable “1-min slide window eval” for evaluating your loudest prior to execution
  5. Select “Stop Test?” checkbox if you want to stop the execution in case of failure
  6. Select “Ignore failure criteria during rampup” to ignore the failures during ramp-ups
  7. You can add one or more failure criteria and select this option uniquely for each criteria
  8. Select the option “Enable 1-min slide window eval for all” on top right of this section to enable for all provided failure criteria

Failure Criteria

Test Your Scenario

  1. Run your scenario by clicking on “RunTest”
  2. Wait for launch Test window to load completely
  3. Now click on “Launch Servers” button
  4. Click on “Abort Test” to abort your execution any time
  5. Observe your execution go through different stages (Pending, Booting, Downloading and Ready)
  6. Once it reaches Ready you can see your execution progress
  7. Once the execution is done you can view the summary with status as passed/failed

Blaze Executionstatus

Analyze Your LoadTest Results

  1. The important part of performance test is to analyze your KPIs
  2. You can see different KPIs in test results summary
  3. To understand more navigate to “Timeline Report” section, bottom left you can see “KPI Panel”,this panel contains different KPIS.These KPIs can be analyzed as required
  4. By default it provides generalized view, you can select single endpoint to analyze KPIs for one particular endpoint

Blazemeter Analyze Results

Schedule Your Load Tests

  1. BlazeMeter is continuous Integration tool, you can schedule your executions and view results when required
  2. Select your test from Tests Menu on top
  3. On to left of project description window you can find SCHEDULE section
  4. Tap on Add button next to it Schedule to see schedule window
  5. Configure the scheduler with required timings and Save the scheduler
  6. The new scheduler will be added to your project
  7. Delete it by tapping on Delete icon
  8. You can add multiple schedulers
  9. Toggle on/off to activate/deactivate the schedulers

Schedule Section

BlazeMeter Pros/Cons

ProsCons
Open sourceRequires a license for additional features and support
Provides Scriptless performance testingTest results analysis requires expertise
Integration with Selenium, JMeter, Gatling, LocustNeed to integrate with Selenium/JMeter to test functional scenarios
User-friendly UI
Report Monitoring from any geographic location
Integrates with CI/CD pipelines

If you are looking for a tool that services your performance needs, BlazeMeter is your best option. You can generate scripts with its scriptless UI, simulate loads and run your tests. You can also simulate the spinning up servers, script runs and results generated within seconds.

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

]]>
https://blogs.perficient.com/2024/05/20/set-your-api-performance-on-fire-with-blazemeter/feed/ 0 358370
Level Up Your Map with the ArcGIS SDK https://blogs.perficient.com/2024/05/09/level-up-your-map-with-the-arcgis-sdk/ https://blogs.perficient.com/2024/05/09/level-up-your-map-with-the-arcgis-sdk/#respond Thu, 09 May 2024 16:12:08 +0000 https://blogs.perficient.com/?p=356410

In today’s tech-driven world, the ability to visualize data spatially has been vital for various industries. Enter ArcGIS, a Geographic Information System (GIS) developed by ESRI, which is here to help us solve our client’s needs. Let’s chart our way into the world of ArcGIS and how it empowers businesses to harness the full capabilities of today’s mapping software.

Overview

At its core, ArcGIS is a comprehensive mapping solution that enables you to deliver a high quality experience for your users. It integrates various geographic data sets, allows users to overlay layers, analyze spatial relationships and extract meaningful insights. The user-friendly features and wide array of capabilities differentiates ArcGIS from competitors.

Standard Features

ArcGIS offers a plethora of map features designed to level up your user’s experience. Basic features such as customizable basemap tiles, display the user’s location in real-time and intuitive pan and zoom functions all makes map navigation a smooth and familiar experience.

However, the true power of ArcGIS lies in its ability to visualize and interact with objects on a map. Custom-styled map markers with the same look and feel of pre-existing symbols, enables users to identify and track objects just as they’re used to seeing them. And if you have many objects in close proximity to one another? Group them together with “clusters” that can break apart or regroup at specific zoom levels.

Advanced Features

By providing methods to display object details or toggle visibility based on predefined groups, ArcGIS gives businesses the power to streamline asset management. And that just scratches the surface of the advanced features available!

With ArcGIS, you can draw on the map to indicate an area, or even let your users draw on the map themselves. You can apply a “highlight” styling on visible objects that meet a criteria. You can search for objects with a multitude of filters, such as object type, any custom attributes (defined and set by your organization’s data management team), or even search for objects within a defined geographical boundary.

The limit of its applications is your imagination.

Offline Maps

But what happens when you’re off the grid? Won’t we lose all of these convenient features? Fear not, as ArcGIS enables continued productivity even in offline environments.

By downloading map sections for offline use, users can still access critical data and functionalities without internet connectivity, a feature especially useful for your on-the-go users.

If storage space is a concern, you can decide which data points for objects are downloaded. So if your users just need to see the symbols on the map, you can omit the attributes data to cut down on payload sizes.

In conclusion, ArcGIS stands as one of the leaders in mapping technology, empowering businesses to unlock new opportunities. From basic map features to advanced asset management capabilities, ArcGIS is more than just a mapping solution—it’s a gateway to spatial intelligence. So, embrace the power of ArcGIS and chart your path to success in the digital age!

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

]]>
https://blogs.perficient.com/2024/05/09/level-up-your-map-with-the-arcgis-sdk/feed/ 0 356410
Make Your Flutter Apps Soar with Pigeon Platform Channels https://blogs.perficient.com/2024/03/21/make-your-flutter-apps-soar-with-pigeon-platform-channels/ https://blogs.perficient.com/2024/03/21/make-your-flutter-apps-soar-with-pigeon-platform-channels/#respond Thu, 21 Mar 2024 18:08:29 +0000 https://blogs.perficient.com/?p=352054

Flutter is great framework for cross platform development. It allows you to make pixel perfect apps that are generated into native code, but what happens if you need to use existing code in iOS or Android directly? For situations like these, Flutter allows you to use platform channels.

Platform channels give you access to platform-specific APIs in a language that works directly with those APIs. Platform channels are available for Kotlin or Java on Android, Swift or Objective-C on iOS and macOS, C++ on Windows and C on Linux.

More information can be found on this here https://docs.flutter.dev/platform-integration/platform-channels

The platform APIs provided by Flutter work as intended, but the whole process is a bit cumbersome to set up. Pigeon allows us to use type safety and code generation to make this process a whole lot simpler.

Create a Pigeon Plugin

We will go ahead and create a simple example api.

Let’s start by creating a new plugin called pigeon_example

flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -i swift pigeon_example
flutter pub add pigeon
flutter pub get

Platform Channel Types in Swift

Below is a list of supported types in Dart and their swift equivalents. We will use some of the most common types in our example

Dart TypesSwift Types
nullnil
boolNSNumber(value: Bool)
intNSNumber(value: Int32)
int, if 32 bits not enoughNSNumber(value: Int)
doubleNSNumber(value: Double)
StringString
Uint8ListFlutterStandardTypedData(bytes: Data)
Int32ListFlutterStandardTypedData(int32: Data)
Int64ListFlutterStandardTypedData(int64: Data)
Float32ListFlutterStandardTypedData(float32: Data)
Float64ListFlutterStandardTypedData(float64: Data)
ListArray
MapDictionary

Define Our API

In order to let Pigeon know what methods we’re going to be exposing we define our API in an abstract Dart class with the @HostApi() decorator, and its methods

Let’s define our Pigeon Example API in a new directory named pigeons.

import 'package:pigeon/pigeon.dart';

@HostApi()

abstract class ExampleApi {
bool getBool();
String getString();
func toggleValue();
}

Generate Pigeon Platform Code

Now we can let the Pigeon package do it’s magic and we can generate some code

dart run pigeon \
--input pigeons/example_api.dart \
--dart_out lib/example_api.dart \
--experimental_swift_out ios/Classes/ExampleApi.swift \
--kotlin_out ./android/app/src/main/kotlin/com/example/ExampleApi.kt \
--java_package "io.flutter.plugins"

Be sure that the paths to all of the files are correct or the next steps won’t work. Generate the code with the output for the platforms needed. This is example is going to focus on using Swift.

Add Method Implementation to the Runner

Next we need to write our native implementation of our methods. When doing this we need to add our files to the runner in Xcode to ensure that they run properly.

class ExampleApiImpl : ExampleApi{
var value = true;

func getBool(){
return value;
}
func toggleValue(){
    value = !value
  }
func getString(){
return "THIS IS AN EXAMPLE";
}

}

Add Pigeon Platform Channel to AppDelegate

You will also need to add this code in your AppDelegate.swift file

@UIApplicationMain

@objc class AppDelegate: FlutterAppDelegate {

override func application(

_ application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?

) -> Bool {

GeneratedPluginRegistrant.register(with: self)

let exampleApi = ExampleApiImpl()

let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

ExampleApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: exampleApi)




return super.application(application, didFinishLaunchingWithOptions: launchOptions)

}

}

 

Now you should be able to use your API in Dart code.

 

import 'package:flutter/material.dart';
import 'package:pigeon_example/example_api.dart';
import 'dart:async';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final exampleApi = ExampleApi();
  bool value = false;
  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
            appBar: AppBar(
              title: const Text('Plugin example app'),
            ),
            body:
                Column(mainAxisAlignment: MainAxisAlignment.center, children: [
              DefaultTextStyle(
                  style: Theme.of(context).textTheme.displayMedium!,
                  textAlign: TextAlign.center,
                  child: FutureBuilder<String>(
                    future: exampleApi
                        .getString(), // a previously-obtained Future<String> or null
                    builder:
                        (BuildContext context, AsyncSnapshot<String> snapshot) {
                      List<Widget> children = [];
                      if (snapshot.data!.isNotEmpty) {
                        children = <Widget>[
                          Text(snapshot.data ?? ''),
                        ];
                      }
                      return Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: children,
                        ),
                      );
                    },
                  )),
              Center(
                child: ElevatedButton(
                  child: const Text('Toggle Value'),
                  onPressed: () async {
                    await exampleApi.toggleValue();
                    var val = await exampleApi.getBool();
                    setState(() {
                      value = val;
                    });
                  },
                ),
              ),
              DefaultTextStyle(
                  style: Theme.of(context).textTheme.displayMedium!,
                  textAlign: TextAlign.center,
                  child: FutureBuilder<bool>(
                    future: exampleApi
                        .getBool(), // a previously-obtained Future<String> or null
                    builder:
                        (BuildContext context, AsyncSnapshot<bool> snapshot) {
                      List<Widget> children;
                      if (snapshot.data == true) {
                        children = <Widget>[
                          const Icon(
                            Icons.check_circle_outline,
                            color: Colors.green,
                            size: 60,
                          ),
                          Padding(
                            padding: const EdgeInsets.only(top: 16),
                            child: Text('Result: ${snapshot.data}'),
                          ),
                        ];
                      } else if (snapshot.data == false) {
                        children = <Widget>[
                          const Icon(
                            Icons.error_outline,
                            color: Colors.red,
                            size: 60,
                          ),
                          Padding(
                            padding: const EdgeInsets.only(top: 16),
                            child: Text('Result: ${snapshot.data}'),
                          ),
                        ];
                      } else {
                        children = const <Widget>[
                          SizedBox(
                            width: 60,
                            height: 60,
                            child: CircularProgressIndicator(),
                          ),
                          Padding(
                            padding: EdgeInsets.only(top: 16),
                            child: Text('Awaiting result...'),
                          ),
                        ];
                      }
                      return Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: children,
                        ),
                      );
                    },
                  ))
            ])));
  }
}

 

Now we can see the values from out example API in our Flutter UI. Button toggles will change our boolean value.

Simulator Screenshot Iphone 12 2024 03 21 At 12.56.11 Simulator Screenshot Iphone 12 2024 03 21 At 12.56.08

This same pattern can be used for any type of data supported by Pigeon.

Pigeon simplifies the process of creating platform channels. It also speeds up the process when multiple channels are needed. This becomes very valuable when you need a package that doesn’t have an implementation in Flutter. It’s a bit tricky to set up the first time, but once your scripts are written, modifying existing channels and creating new ones is a breeze.

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

 

]]>
https://blogs.perficient.com/2024/03/21/make-your-flutter-apps-soar-with-pigeon-platform-channels/feed/ 0 352054
Parameterize Your Automated QA Test Scenarios With Cucumber https://blogs.perficient.com/2024/02/27/parameterize-your-automated-qa-test-scenarios-with-cucumber/ https://blogs.perficient.com/2024/02/27/parameterize-your-automated-qa-test-scenarios-with-cucumber/#comments Tue, 27 Feb 2024 16:21:29 +0000 https://blogs.perficient.com/?p=355275

Creation of automated QA test scripts in Cucumber provides a low barrier of entry for your QA team. What is Cucumber? Cucumber is Behavior-Driven Development tool that uses Gherkin for scenario syntax. Gherkin is a simple, non-technical language designed for easy maintenance and readability. Gherkin can easily integrate with open-source tools like Selenium, Appium for QA Automation.

We can extend Gherkin syntax in Cucumber test scripts even further with parameters. Parametrization provides the ability to run your scenarios using different test data, write less code and reuse it in multiple locations. In this article, we will learn how to use parameters to create robust test scenarios.

Before digging deep into parameterization, let’s learn about a few keywords!

Useful Keywords:

Scenario: Group of steps that contains user actions and validations.

Test Step: Represents a single user action or validation defined in simple language and starts with keywords like Given, When, Then, And and But.

Step Definition: A method linked to each test step in a scenario. One step definition also can be linked to multiple test steps by following parameterization techniques.

Scenario Outline:  Keyword used for scenarios that contains parameters with values defined. Use Scenario Outline to enable parameters instead of the keyword Scenario. Parameters are defined as variables inside <>. The variables are defined via the Examples keyword.

Examples: Keyword to define variables for a Scenario Outline. I.E. Login credentials for test accounts.

Parameterization Scenarios:

Scenario 1:

Scenario: Verify user can login to Login Page1

Given I am in LoginPage1

When I enter username and password

Then I verify that user is logged into HomePage

And I verify that “home” tab is displayed

And I verify the page title

Scenario 2:

Scenario: Verify user can login to LoginPage2

Given I am in LoginPage2

When I enter username and password

Then I verify that user is logged into HomePage

And I verify that “user” tab is displayed

And I verify the page title and save the title

 

Parameterize with Pre-defined Values

Parameters that can allow only strongly typed values are considered as pre-defined values.

Let’s look into Given statements from above example – Scenario 1: “I am in LoginPage1” and Scenario 2: “I am in LoginPage2.” The steps are same except for the LoginPage1 and LoginPage2 values. We’re going to create a single step definition for both steps.

 

@Given(“^I am in (LoginPage1|LoginPage2)$”)

Public void iAmInLoginPage(String parameter1){

//code here

}

 

 

Note: The parametrized step definitions will start with ^ and ends with $

 

Parameterize with Undefined Values

Parameters with undefined values are variables that can have different input values.

We need to test the above scenarios with different login credentials, which can change often. This can be achieved by updating the test data in the scenario, not the entire script linked to the scenario. The test step “When I enter username and password” is the perfect candidate for our use case. Let’s use the Scenario Outline keyword and pass the parametrized values as Examples. We’ll use <> to pass the username and password to our When clause.

 

Scenario Outlines:

Scenario 1:

Scenario Outline: Verify user is able to login to LoginPage1

Given I am in LoginPage1

When I enter <username> and <password>

Then I verify that user is logged into HomePage1

And I verify that “home” tab is displayed

And I verify the page title and save the title

Examples:

|username|password|

|user1|password1|

|user2|password2|

Note: The above scenario will be executed with iterations for user1 and user2.

The step definitions should use regular expressions to pass values to the scenario as shown below.

 

@When(“^I enter (.*) and (.*)$”)

Public void iEnterUserNameAndPassword(String username,String password){

//code here

}

 

Parameterize with Fixed/Default Values:

Default values are parameters defined in test step of the scenario. We are not making use of parameter names, we are directly passing values from test steps instead.

Let’s investigate the test step Scenario 1: “And I verify that “home” tab is displayed” and Scenario 2: “And I verify that “user” tab is displayed.” Both steps are the same except for the values home and user. Even though they aren’t parameter names, they are values used directly in our test steps. However, we can still create single step definition and link it to both test steps.

 

@And(“I verify that {String} tab is displayed”)

Public void iVerifyGivenTanIsDisplayed(String tabName){

//code here

}

 

Parameterize Null Values:

There are times when a particular parameter can or cannot have a value within our scenarios. This is where we would make use of Null Value parameters.

Let’s take last steps “And I verify the page title” and “And I verify the page title and save the title.” We can create a parameter to accept “ and save the title” or null. This can be achieved with the regular expression character ? and ensure the parameter has a leading space.

@Then(“^I verify the page title( and save the title)?$”)

Public void iVerifyPageTitle(String saveTitle){

//codehere

If(saveTitle != null){

//code here

}

}

 

Test scenario parameters enable reuse for test steps across different scenarios. Your code will be more readable and portable as a result. Parameterization decreases our script efforts by linking step definitions to multiple test steps. Adoption of parameters improves code quality which will allow for less errors, easy updates and better overall code.

You will love the code efficiencies that parameters provide! Why not give them a try.

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

 

 

]]>
https://blogs.perficient.com/2024/02/27/parameterize-your-automated-qa-test-scenarios-with-cucumber/feed/ 1 355275
Generative AI Revolution: A Comparative Analysis https://blogs.perficient.com/2024/01/19/generative-ai-revolution-a-comparative-analysis/ https://blogs.perficient.com/2024/01/19/generative-ai-revolution-a-comparative-analysis/#respond Fri, 19 Jan 2024 16:21:50 +0000 https://blogs.perficient.com/?p=353976

In the world of Generative Artificial Intelligence (AI), a new era of large language models has emerged with the remarkable capabilities. ChatGPT, Gemini, Bard and Copilot have made an impact in the way we interact with mobile device and web technologies. We will perform a comparative analysis to highlight the capabilities of each tool.

 

ChatGPTGeminiBardCopilot
Training DataWebWebWebWeb
Accuracy85%85%70%80%
Recall85%95%75%82%
Precision89%90%75%90%
F1 Score91%92%75%84%
MultilingualYesYesYesYes
InputsGPT-3.5: Text Only
GPT-4.0: Text and Images
Text, Images and Google DriveText and ImagesText and Images
Real Time DataGPT-3.5: No
GPT-4.0: Yes
YesYesYes
Mobile SDKhttps://github.com/skydoves/chatgpt-androidAPI Onlyhttps://www.gemini.com/mobileAPI Only
CostGPT-3.5
GPT-4.0
Gemini Pro
Gemini Pro Vision
UndisclosedUndisclosed

Calculation Metrics:

TP – True Positive

FP – False Positive

TN – True Negative

FN – False Negative

Accuracy = (TP +TN) / (TP + FP + TN + FN)

Recall = TP / (TP + FN)

Precision = TP / (TP + FP)

F1 Score = 2 * (Precision * Recall) / (Precision + Recall)

 

Our sample data set consists of 100 queries against Gemini AI. The above formula applied calculates the following scores:

Accuracy: (85 + 0) /100 = 85%

Recall: 85/ (85 + 5) = 94.44%

Precision: 85/ (85 + 10) = 89.47%

F1-Score: 2 * (0.894 * 0.944) / (0.894 + 0.944) = 91.8%

 

Recommended AI Tool:

I recommend Gemini based on its accuracy and consistency.  The ease of integration to the iOS and Android platforms and performance stands out amongst it’s competitors. We will illustrate how easy it is to integration Gemini with 10 easy steps.

Let’s Integrate Gemini into an Android Application!

  1. Download the Android Studio preview release Canary build (Jelly Fish| 2023.3.1).
  2. Create a new project: File -> New -> New Project
  3. Select the Phone and Tablet
    1. Under New Project -> Generate API Starter
    2. Click Next to Proceed
  4. Fill all the necessary details
    1. Enter the Project Name: My Application (or whatever you want to name your project)
    2. Enter the Package Name: (com.companyname.myapplication).
    3. Select the Location to save the project
    4. Select the Minimum SDK version: as API 26 (“Oreo”;Android 8.0)
    5. Select the Build Configuration Language as Kotlin DSL(build.gradle.kts)
    6. Click Finish to proceed 
  5. Create a starter app using the Gemini API
  6. To generate the API. Go to Gemini Studio.
  7. Click Get API Key -> click Create API Key in New Project or Create API Key in Existing Project in the Google AI studio
  8. Select the API key from the Prompt and paste in the Android Studio.
  9. Click Finish to proceed.
  10. Click the Run option in the Android Studio.

And you’re up and running with Generative AI in your Android app!

I typed in “Write a hello world code in java” and Gemini responded with code snippet. You can try out various queries to personalize your newly integrated Generative AI application to your needs.

Screenshot 2024 01 17 At 10.05.06 Pm

Alternatively, you can just Download the Sample app from the GitHub and add the API key to the local.properties to run the app.

It’s essential to recognize the remarkable capabilities of Generative AI tools on the market. Comparison of various AI metrics and architecture can give insight into performance, limitations and suitability for desired tasks. As the AI landscape continues to grow and evolve, we can anticipate even more groundbreaking innovations from AI tools. These innovations will disrupt and transform industries even further as time goes on.

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

]]>
https://blogs.perficient.com/2024/01/19/generative-ai-revolution-a-comparative-analysis/feed/ 0 353976
White Label Your Mobile Apps with Azure https://blogs.perficient.com/2023/12/21/white-label-your-mobile-apps-with-azure/ https://blogs.perficient.com/2023/12/21/white-label-your-mobile-apps-with-azure/#respond Thu, 21 Dec 2023 15:44:28 +0000 https://blogs.perficient.com/?p=338661

Enterprises and organizations that manage products with overlapping feature sets often confront a unique challenge. Their core dilemma involves creating multiple branded mobile applications that share a common codebase while enabling each app to provide a distinct user experience with minimal development overhead. As a leader in custom mobile solutions, Perficient excels in white labeling mobile applications using the power and flexibility of Azure DevOps.

Tackling the White Label Challenge

Consider a scenario where your application has gained popularity, and multiple clients desire a version that reflects their own brand identity. They want their logos, color schemes, and occasionally distinct features, yet they expect the underlying functionality to be consistent. How do you meet these demands without spawning a myriad of codebases that are a nightmare to maintain? This post outlines a strategy and best practices for white labeling applications with Azure DevOps to meet this challenge head-on.

Developing a Strategy for White Label Success

White labeling transcends merely changing logos and color palettes; it requires strategic planning and an architectural approach that incorporates flexibility.

1. Application Theming

White labeling starts with theming. Brands are recognizable through their colors, icons, and fonts, making these elements pivotal in your design. Begin by conducting a thorough audit of your current style elements. Organize these elements into variables and store them centrally, setting the stage for smooth thematic transitions.

2. Establishing Your Default Configuration

Choosing a ‘default’ configuration is crucial. It sets the baseline for development and validation. The default can reflect one of your existing branded applications and acts as a unified starting point for addressing issues, whether related to implementation or theming.

3. Embracing Remote/Cloud Configurations

Tools like the Azure App Configuration SDK or Firebase Remote Configuration allow you to modify app settings without altering the code directly. Azure’s Pipeline Library also helps manage build-time settings, supporting flexible brand-specific configurations.

Using remote configurations decouples operational aspects from app logic. This approach not only supports white labeling but also streamlines the development and customization cycle.

Note: You can add your Brand from the step 2. Adding Your “Brand” Configuration to Your Build into your build artifacts, and reference the correct values in your remote configurations for your brand.

Coordinating White Labeled Mobile Apps with Azure Pipelines

With your application ready for theming and remote configuration, use Azure Pipelines to automate the build and release of your branded app artifacts. The structure of your build stages and jobs will depend on your particular needs. Here’s a pattern you can follow to organize jobs and stages for clarity and parallelization:

1. Setting Up Your Build Stage by Platforms

Organize your pipeline by platform, not brand, to reduce duplication and simplify the build process. Start with stages for iOS, Android, and other target platforms, ensuring these build successfully with your default configuration before moving to parallel build jobs.

Run unit tests side by side with this stage to catch issues sooner.

2. Adding Your “Brand” Configuration to Your Build

Keep a master list of your brands to spawn related build jobs. This could be part of a YAML template or a file in your repository. Pass the brand value to child jobs with an input variable in your YAML template to make sure the right brand configuration is used across the pipeline.

Here’s an example of triggering Android build jobs for different brands using YAML loops:

stages:
    - stage: Build
      jobs:
          - job: BuildAndroid
            strategy:
                matrix:
                    BrandA:
                        BrandName: 'BrandA'
                    BrandB:
                        BrandName: 'BrandB'
            steps:
                - template: templates/build-android.yml
                  parameters:
                      brandName: $(BrandName)

3. Creating a YAML Job to “Re-Brand” the Default Configuration

Replace static files specific to each brand using path-based scripts. Swap out the default logo at src/img/logo.png with the brand-specific logo at src/Configurations/Foo/img/logo.png during the build process for every brand apart from the default.

An example YAML snippet for this step would be:

jobs:
    - job: RebrandAssets
      displayName: 'Rebrand Assets'
      pool:
          vmImage: 'ubuntu-latest'
      steps:
          - script: |
                cp -R src/Configurations/$(BrandName)/img/logo.png src/img/logo.png
            displayName: 'Replacing the logo with a brand-specific one'

4. Publishing Your Branded Artifacts for Distribution

Once the pipeline jobs for each brand are complete, publish the artifacts to Azure Artifacts, app stores, or other channels. Ensure this process is repeatable for any configured brand to lessen the complexity of managing multiple releases.

In Azure, decide whether to categorize your published artifacts by platform or brand based on what suits your team better. Regardless of choice, stay consistent. Here’s how you might use YAML to publish artifacts:

- stage: Publish
  jobs:
      - job: PublishArtifacts
        pool:
            vmImage: 'ubuntu-latest'
        steps:
            - task: PublishBuildArtifacts@1
              inputs:
                  PathtoPublish: '$(Build.ArtifactStagingDirectory)'
                  ArtifactName: 'drop-$(BrandName)'
                  publishLocation: 'Container'

By implementing these steps and harnessing Azure Pipelines, you can skillfully manage and disseminate white-labeled mobile applications from a single codebase, making sure each brand maintains its identity while upholding a high standard of quality and consistency.

For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!

]]>
https://blogs.perficient.com/2023/12/21/white-label-your-mobile-apps-with-azure/feed/ 0 338661