Modern Android App

Best Practices 2023.

Siamak (Ash) Ashrafi
24 min readAug 13, 2023

TOC

App

  1. Description of App (video)
  2. How to build the App
  3. Running the App

Background

  1. Compose UI (Google Maps & Card Flip)
  2. UI State — StateFlow / SharedFlow / ViewModel
  3. Clean Architecture (MVVM)
  4. Modularization
  5. App Data: Yelp / GraphQL / Apollo GraphQL (Kotlin Flow)

Code Architecture

  1. Base Template — Google Android Team Architectural Template
  2. File Structure — Layers
  3. Data Flow
  4. Property-based Testing (Kotest — edge case generation with extensions)

Features

Android Arch

  • Gradle modularised project by features
  • The Clean Architecture with MVI pattern in presentation layer
  • Jetpack Compose (maps) / Material 3 design 100% Previews — for UI layer
  • Kotlin Coroutines & Kotlin Flow — for concurrency & reactive approach
  • Kotlin Serialization converter — for JSON parsing
  • Ktor — for networking
  • Hilt — for Dependency Injection pattern implementation
  • Room — for local database
  • Coil — for image loading
  • Version Catalog — for dependency management
  • JUnit5, Turbine and MockK — for unit tests
  • Jetpack Compose test dependencies and Hilt — for UI tests
  • GitHub Actions — for CI/CD
  • Renovate — to update dependencies automatically
  • KtLint and Detekt — for code linting
  • API keys stored in app’s BuildConfig

    Features
  • Do not need to give location permission to use the app.
  • Save state though restarts
  • Open source

App

Example to show the latest tools. 🧐

Yelp Map:

Use Yelp GraphQL API so show fitness/restaurants/bars on an infinitely scrollable map. You can see details (with driving directions) and choose favorites. Favorites scroll with some snazzy graphics. (see video below 👇🏾)

Video:

This video demonstrates the Yelp Finder app, a declarative reactive Android application built using Jetpack Compose and Kotlin flows. The video showcases the app’s features and how it efficiently retrieves and displays Yelp data.

Here’s a summary of the key features:

  • Map View: Makes efficient Yelp GraphQL calls for businesses on the map by category and automatically updates markers as the map moves or the category changes. Tapping a marker shows its name, rating, and centers the map on it.
  • List View: Shows the same data as the map view but in a list format. Tapping a list item shows a detailed view.
  • Detailed View: Shows the business image, location on the map, and driving directions if permissions are granted. It also has a share button for social media.
  • Favorites View: Shows businesses favorited from the list view. These favorites are also accessible from the heart icon in the bottom bar, allowing you to manage them globally.
  • Real-time Updates: All tabs share the same data source, so they update in real-time as you move the map, update the list, or change favorites.
  • Driving Directions: The detailed view instantly shows driving directions when you grant permissions. You can also re-center the map to your current location using the compass icon.
  • Global State: The app preserves its state across restarts, so you’ll always see the same data when you reopen it.

Overall, Yelp Finder is a powerful and user-friendly app that helps you explore and find businesses near you. It leverages the capabilities of Jetpack Compose and Kotlin flows to provide a smooth and efficient experience.

Code:

Much of this code (& article) was written with help from the latest experimental version of ChatGPT (somewhat useful). Most of the code generated was wrong or not the best solution but a great starting point to find the correct answer.

Research Preview — March 23

The current release version does not have the latest Android updates (It’s useless)

Background

Prerequisites to understand the code …

Compose UI

Video series explaining how Compose works by Simona Stojanovic and Jolanda Verhoef

MAD Skills: Compose Layouts and Modifiers

— — — —

Compose UI elements used in the App

Tutorial for Compose Pager.

Tutorial for Card Flip.

Tutorial for Compose Maps

Google Maps on Android now works with Compose

Google Maps for Compose

Example

Google Maps Compose Code Example — Git Repo:

  • Use LaunchedEffect — Start a coroutine from a Composable.
  • Use remember to

