Showing posts with label CI/CD. Show all posts
Showing posts with label CI/CD. Show all posts

Version Control, Branch Management, and CI/CD in Android Development

In Android app development, effective version control, branch management, and CI/CD integration are vital for maintaining code quality, collaboration, and seamless releases. This blog article will provide a comprehensive guide for Android developers to efficiently manage their Android projects, from versioning to deployment.


1. Versioning Your Android App

Versioning your Android app is crucial to track changes over time, manage releases, and ensure backward compatibility. Android uses the versionCode and versionName in build.gradle to define the version of the app.

What is Versioning?

  • versionCode: A unique integer that represents the version of your app internally. It must be incremented with every release (new features, fixes, etc.).

  • versionName: A user-readable string that identifies the version of the app (e.g., 2.3.0, 1.0.1).

Example in build.gradle (Kotlin DSL)

android {
    defaultConfig {
        applicationId = "com.example.app"
        versionCode = 10 // Increment on every release
        versionName = "2.3.0" // User-visible version
    }
}

Versioning Best Practices:

  • Use Semantic Versioning: Follow the format MAJOR.MINOR.PATCH (e.g., 2.3.0).

  • Tag Versions in Git: Create a Git tag for every release (e.g., v2.3.0), so it's easy to track versions and rollback if needed.

git tag v2.3.0
git push origin v2.3.0

2. Product Flavors and Build Variants

In Android, product flavors allow you to build different versions of your app, tailored to various configurations, environments, or users. You can have distinct versions of your app for development, testing, production, or different customers (white-labeling).

Why Use Product Flavors?

  • Multiple configurations: Set different API endpoints, themes, or features for each flavor.

  • Easy differentiation: Build variants like devDebug, qaRelease, prodRelease, etc., to test and deploy different versions of the app.

Configuring Product Flavors in build.gradle

android {
    flavorDimensions += "environment"
    productFlavors {
        create("dev") {
            dimension = "environment"
            applicationIdSuffix = ".dev"
            versionNameSuffix = "-dev"
            buildConfigField("String", "BASE_URL", "\"https://dev.api.example.com\"")
        }
        create("qa") {
            dimension = "environment"
            applicationIdSuffix = ".qa"
            versionNameSuffix = "-qa"
            buildConfigField("String", "BASE_URL", "\"https://qa.api.example.com\"")
        }
        create("prod") {
            dimension = "environment"
            buildConfigField("String", "BASE_URL", "\"https://api.example.com\"")
        }
    }
}

Types of Build Variants:

  • devDebug: For local development with debug configurations.

  • qaRelease: For testing with QA configurations.

  • prodRelease: For the final production version of the app.

You can choose and build the desired flavor directly in Android Studio’s Build Variants panel.

3. Branch Management Strategy

Branch management is a fundamental practice for maintaining a clean and efficient codebase. It allows multiple developers to work simultaneously without overwriting each other’s changes.

Recommended Branching Model

Git Flow

The Git Flow model is widely used for managing feature development, releases, and hotfixes:

main        ← production-ready code
develop     ← current development version
feature/*   ← new features (e.g., feature/login-screen)
bugfix/*    ← minor fixes
hotfix/*    ← urgent production issues
release/*   ← staging for QA and final release
  • main branch: Always contains production-ready code.

  • develop branch: The latest development code that is stable.

  • feature/* branches: Used for individual features or tasks.

  • release/* branches: A preparation for final testing and deployment.

  • hotfix/* branches: Used for critical issues in production.

Best Practices for Branch Management:

  • Start with develop: Create a feature branch from develop for new features.

git checkout develop
git checkout -b feature/onboarding-screen
  • Commit frequently: Make small, incremental commits with meaningful messages.

  • Pull regularly: Keep your branch up-to-date with develop by pulling changes frequently.

git fetch origin
git rebase origin/develop
  • Merge via PRs: Always merge feature branches through a Pull Request (PR) to ensure code quality via code reviews.

Git Commands for Branch Management:

# Create a feature branch
git checkout -b feature/login-screen

# Commit changes
git add .
git commit -m "Added login screen UI"

# Push changes
git push origin feature/login-screen

# Merge into develop
git checkout develop
git merge feature/login-screen

4. CI/CD and Git Integration

Continuous Integration (CI) and Continuous Deployment (CD) are crucial for automating the build, test, and deployment process. In Android development, CI/CD ensures that your app is always in a deployable state and that tests are run consistently.

CI/CD Workflow with GitHub Actions

A simple GitHub Actions workflow might look like this:

name: Android CI

on:
  push:
    branches: [ develop, release/* ]
  pull_request:
    branches: [ develop ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Build devDebug
        run: ./gradlew assembleDevDebug

      - name: Run tests
        run: ./gradlew testDevDebugUnitTest

CI/CD Best Practices:

  • Automate Builds: Trigger builds automatically on PRs and pushes to develop or release/*.

  • Run Unit/UI Tests: Ensure every commit and PR runs unit tests (JUnit, Espresso) and static code checks (ktlint, detekt).

  • Build APK/AAB: Use assembleRelease or bundleRelease to build your app for distribution.


5. Release & Distribution

Once your app is ready for production, it's time to distribute it to users. You can either publish it to the Google Play Store or distribute it to a group of testers via Firebase App Distribution.

Release to Google Play Store:

  • Signing: Use a signed APK or AAB.

  • Versioning: Ensure versionCode is incremented.

  • Use Fastlane for Automation:

    • Automate screenshots, changelogs, and upload to the Play Store.

fastlane android beta

Firebase App Distribution:

Firebase App Distribution is great for distributing pre-release versions to testers.

firebase appdistribution:distribute app-release.apk \
  --app <APP_ID> \
  --groups "qa-team"

Challenges and Solutions
  • Challenge: Managing multiple build variants (e.g., for different countries or environments). Solution: Use Gradle flavors (e.g., country1Debug, country2Release) and automate variant selection in the pipeline.
  • Challenge: Slow build times. Solution: Cache dependencies and parallelize test execution.
  • Challenge: App store approval delays. Solution: Use continuous delivery to deploy to beta channels for early feedback, reserving continuous deployment for internal environments.

My thoughts

Managing versioning, flavors, branches, and CI/CD in Android development is crucial to ensure smooth, scalable, and maintainable app development. By adopting a structured approach to versioning, branching, and automation, you can streamline development, reduce errors, and improve the release process.

With the strategies outlined above, your Android development workflow will be efficient, collaborative, and optimized for both feature development and rapid deployment.


📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! 👇

Happy coding! 💻

Shipping High-Quality Android Code with Testing in Iterative Cycles (Kotlin + Jetpack Compose)

In modern Android development, building robust apps goes far beyond just writing code. It’s about delivering high-quality features in iterative cycles with the confidence that everything works as expected. This article walks you through a full feature development cycle in Android using Kotlin, Jetpack Compose, and modern testing practices.

We’ll build a simple feature: a button that fetches and displays user data. You'll learn how to:

  • Structure code using ViewModel and Repository

  • Write unit and UI tests

  • Integrate automated testing in CI/CD pipelines

Let’s dive in 

Full Cycle with Testing - Detailed Steps


Step 1: Set up your environment and dependencies

Before starting, ensure your project has the following dependencies in build.gradle (Module-level):

dependencies {
    implementation "androidx.compose.ui:ui:1.3.0"
    implementation "androidx.compose.material3:material3:1.0.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
    testImplementation "junit:junit:4.13.2"
    testImplementation "org.mockito:mockito-core:4.0.0"
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.0"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.0"
}

Step 2: Implement the Feature

Let’s assume the feature involves a button that, when clicked, fetches user data from an API.

2.1: Create the User Data Model

data class User(val id: Int, val name: String)

2.2: Create the UserRepository to Fetch Data

class UserRepository {
    // Simulate network request with delay
    suspend fun fetchUserData(): User {
        delay(1000)  // Simulating network delay
        return User(id = 1, name = "John Doe")
    }
}

2.3: Create the UserViewModel to Expose Data

class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _userData = MutableLiveData<User?>()
    val userData: LiveData<User?> get() = _userData

    fun fetchUser() {
        viewModelScope.launch {
            _userData.value = repository.fetchUserData()
        }
    }
}

2.4: Create the UserScreen Composable to Display Data

@Composable
fun UserScreen(viewModel: UserViewModel) {
    val user by viewModel.userData.observeAsState()
    Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { viewModel.fetchUser() }) {
            Text("Fetch User")
        }
        user?.let {
            Text(text = "User: ${it.name}")
        } ?: Text(text = "No user fetched yet")
    }
}

Step 3: Write Unit Tests

3.1: Unit Test for UserRepository

Write a simple unit test to test that UserRepository correctly fetches user data.

@RunWith(MockitoJUnitRunner::class)
class UserRepositoryTest {
    private lateinit var repository: UserRepository

    @Before
    fun setup() {
        repository = UserRepository()
    }

    @Test
    fun testFetchUserData() = runBlocking {
        val result = repository.fetchUserData()
        assertEquals(1, result.id)
        assertEquals("John Doe", result.name)
    }
}

3.2: Unit Test for UserViewModel

Next, test that the UserViewModel correctly interacts with the repository and updates the UI state.

class UserViewModelTest {
    private lateinit var viewModel: UserViewModel
    private lateinit var repository: UserRepository

    @Before
    fun setup() {
        repository = mock(UserRepository::class.java)
        viewModel = UserViewModel(repository)
    }

    @Test
    fun testFetchUser() = runBlocking {
        val user = User(1, "John Doe")
        `when`(repository.fetchUserData()).thenReturn(user)

        viewModel.fetchUser()

        val value = viewModel.userData.getOrAwaitValue()  // Use LiveData testing extensions
        assertEquals(user, value)
    }
}

Step 4: Write UI Tests with Jetpack Compose

4.1: Write Compose UI Test

Use ComposeTestRule to test the UserScreen composable.

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testFetchUserButton_click() {
    val viewModel = UserViewModel(UserRepository())

    composeTestRule.setContent {
        UserScreen(viewModel)
    }

    // Assert that the UI initially shows the "No user fetched yet" text
    composeTestRule.onNodeWithText("No user fetched yet").assertIsDisplayed()

    // Simulate button click
    composeTestRule.onNodeWithText("Fetch User").performClick()

    // After clicking, assert that "User: John Doe" is displayed
    composeTestRule.onNodeWithText("User: John Doe").assertIsDisplayed()
}

4.2: Write Espresso Test for Hybrid UI (if needed)

In case you’re testing a hybrid UI (Jetpack Compose + XML Views), you can also use Espresso.

@Test
fun testFetchUserButton_click() {
    onView(withId(R.id.fetchUserButton)).perform(click())
    onView(withId(R.id.resultText)).check(matches(withText("User: John Doe")))
}

Step 5: Run All Tests in CI/CD

  • Set up GitHub Actions or Jenkins to automatically run the tests on every push to the repository.

Here’s an example configuration for GitHub Actions:

name: Android CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up JDK
        uses: actions/setup-java@v2
        with:
          java-version: '11'

      - name: Build and test
        run: ./gradlew build testDebugUnitTest connectedDebugAndroidTest

Step 6: Code Review and Refactor

With your code and tests in place:

  •  Refactor for better architecture

  •  Add new states like loading and error

  •  Add tests for edge cases

  •  Merge changes with confidence

  •  Release to staging/production


Step 7: Deploy to Staging or Production

After successful tests, deploy the feature to a staging environment or directly to production, depending on your CI/CD pipeline.


Key Takeaways:

  1. Iterative development: Implement small, testable features and enhance them with every iteration.

  2. Automate testing: Unit tests, UI tests, and integration tests should be automated and run on every code change.

  3. Focus on user behavior: Write tests that mimic real user behavior (e.g., pressing buttons and verifying UI changes).

  4. Continuous integration: Set up CI/CD pipelines to run tests automatically and ensure the stability of your app at all times.

By following this cycle, you ensure that the app is always in a deployable state, with tests proving that each feature works as expected.

Final Thoughts

Iterative development backed by strong testing practices ensures you can deliver robust Android features without fear of breaking the app. With tools like Jetpack Compose, JUnit, and CI/CD, it’s never been easier to ship confidently.

💡 “The best code is not only written, it's verified and trusted by tests.”



📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! 👇

Happy coding! 💻✨

Implementing Continuous Integration (CI) for Android Applications

 Continuous Integration (CI) is an essential practice in modern software development that helps ensure code quality, minimize integration issues, and deliver a stable product. In this article, we will discuss the importance of CI for Android development and provide a step-by-step guide to implement a CI pipeline effectively, complete with code examples.



Why Continuous Integration is Crucial for Android Apps

CI enables developers to merge their code changes into a shared repository multiple times a day. By running automated tests and builds with each integration, CI ensures that errors are caught early in the development process, preventing them from accumulating and becoming difficult to resolve.

Key benefits of CI for Android apps include:

  • Automated Testing: Ensures new code doesn’t break existing functionality.

  • Early Issue Detection: Detects integration issues early, reducing time-consuming debugging sessions.

  • Better Collaboration: Allows developers to work collaboratively without worrying about breaking changes.

  • Fast Feedback: Provides fast feedback to developers, allowing them to address problems quickly.

Tools for CI in Android Development

To implement CI for Android, there are several tools and services available:

  1. Jenkins: A popular, open-source automation server that is highly configurable.

  2. GitHub Actions: CI/CD workflows directly integrated with GitHub repositories.

  3. Bitrise: A cloud-based CI/CD service that is designed specifically for mobile applications.

  4. CircleCI: Known for its fast performance and easy integration with GitHub and Bitbucket.

  5. GitLab CI: CI/CD integration available for projects hosted on GitLab.

Step-by-Step Guide to Set Up CI for an Android Project

Step 1: Configure Version Control

Start by configuring your version control system. Git is widely used for Android projects, and CI pipelines are typically triggered by changes in the Git repository. Consider using platforms like GitHub, Bitbucket, or GitLab to host your repository.

Step 2: Choose a CI Tool

Select a CI tool based on your project needs. For this guide, let’s use GitHub Actions to set up CI.

Step 3: Define Your Build Workflow

In GitHub Actions, you define workflows using a YAML file placed in the .github/workflows/ directory within your repository. Here’s a sample configuration for an Android project:

name: Android CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    - name: Cache Gradle files
      uses: actions/cache@v2
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Build with Gradle
      run: ./gradlew build

    - name: Run Unit Tests
      run: ./gradlew test

    - name: Lint Checks
      run: ./gradlew lint

CI builds require that all dependencies are defined in the build.gradle file. Make sure all dependencies are resolved through repositories like Maven Central or JitPack so that the CI tool can download them without manual intervention.

Step 5: Run Tests

Testing is a critical component of CI. Make sure to include unit tests (using JUnit), UI tests (using Espresso), and integration tests as part of your build pipeline. The workflow above includes a step to run unit tests with Gradle.

Step 6: Code Quality Checks

Tools like Detekt (for Kotlin) or Lint can be added to the CI pipeline to enforce coding standards and ensure code quality. Adding a lint check step will help identify any code issues before merging.

Step 7: Configure Notifications

Configure notifications to keep the team informed about build status. GitHub Actions will show the status of the build on pull requests, and you can also set up Slack or email notifications for build failures.

Best Practices for CI in Android Projects

  1. Keep Builds Fast: Use caching to reduce build times. Cache Gradle dependencies and any other artifacts that take time to generate.

  2. Run Tests in Parallel: For faster feedback, configure the CI pipeline to run unit tests and UI tests in parallel.

  3. Automate Everything: Automate not just builds and tests, but also code quality checks, deployment, and versioning.

  4. Fail Fast: Ensure that the build fails immediately upon detecting an issue. This makes debugging easier and prevents cascading errors.

  5. Use Emulator Snapshots: For UI testing, use emulator snapshots to reduce the time required to boot up emulators.

Conclusion

Implementing Continuous Integration for Android applications helps ensure the stability, reliability, and quality of your app. With tools like GitHub Actions, Jenkins, and Bitrise, you can create automated pipelines that continuously verify the health of your project. By following best practices, you can streamline development, improve collaboration, and ultimately deliver a better product to your users.

By implementing these steps, you can create a reliable and efficient CI pipeline for your Android applications, making your development process smoother and your final product more robust.

#Kotlin #Code4Kotlin #CI