UI State

In Compose, the UI state is managed using state variables. These variables are marked with the mutableStateOf function, which allows the Compose runtime to automatically recompose any affected parts of the UI whenever the value of the variable changes.

State variables should be defined as part of the composable function that uses them. When the composable function is called, the state variables are initialized with their default values. If a state variable’s value is changed by the composable function, Compose will automatically update the relevant parts of the UI to reflect the new value.

To avoid unnecessary recomposition, Compose uses a unidirectional data flow approach, where state flows from the ViewModel down to the composable functions. This means that the ViewModel is responsible for holding the UI state, and the composable functions simply render the UI based on the state provided by the ViewModel.

Overall, this approach to managing UI state in Compose helps to simplify and streamline the process of building reactive and highly performant user interfaces.

UI is updated by state exposure using Kotlin Flows / Channels (StateFlow) Compose: Use mutableStateOf

UI state consumption is done by the terminal operator “.collect” Need to track the lifecycle. ViewModels last longer then then UI. Only collect when the UI is active.

Kotlin Flows

In Kotlin coroutines, StateFlow, SharedFlow, and MutableSharedFlow are all different implementations of a reactive data flow.

  • StateFlow is an observable data holder that emits the current value and then emits new updates to that value. It represents a single value that changes over time and is best suited for use cases where you need to observe a changing state.
  • SharedFlow is a hot stream of values that can be observed by multiple subscribers. It is best suited for scenarios where you need to emit events that multiple consumers can handle.
  • MutableSharedFlow is similar to SharedFlow, but it allows you to emit new values to the flow programmatically. It is best suited for use cases where you need to update the stream of values dynamically.

In summary, StateFlow is used for observing changing states, SharedFlow is used for emitting events that multiple consumers can handle, and MutableSharedFlow is used for updating the stream of values dynamically.

Nice four part series:

Declarative, Reactive, Clean Architecture (MVVM)

Declarative, Reactive, Clean Architecture in Android is an approach to Android app development that focuses on writing concise, maintainable, and scalable code that is easy to test and reason about. It consists of three main principles: declarative programming, reactive programming and clean architecture.

  • Declarative programming means that instead of writing code that describes how to do something step-by-step, we write code that describes what we want to achieve. In practice, this means using declarative programming frameworks such as Jetpack Compose for UI development and Kotlin coroutines for asynchronous operations.
  • Reactive programming is a programming paradigm that allows for asynchronous and event-driven programming by using reactive streams. In practice, this means using reactive libraries such as Kotlin Flow to handle data streams and event-driven operations. The data flows from the data layer to the domain layer, and then to the presentation layer, which reacts to the changes in the data and updates the UI accordingly.
  • Clean architecture is a software design pattern that promotes separation of concerns by dividing the app into layers. The three main layers in clean architecture are presentation, domain, and data. The presentation layer is responsible for displaying data to the user and receiving input from them. The domain layer contains business logic and use cases. The data layer handles data storage and retrieval. The layers are connected using interfaces, so they can be easily swapped out and tested independently.

Overall, this approach to Android app development focuses on writing code that is easy to read, modify, extend, test, and maintain over time.

— — —

  1. Presentation layer:
  • Responsibility is to display data and collect user input
  • This layer consists of the UI components (Composables) and their corresponding ViewModels. The ViewModel subscribes to Use Cases and gets the necessary data to display on the UI.

View: The view is responsible for rendering the UI and handling user input. In MVVM, the view is typically implemented as a composable function in Jetpack Compose. The view observes changes to the viewmodel and updates the UI as necessary.

ViewModel: The viewmodel acts as a bridge between the model and view layers. It contains the UI-related logic and exposes data to the view via reactive programming techniques such Kotlin Flow. In Clean Architecture, the viewmodel interacts with the use case layer to perform business logic operations, and with the repository layer to retrieve and save data.

  • ViewModel exposes UI state
  • UI notifies ViewModel of events
  • ViewModel updates state and is consumed by the UI

~~~~

2. Domain layer:

  • Defining the models, interfaces, use cases of the business logic
  • In this layer, the Use Cases execute the business logic. The Use Cases interact with the Repository to retrieve and save data.

Model: The model is responsible for handling business logic and data access. In Clean Architecture, this layer typically consists of entities, data sources (such as APIs or local databases), and repository interfaces that define how data can be retrieved and stored.

Use Cases: The use case contains the business logic of the application, including user workflows, data validation, and interaction with external services. In Clean Architecture, use cases are defined as interactor classes that implement interfaces defining the use cases, such as “Get User Profile” or “Update User Settings”.

Has the UseCases- Naming is {verb in present tense explainig what you are doing} {noun/what} {the world UseCase} example: getLatestGymLocationUseCase

  1. Simple
  2. Lightweight
  3. Immutable Use Suspend functions, Flows and Callbacks to message the UI Layer. Usecase use other Usecases. Lifecycle — scoped to caller / create a new instance when passing as a dependency Threading — Must be main-safe / Move job to data layer if result can be cached.

Common Tasks:

  1. Encapsulate reusable business logic.
  2. Combine data from multiple repositories

Use a usecase to combine data that is needed by the ViewModel. Use the kotlin invoke function to make it callable: suspend operator fun invoke(): List

Usecases should be thread safe as they can be called noumerous times. Everything must go though Domain Layer.

  1. Reduce complexity of UI layer
  2. Avoid duplication
  3. Improve testability
  • Case class instances should be callable as functions by defining the invoke() function with the operator modifier.
  • Use cases don’t have their own lifecycle. Instead, they’re scoped to the class that uses them.
  • Use cases from the domain layer must be main-safe!
  • Encapsulate repeatable business logic present in the UI layer in a use case class.
  • If logic involves multiple repositories create a <Complex>UseCase class to abstract the logic out of the ViewModel and make it more readable. This also makes the logic easier to test in isolation, and reusable in different parts of the app.

~~~~

3. Data layer:

  • Responsible for getting all the data (network, RoomDB, Datastore, etc.)
  • In this layer, the Repository abstracts away the data source. The Repository interacts with the necessary Data Sources to retrieve and save data.

Repository: The repository abstracts the details of data storage and retrieval from the rest of the application, allowing different data sources to be used interchangeably. Acts as a single source of truth. The repository could be used to update/resolve local cache when connecting to the API. The repository interface is typically implemented by one or more data sources, such as a remote API, local database, or cache.

Repositories Function:

  1. Expose data
  2. Centralize changes
  3. Resolve conflicts
  4. Contain business logic (domain layer)
  5. Connect to only one data source.

— — —

Great instructional video by my friend Matthias Kerat for those that need it.

— Dependency Injection —

We use Hilt Dependency Injection

Dependency Injection is a design pattern used in software engineering that helps to improve code quality, maintainability, and scalability. In Android development, DI is essential because it helps to manage dependencies and simplify code.

Hilt is a DI library that simplifies the implementation of DI in Android apps. It is built on top of the popular DI library, Dagger. Hilt is designed to reduce the amount of boilerplate code required for setting up DI in an Android app and provides a consistent and standardized approach to DI.

The following are some of the benefits of using Hilt in a modern Android app with Kotlin:

  1. Simplifies Dependency Injection: Hilt simplifies the implementation of Dependency Injection in an Android app. Developers can focus on writing the business logic of the app instead of worrying about the complexity of setting up DI.
  2. Reduces Boilerplate Code: Hilt reduces the amount of boilerplate code required for setting up DI in an Android app. This makes the codebase more concise and easier to maintain.
  3. Standardized Approach: Hilt provides a standardized approach to DI in Android apps. This makes it easier for developers to understand and work with code written by other developers.
  4. Integration with Android Architecture Components: Hilt integrates with Android Architecture Components, such as ViewModel and LiveData, making it easy to implement DI in these components.
  5. Supports Testing: Hilt supports testing, making it easy to write unit tests and integration tests for the app.

In summary, Hilt is a powerful DI library that simplifies the implementation of DI in Android apps. It reduces boilerplate code, provides a standardized approach to DI, and supports testing. Using Hilt can help to improve code quality, maintainability, and scalability in a modern Android app with Kotlin.

Ways to use Hilt:

  1. Dependency Injection: Hilt is primarily used for dependency injection, which is a technique to provide dependencies to classes rather than hardcoding them. Hilt can inject dependencies into Activities, Fragments, Services, BroadcastReceivers, ContentProviders, and ViewModels.
  2. ViewModels: Hilt can be used to inject dependencies into ViewModels. This makes it easier to manage and test ViewModels, as dependencies can be easily swapped out for mock objects during testing.
  3. Testing: Hilt makes it easy to test Android apps by providing a set of testing tools. With Hilt, you can test your app’s classes in isolation by using mocked dependencies. Hilt also provides an easy way to set up your test environment, as it manages the creation and destruction of objects.
  4. Compose: Hilt can be used to provide dependencies to Composable functions in Jetpack Compose. This allows you to easily inject dependencies into Composable functions and manage their lifecycle.
  5. WorkManager: Hilt can be used to inject dependencies into WorkManager classes. This allows you to easily manage dependencies and swap them out for mock objects during testing.
  6. Room: Hilt can be used to provide dependencies to Room database classes. This makes it easy to manage and test Room databases, as dependencies can be easily swapped out for mock objects during testing.
  7. Retrofit: Hilt can be used to provide dependencies to Retrofit classes. This allows you to easily manage and test Retrofit instances, as dependencies can be easily swapped out for mock objects during testing.

Overall, Hilt simplifies the process of dependency injection and makes it easier to manage dependencies and test Android apps.

Modularization

Android modularity is a process of breaking down a large monolithic application into smaller, independent modules. Each module has its own responsibilities and can be developed, tested, and maintained independently. The modular approach helps developers to manage large and complex projects more efficiently and allows for better collaboration among team members.

Modularization allows developers to create smaller, more focused modules that can be developed and tested independently. These modules can then be combined to create a larger application. Each module should have a clear and distinct purpose, and should be designed to be as self-contained as possible.

The main benefits of Android modularity include:

  1. Increased code reusability: Modularization allows developers to reuse code across multiple projects and modules, reducing the amount of code duplication and improving overall code quality.
  2. Better collaboration: By breaking down a large project into smaller modules, developers can work more independently and collaborate more effectively.
  3. Easier maintenance: Smaller modules are easier to maintain, test and debug, reducing the time and effort required to maintain the application.
  4. Faster build times: Modularization can reduce build times by allowing developers to build and test only the modules that have been updated, rather than the entire application.

In Android, modularity can be achieved using different approaches such as:

  1. Feature modules: These are self-contained modules that represent a feature or a use case in the application. Each feature module contains its own code, resources, and dependencies, and can be developed, tested, and deployed independently.
  2. Library modules: These are modules that contain code and resources that can be shared across multiple feature modules. Library modules are designed to be reusable and are often used for common functionality such as networking, database access, and UI components.
  3. Dynamic delivery: This is a technique that allows developers to deliver features and functionality to users on demand. With dynamic delivery, features are packaged into modules that can be downloaded and installed by the user at runtime, rather than being included in the initial installation package.

Overall, Android modularity is a powerful technique that can help developers to create more maintainable, scalable, and flexible applications.

Benefits of modularization

Scalability, Enabling work in parallel, Ownership, Encapsulation. Reduced build time, Dynamic delivery, Reusability

As explained in the article below Modularization Learning Journey:

Data — Yelp GraphQL

We use Yelp GraphQL as our data source.

Yelp GraphQL is a public API provided by Yelp that enables developers to query Yelp’s vast database of local business information using the GraphQL query language. With Yelp GraphQL, developers can retrieve information such as business names, addresses, phone numbers, hours of operation, ratings, reviews, and more. Yelp GraphQL also supports advanced filtering, sorting, and pagination options, making it easier for developers to query only the data they need. The Yelp GraphQL API is available for free with some usage limits, and it can be accessed using a variety of programming languages and frameworks.

Apollo GraphQL (Kotlin Flow) with Compose

Apollo GraphQL is an open-source, client-side library that provides an easy and efficient way to work with GraphQL APIs in various programming languages. It is designed to help developers to build high-performance, flexible, and scalable applications by providing features such as caching, real-time updates, error handling, and more. Apollo GraphQL is a popular choice for developing modern mobile and web applications that use GraphQL APIs. It simplifies the process of integrating with GraphQL servers by providing a type-safe and efficient way to query, mutate, and subscribe to data. With Apollo GraphQL, developers can focus on building features, rather than worrying about the complexities of working with GraphQL APIs.

Video Tutorial:

API Keys

Get your SHAW-1 from Gradle Terminal.

  1. Open your project in Android Studio.
  2. Click on the Gradle tab on the right side of the screen.
  3. In the Gradle console, type the following command:
signingReport

The SHA1 certificate fingerprint will be displayed in the console.

> Task :app:signingReport
Signing report
---------------------------------------------------
Key file : C:\Users\<username>\.android\debug.keystore
Alias : androiddebugkey
SHA-1 fingerprint : XC:X3:X2:XE:CF:9D:X2:X2:X4:XF:XD:99:5C:92:71:65:6D:64:FE:XB
---------------------------------------------------

In the app/src/main/AndroidManifest.xml file for the app to use maps

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />

Add dependency to TOML file

mapsplatformSecretsPlugin = "2.0.1" # mapsplatform-secrets

Added to build for the feature

plugins {
alias(libs.plugins.mapsplatform.secrets)
}
secrets {
defaultPropertiesFileName = "secrets.defaults.properties"
}

Can call `$MAPS_API_KEY` in code

class MapsClient : MapsAPI {
// "origin=${(37.7749 + Math.random()/100 )},${-122.4194 + Math.random()/100 }"
// "destination=${(37.7749 + Math.random()/100 )},${-122.4194 + Math.random()/100 }"
override fun getMapDirections(org: String, des: String): String {
// https://maps.googleapis.com/maps/api/directions/json?origin=10.3181466,123.9029382&destination=10.311795,123.915864&key=<YOUR_API_KEY>
val base = "https://maps.googleapis.com/maps/api/directions/json"
// 37.7749° N, -122.4194°
val org = "origin=$org"
val des = "destination=$des"
val call = "$base?$org&$des&key=$MAPS_API_KEY"
return URL(call).readText()
}
}

Our Base Template

The base template shares many common ideas with the Now In Android App. Learn more about that architecture. Architecture Learning Journey:

Clean architecture Allows for separation and unidirectional flow.

Uni Directional Flow though the various layers

Detailed view of the — Now In Android (NIA) App.

Detailed view of Now In Android data flow

Google Android Team Architectural Template

Base template we are using. Detailed explaining below thanks to Jose Alcérreca

These templates are not empty, they contain some opinions in the form of architectural patterns and used libraries:

Template Repo:

Ask them to add the Compose BOM (providing the code)

Which they did 🙏🏾 Thx!

Next: ask them to start using collectAsStateWithLifecycle

We do this to handle the flow subscription though screen rotation.

.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)

As explained in the above section UI State, but if you still need a video to watch …

Functionality Added to Base Template

  • Yelp GraphQL (very efficient)
  • All API keys are in local.properties and not Git.
  • Compose Google Maps
  • Features structured in clean architecture.
  • DataStore
  • Property-based Testing (KoTest)

Thanks to Simona Stojanovic for the proper way to inject the DataStore

___

NOTE :: We also enjoy building with this template:

Thanks to Krzysztof Dąbrowski

Jetpack Compose, MVI, Material 3, Kotlin coroutines/Flow, Kotlin serialization, Hilt and Room …

Compose UI testing, Turbine, MockK, JUnit5, Renovate, GitHub Actions, ktlint and detekt …

File Structure — Core Layers

The core layers are shared by all the models as explained in the video series: Architecture — MAD Skills

— — —

Clean Architecture File Structure

We follow the same structure/function as Now In Android

  • App — Brings everything together required for the app to function correctly. This includes UI scaffolding and navigation.

Core: Shared by all features.

  • Data — Network API Calls — Fetching app data from multiple sources, shared by different features.
  • Database — RoomDB & DataStore
  • Domain — Use Cases
  • Model — Model classes used throughout the app
  • Network — Making network requests and handling responses from a remote data source.
  • Testing — Testing dependencies, repositories and util classes.
  • UI — Compose Theme — UI components, composables and resources, such as icons, used by different features.
features directories

Feature Layers:

  • Tabs — Maps / List / Favorites (Start)
  • Details — Form list get the details of the place
  • Pager — Carousel of favorites
  • Store (still a work in progress)

Follows Clean Arch:

  • Lower layers can not touch any layers above.
  • Higher layers can only touch layers one level below.

Our Dependency Tree:

Data

core-data -> core-model
core-network -> core-model
core-database -> core-model

Domain

core-domain -> core-data & core-model
core-model no dependencies (as it should be)

Presentation (Features)

feature-tabs -> core-domain & core-ui
feature-pager -> core-domain & core-ui
feature-store -> core-ui

In each module we have each app.

At the top level, there is the project root directory which contains the Gradle build files and the app module directory. The app module directory typically contains the source code and resources for the app.

Within the app module directory, the package structure follows the Clean Architecture principles, with separate packages for each layer of the architecture.

The data package that contains the classes and interfaces for the repository and data sources, a domain package that contains the use case classes and domain models, and a presentation package that contains the view models, Compose UI screens, and other presentation-related classes.

NOTE: A plugin is a piece of software that adds new features to Gradle. A dependency is a library that is used by your project.

Testing

Property-based testing

By default, the androidx.test.runner.AndroidJUnitRunner does not support running JUnit5 tests. However, there are some third-party libraries available that enable running JUnit5 tests on Android devices, such as androidx.test.ext:junit-jupiter. This library provides a custom test runner called AndroidJUnit5, which extends the AndroidJUnitRunner and allows you to run JUnit5 tests on Android.

To use AndroidJUnit5, you need to include the following dependencies in your build.gradle file:

androidTestImplementation 'androidx.test.ext:junit-jupiter:1.1.3'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'

Testing is a critical part of modern Android development, particularly when using declarative, reactive, and clean architecture with Kotlin and Jetpack Compose.

With this architecture, we can easily write unit tests for the business logic in our UseCases, as well as integration tests for our Repository layer using in-memory databases.

We can also test our UI using Compose’s testing libraries, such as the createComposeRule and onNodeWithText functions.

Additionally, Hilt makes it easy to use dependency injection in our tests, allowing us to provide fake dependencies to our classes.

By testing our code, we can catch bugs early and ensure that our app functions as expected on a variety of devices and configurations.

Good introductory video

Into video to Modern Android Testing

Source:

Setup Hilt:

We define a custom test runner for an Android JUnit test. The HiltTestRunner class extends the AndroidJUnitRunner class, which is the default test runner provided by Android.

The override function newApplication() is called when the test runner is launched, and it returns an instance of the Application class for the test. In this case, it calls the newApplication() function of the AndroidJUnitRunner class, passing it the HiltTestApplication class as the application to be used for the test.

HiltTestApplication is a subclass of the Application class that is annotated with @HiltAndroidApp. This annotation is used to initialize Hilt for the application and provide the required dependencies to the classes that are being tested. By using the HiltTestRunner with HiltTestApplication, it allows Hilt to provide the necessary dependencies to the test classes as well.

found in:

package com.test.fitnessstudios.core.testing
class HiltTestRunner : AndroidJUnitRunner() {

override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
  1. Add @HiltAndroidTest — is
  2. Call @get:Rule val rule = HiltAndoridRule(this)
  3. use HiltTestApplicaton class
  4. setup() rule.inject(this)

Fake it

@TestInstallIn is an annotation from the Hilt library that is used for testing Hilt-enabled classes. It is used to specify the modules to be installed in the Hilt graph for testing. The @TestInstallIn annotation can be applied to the test class or to individual test methods.

androidx.test.core

Some of the features of androidx.test.core include:

  • AndroidJUnitRunner: a custom test runner that is used for running Android tests with JUnit 4 or JUnit 5.
  • ApplicationProvider: a class that provides access to the Application instance for the application under test.
  • ActivityScenario: a class that provides a way to launch an activity in isolation for testing.
  • LiveDataTestUtil: a class that provides a way to test LiveData instances in a synchronous and controlled manner.
  • Espresso: a testing framework that provides APIs for writing UI tests that interact with the user interface of an application.

android.compose.ui:ui-test-manifest

The ui-test-manifest plugin generates a TestRunner class that you can use to run UI tests. The generated TestRunner class extends androidx.test.runner.AndroidJUnitRunner, which is a JUnit test runner that launches an Android application and runs JUnit test cases against it. The ui-test-manifest plugin also generates a test manifest that sets up the instrumentation settings for running UI tests, such as the application package, the test package, and the runner class.

In summary, androidx.compose.ui:ui-test-manifest is used to facilitate UI testing of Compose-based apps by providing a test runner and a default test manifest.

Unit Testing

  • Write small tests that test individual pieces of code in isolation.
  • Use a testing framework such as JUnit or Spek.
  • Use test doubles such as fake objects or stubs to replace real objects in tests.
  • Use the arrange-act-assert pattern for test structure.
  • You can use Hilt to inject dependencies (fakes) into your ViewModel, repository, or other components.

DataStore Testing

Compose Testing

To test Compose functions, we can use the androidx.compose.ui:ui-test-junit4 dependency, which provides the createComposeRule() method for creating a ComposeTestRule object. This object allows us to write tests for Compose functions.

Once we have the ComposeTestRule object, we can use the setContent() method to set the Composable function that we want to test. We can then use the onNodeWithText() and performClick() methods to simulate user interactions and verify that the Composable function behaves as expected.

ViewModel Testing

To create a fake implementation of a ViewModel in our tests, we can create a subclass of our ViewModel that overrides the necessary methods with our own implementation. We can use this fake ViewModel in our tests instead of the real one. For example, if our ViewModel uses a Repository to fetch data, we can create a fake implementation of the Repository that returns a fixed set of data for our tests.

RoomDB testing

By using an in-memory database configuration, you can ensure that your tests run quickly and without interference with the production database.

Testing Kotlin Flow

Turbine

Turbine is a small testing library for kotlinx.coroutines Flow.

Testing ViewModel with Flows

Setup the default dispatcher.

https://github.com/philipplackner/KotlinFlowsGuide/blob/master/app/src/test/java/com/plcoding/kotlinflows/TestDispatchers.kt

If you are doing server side or android development then you want the modules that end with JVM, such as `kotest-property-jvm`.

  1. Add the Kotest Gradle plugin to your buildscript dependencies in your project-level build.gradle.kts file:

Gradle plugin for running JVM tests with Kotest. Requires Kotest 5.5.x or higher.

This is an alternative to using JUnit5 support and provides better output than the gradle default, especially for nested tests but should be considered alpha.

buildscript {
dependencies {
classpath("io.kotest:kotest-gradle-plugin:<version>")
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

And then the dependency:

testImplementation 'io.kotest:kotest-runner-junit5:$version'

Property Testing
The property test framework is supported on all targets.

Using AI/ML to generate the test cases.

Future plans:

--

--