AshBike MAD
Modern Android Development
The BasePro project is a sophisticated, multi-module monorepo designed as a scalable foundation for building modern, feature-rich Android applications. Its architecture emphasizes a clean separation of concerns, reusability, and maintainability.
Please look for updates here: README.md
Core Architectural Pillars
- Layered Monorepo Structure: The project is intelligently organized into three distinct layers, which allows for maximum code reuse and scalability:
- Core Layer: Provides the foundational building blocks for all applications, including shared UI components (
AshBikeTheme), data models, database access, and utility functions. - Feature Layer: Contains encapsulated business logic for specific functionalities. This is the powerhouse of the project, with independent modules for key capabilities.
- Application Layer: The top layer, which consists of the final, shippable products (like
AshBike) that are assembled from the various feature and core modules. - Clean Architecture & Separation of Concerns: The design strictly separates the UI (Jetpack Compose), business logic (ViewModels and UseCases), and data layers. This makes the codebase highly testable, easier to debug, and simpler to update.
- Robust Background Processing: A key feature, demonstrated in the
AshBikeapp, is the use of aForegroundServicefor critical tasks like live ride tracking. This ensures that data collection (like GPS location) is reliable and continues uninterrupted, even when the app is not in the foreground.
Example Application: AshBike
Using this project makes building Android applications like stacking Lego® Bricks.
AshBike Clean Architecture
Clean Architecture allows for rapid development. Details explained in the full article below 👇🏽.
+------------------------------------------------------------------+
| UI Layer (Jetpack Compose) - e.g., BikeUiRoute |
| - Observes ViewModel State |
| - Sends User Events |
+---------------------------------▲--------------------------------+
│ (UI State)
│
▼ (Events)
+------------------------------------------------------------------+
| ViewModel Layer - e.g., BikeViewModel |
| - Holds UI State (BikeUiState) |
| - Calls UseCases in response to events |
+---------------------------------▲--------------------------------+
│ (Results)
│
▼ (Calls)
+------------------------------------------------------------------+
| Domain Layer (UseCases) - e.g., RideStatsUseCase |
| - Contains business logic |
| - Orchestrates data from Repositories |
+---------------------------------▲--------------------------------+
│ (Data Models)
│
▼ (Requests)
+------------------------------------------------------------------+
| Data Layer (Repositories) - e.g., WeatherRepo, BikeRideRepo |
| - Provides a single source of truth for data |
| - Abstracts away data sources (API vs. DB) |
+---------------------------------▲--------------------------------+
│
▼
+------------------------+ +----------------------------------+
| External API (Weather)| | Local Database (BikeRideDB) |
+------------------------+ +----------------------------------+In essence, the BasePro architecture is a powerful and flexible framework that enables the rapid development of complex, interconnected applications. By modularizing advanced features like NFC, BLE, and Maps, it creates a "plug-and-play" ecosystem where developers can build sophisticated user experiences on a stable and scalable foundation.
Architectural Blueprint of the BasePro Project
This section establishes the high-level architectural framework of the BasePro project, providing the foundational context for the subsequent deep dive into the AshBike application. The analysis reveals a sophisticated, multi-layered architecture designed for scalability, maintainability, and the development of multiple, distinct application products from a unified codebase.
A Multi-Module Monorepo Strategy
The project is structured as a multi-module monorepo, an advanced architectural approach that supports multiple, distinct application targets from a single codebase. This strategic decision is immediately evident from the project’s settings.gradle.kts file, which includes not only a generic :app module but also a dedicated applications directory containing discrete application modules such as
- :applications:ashbike,
- :applications:home
- :applications:photodo
The directory structure, segregated into applications/, feature/, and core/ layers, strongly indicates an architecture designed to build a suite of related applications — a “super-app” framework — rather than a single, monolithic product. The modules within the applications directory serve as the final, shippable products. This is confirmed by the build.gradle.kts file for applications/ashbike, which applies the com.android.application plugin, designating it as a buildable, standalone Android application, not merely a library module
This structure enables the development team to build and ship distinct applications like AshBike and PhotoDo as separate artifacts while leveraging a shared foundation of feature and core modules. This represents a deliberate and powerful strategy for maximizing code reuse and ensuring consistent development practices across a family of products. The feature modules encapsulate vertical slices of functionality, while the core modules provide a library of foundational services, data models, and UI components. This modularity is a hallmark of a mature and scalable monorepo strategy, designed for long-term maintainability and efficient parallel development.
The following table provides a high-level overview of the project’s modular structure, categorizing each module according to its role within the architecture.
Modern Android Development (MAD) Principles
The entire codebase demonstrates a rigorous and consistent application of Modern Android Development (MAD) best practices, which form the architectural bedrock of the system. This commitment ensures the project is robust, testable, and aligned with current industry standards.
Unidirectional Data Flow (UDF)
The presentation layer consistently employs a UDF pattern, where data flows in one direction and events flow in the opposite. UI components, implemented as @Composable functions, observe state exposed by ViewModels and communicate user actions back to the ViewModel via a well-defined set of events.
This pattern is clearly exemplified in the interaction between
- BikeUiRoute, which observes state from the BikeViewModel , and the corresponding BikeUiState
- BikeEvent sealed classes that define the state and events contract. This strict separation makes the UI predictable and easier to debug.
Dependency Injection (DI) with Hilt
Hilt is used pervasively throughout the project to manage dependencies, promoting loose coupling and enhancing testability. Key Hilt annotations are found in every layer of the architecture. The @AndroidEntryPoint annotation on MainActivity signals that Hilt should inject dependencies into this activity.
ViewModels are annotated with @HiltViewModel (e.g., BikeViewModel ), allowing them to receive dependencies from the Hilt graph. Finally,
@Inject is used in the constructors of repositories, use cases, and other classes to declare their dependencies, which Hilt then provides at runtime.
Jetpack Compose for UI
The user interface is built entirely with Jetpack Compose, a modern, declarative UI toolkit. This approach enables the creation of state-driven and dynamic user interfaces with less boilerplate code compared to traditional XML-based layouts. The use of @Composable functions is ubiquitous, and the application’s UI is bootstrapped within the setContent block in MainActivity, which is the standard entry point for a Compose-based application.
The AshBike Application
A Comprehensive Analysis
This section provides a granular, flow-based analysis of the AshBike application. The examination traces the user journey and data flow from the initial application launch through the core experience of tracking a live ride to the final stages of data persistence and synchronization with external services like Google Health Connect.
Application Entry and Navigation Flow
The entry and navigation architecture of the AshBike application is well-defined, following standard Android and Jetpack Compose conventions to create a clear and robust user flow.
Application Entry Point
The AndroidManifest.xml for the ashbike application module explicitly declares com.ylabz.basepro.applications.bike.MainActivity as the primary entry point and launcher activity for the application.1 This is the first component instantiated when the user launches the app.
Initial Setup in MainActivity
The MainActivity 1 serves as the application’s bootstrap. Annotated with
@AndroidEntryPoint, it leverages Hilt for dependency injection from the moment of its creation. Its primary responsibilities include:
- Injecting the SettingsViewModel to observe the user’s selected theme and applying it globally via the AshBikeTheme composable. This ensures a consistent look and feel across the entire application.
- Handling runtime permissions. The MainScreen composable, which is called from the activity, requests essential location permissions (ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) upon launch, ensuring the app has the necessary access for its core functionality.
- Setting the root composable of the UI via the setContent block. This block hosts the RootNavGraph, which is the top-level container for the application’s navigation.
Navigation Architecture
The navigation is structured hierarchically, using Jetpack Navigation Compose to manage the flow between different screens.
- The RootNavGraph acts as the highest-level navigation host. Its primary role is to delegate the main UI structure to the
MainScreen composable. - MainScreen is the core of the user-facing navigation experience. It sets up a Scaffold, which provides a standard Material Design layout structure, including a HomeBottomBar for tab-based navigation and a
NavHost for the main content area. - The NavHost is configured with the routes defined in the bikeNavGraph function. This function encapsulates the navigation logic for the primary features of the AshBike application. The start destination is set to BikeScreen.HomeBikeScreen.route, ensuring the user lands on the main dashboard. The graph defines the composable destinations for the main tabs:
- Home screen (BikeUiRoute)
- Trips list (TripsUIRoute)
- Settings screen (SettingsUiRoute)
- RideDetailScreen for viewing historical ride data
Dashboard, Service, and State Management
The live ride tracking feature is the centerpiece of the AshBike application. Its implementation showcases a sophisticated and resilient architecture that separates UI concerns, long-running background tasks, and state management logic into distinct, well-defined components.
The Dashboard UI (BikeUiRoute)
The main ride screen is implemented in the BikeUiRoute composable.1 This function serves as the state-aware container for the dashboard UI. It observes the BikeUiState StateFlow exposed by the BikeViewModel. Based on the current state, it conditionally renders the appropriate UI:
- BikeUiState.WaitingForGps: Displays a WaitingForGpsScreen, informing the user that the application is acquiring a satellite fix.
- BikeUiState.Success: Renders the main BikeDashboardContent, which contains the speedometer, real-time metrics, and ride controls.
- BikeUiState.Error: Shows an ErrorScreen with a message describing the issue.
This pattern ensures that the UI is always a direct reflection of the application’s current state.
The Foreground Service (BikeForegroundService)
The core logic for tracking a live ride is encapsulated within a BikeForegroundService, not directly in the BikeViewModel. This is a critical and well-executed architectural decision. It ensures that ride tracking can continue reliably and without interruption, even if the user backgrounds the application or the Android system destroys the UI activity due to memory pressure.
The service is managed as a foreground service, which gives it a higher priority in the system and displays a persistent notification to the user, making them aware of the active tracking session. The service itself uses a FusedLocationProviderClient to request high-accuracy location updates. The incoming data is processed within the onLocationResult callback of a LocationCallback object. The service is designed as a bound service, extending LifecycleService and providing an IBinder via its LocalBinder inner class. This allows clients, such as the BikeViewModel, to bind to the service and communicate with it directly, accessing its public properties and methods to observe ride data and send commands.
State Management (BikeViewModel)
The BikeViewModel functions as the essential mediator between the
BikeForegroundService and the UI layer. It does not contain the ride tracking logic itself but orchestrates the interaction between the UI and the service.
- It exposes the uiState: StateFlow<BikeUiState>, which the
BikeUiRoute observes for rendering. - Using the standard “Bound Service” pattern, the ViewModel contains bindToService and unbindFromService methods, along with a ServiceConnection object. When the ViewModel’s lifecycle starts, it binds to the BikeForegroundService.
- Once bound, the ViewModel collects the rideInfo StateFlow from the service instance. It then transforms this raw data from the service into the appropriate BikeUiState (e.g., BikeUiState.Success) that the UI can consume.
- It handles all user interactions through a centralized onEvent(event: BikeEvent) function. This function translates UI-level events, such as a user tapping the “Start Ride” button (BikeEvent.StartRide), into specific commands that are then sent to the BikeForegroundService to control the ride tracking state.
The Logic Core: Use Case Layer
The application’s business logic is further decomposed into a dedicated use case layer, a hallmark of a highly mature and testable architecture. This design choice effectively separates what the application does (the business rules encapsulated in a use case) from how it does it (the implementation details within a repository or service). This creates a clean, modular, and platform-agnostic core logic that is easy to understand and reuse.
RideStatsUseCase
This use case is designed as a pure, stateless computation engine. Its sole responsibility is to transform raw data streams — such as locationFlow and speedFlow — into derived metric streams like total distance, maximum speed, and elevation gain. A key feature of its design is the resetSignal: Flow<Unit> parameter in its functions. This allows the use case’s calculations to be controlled externally, making it highly reusable and straightforward to unit test in isolation without any stateful dependencies.
RideSessionUseCase
In contrast to the stateless RideStatsUseCase, the RideSessionUseCase is a stateful orchestrator. It manages the entire lifecycle of a ride session, including handling start, pause, and stop events. It injects and coordinates the various data repositories (UnifiedLocationRepository, CompassRepository) and the RideStatsUseCase. By driving the resetSignal that is passed to the RideStatsUseCase, it controls when the statistical calculations begin or reset. Its purpose is to combine all the underlying data sources and computations to produce a complete, real-time RideSession object that represents the current state of the ride.
This two-tiered use case structure perfectly embodies the Single Responsibility Principle. The RideStatsUseCase is responsible for the pure mathematics of metric calculation, while the RideSessionUseCase is responsible for the stateful orchestration of the ride’s lifecycle. The BikeForegroundService injects the RideSessionUseCase, which in turn injects the RideStatsUseCase, creating a clear and logical dependency chain from the highest level of orchestration down to the lowest level of computation.
Data Persistence and Synchronization
The application employs a robust strategy for data persistence, using a dedicated local database for immediate storage and a well-defined synchronization mechanism for integrating with external health platforms.
Local Database (ashbike:database)
The AshBike application utilizes a dedicated Room database module, applications:ashbike:database, to store all ride-related data locally. This ensures that user data is preserved even when the app is closed and is available for offline access. The database, defined in BikeRideDB.kt, is structured around two primary entities:
- BikeRideEntity
package com.ylabz.basepro.applications.bike.database
@Entity(tableName = "bike_rides_table")
data class BikeRideEntity(- RideLocationEntity
@Entity(
tableName = "ride_locations",
foreignKeys = [
ForeignKey(
entity = BikeRideEntity::class,
parentColumns = ["rideId"],
childColumns = ["rideId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [ Index("rideId") ]
)
data class RideLocationEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val rideId: String, // ← FK → BikeRideEntity.rideId
val timestamp: Long, // epoch millis
val lat: Double,
val lng: Double,
val elevation: Float? = null
)The RideLocationEntity stores the individual GPS points for each ride. This entity is linked to BikeRideEntity via a foreign key on the rideId column. The relationship is defined with an onDelete = CASCADE constraint, which ensures that when a ride summary is deleted, all of its associated location points are also automatically removed, maintaining data integrity.
The BikeRideDao provides the data access interface, intelligently using the @Transaction annotation for queries that fetch a ride along with its locations, guaranteeing that the operation is atomic and the data is consistent.
Health Connect Synchronization
The feature/trips module is responsible for orchestrating the synchronization of completed rides to Google Health Connect. The process is initiated from the UI, where the TripsViewModel responds to a
SyncRide event. This event triggers a call to the SyncRideUseCase, which is responsible for transforming the application’s internal BikeRide domain model into a list of Health Connect Record objects, such as ExerciseSessionRecord, DistanceRecord, and TotalCaloriesBurnedRecord.
The actual writing of data to Health Connect is handled by the HealthViewModel, which is observed by the TripsUIRoute. The
TripsViewModel emits a TripsSideEffect.RequestHealthConnectSync, which contains the prepared list of records. The TripsUIRoute observes this side effect and triggers the appropriate write action on the HealthViewModel. This demonstrates an excellent example of decoupled communication between two feature ViewModels, mediated by the UI layer, which prevents direct dependencies between feature logic.
Historical Data
The Trips Feature
The feature/trips module provides the user interface for viewing and managing a list of past rides, demonstrating a clean implementation of the master-detail pattern.
- The TripsViewModel injects the BikeRideRepo and uses it to fetch a Flow of all rides with their locations. It then maps these database entities to a UI-specific model, BikeRideUiModel. This model contains pre-formatted, display-ready strings for properties like date, duration, and distance, which simplifies the rendering logic in the UI.
- The TripsUIRoute is the main composable for this feature. It displays the list of rides using the BikeTripsCompose composable, which in turn renders a LazyColumn of BikeRideCard components, one for each past ride.
- When a user selects a ride from the list, the app navigates to the RideDetailScreen. The rideId of the selected ride is passed as a navigation argument. The RideDetailViewModel for this screen then uses this rideId to fetch the specific ride’s full data, including the complete location path for map rendering, from the BikeRideRepo.
Foundational Layers
The Core Modules
The core modules serve as the foundational bedrock of the BasePro project, providing shared logic, data models, and resources that are consumed by the various feature and application modules. This layered approach is fundamental to the project’s scalability and maintainability.
core:data
The Central Data Hub
This module is the heart of the application’s data layer, providing a centralized and abstracted interface to all external and internal data sources.
Networking
The module is designed with the flexibility to handle multiple backend service types. It includes configurations for both RESTful and GraphQL APIs. The YelpClient demonstrates the integration of a GraphQL API using an ApolloClient, with the corresponding schema defined in shema.json. Concurrently, weather data is fetched from a RESTful service via Retrofit, as defined in OpenWeatherService.kt. This dual capability allows the application to seamlessly integrate with diverse backend architectures.
Sensor Repositories
core:data provides high-level, abstracted repositories for interacting with device sensors. Key examples include the CompassRepository for orientation data and the UnifiedLocationRepository for location data. The use of Hilt qualifiers, such as @LowPower and @HighPower within the UnifiedLocationModule, is a particularly powerful pattern.
It allows the application to inject different implementations of the
UnifiedLocationRepository based on the specific power and accuracy requirements of a given feature. For instance, a live ride tracking feature can request the @HighPower implementation, while a passive location-aware feature might use the @LowPower version to conserve battery.
Health Connect Abstraction
The HealthSessionManager provides a clean, high-level API for all interactions with the Health Connect SDK. It encapsulates the complexities of checking for SDK availability, managing user permissions, and aggregating disparate health records into meaningful session data. This abstraction shields the feature modules from the low-level details of the Health Connect client library.
core:model
The Canonical Data Models
This module is dedicated to defining the Plain Old Kotlin Objects (POKOs) that represent the primary business objects of the application. These include canonical models like BikeRide, the Yelp BusinessInfo, and SleepSessionData.
By isolating these data models in a lightweight, platform-agnostic module with minimal dependencies, they can be shared across all layers of the architecture — from the data repositories to the ViewModels and even across different application targets like the wearos features. This prevents the propagation of platform-specific dependencies and ensures a consistent data language throughout the entire codebase.
core:ui & core:util
Shared Resources
These modules provide shared, cross-cutting resources and utilities that support the entire application suite.
core:ui
centralizes shared UI elements and theming. Its most critical component is
AshBikeTheme.kt, which defines the application’s MaterialTheme. This includes custom color schemes defined in Color.kt and standardized typography from Type.kt. This centralization ensures a consistent and polished visual identity across all features and application modules.
core:util
provides common utility functions. A key example is the Logging object, which offers a centralized, tag-based, and level-configurable logging mechanism. This allows developers to enable or disable logs globally or on a per-class basis, streamlining the debugging process.
Architectural Strengths
Exceptional Modularity
The project’s multi-module architecture is its most significant strength. The clear separation into application, feature, and core layers enables parallel development, facilitates faster build times through Gradle’s parallel execution and caching, and establishes clear ownership boundaries for different parts of the codebase.
Strict Separation of Concerns
The consistent application of a clean architecture — with distinct UI, ViewModel, Use Case, and Repository layers — results in a codebase that is highly testable, maintainable, and easy to reason about. Each component has a single, well-defined responsibility.
Robust Background Processing
The decision to use a ForegroundService for live ride tracking is a best-practice implementation.1 It correctly separates the long-running, critical task of location tracking from the ephemeral UI, ensuring data integrity and a reliable user experience even when the app is not in the foreground.
Scalability for a Multi-Platform Future
The architecture is explicitly designed for growth. The abstraction of data models and business logic into core and feature modules makes it straightforward to support new application targets and platforms, such as the already-included wearos features, with maximum code reuse.
Conclusion
The modular design allows BasePro to seamlessly integrate a wide range of advanced hardware and software capabilities:
- NFC & Bluetooth Low Energy (BLE)
- Advanced Maps & Location Services
- Real-time GPS tracking of cycling routes.
- Calculation of key ride statistics like distance, speed, and elevation gain.
- Displaying saved ride paths and elevation profiles on a map for post-ride analysis.
- It interfaces directly with Google Health Connect to read and write exercise data. Completed rides from
AshBikecan be seamlessly synced as exercise sessions, contributing to a user's unified health data. - Local Persistence: Utilizes a dedicated Room Database (
BikeRideDB) to store all ride data locally, ensuring offline access and data integrity. - Weather Service Integration: Includes a
WeatherRepocapable of fetching weather data from an external API, allowing apps to provide contextual environmental information.
The BasePro project stands as an exemplary model of modern Android architecture. It is more than just a codebase; it is a strategic blueprint for building a suite of scalable, maintainable, and feature-rich applications. The project’s true strength lies in its disciplined, multi-layered structure, which rigorously separates core functionalities, encapsulated features, and the final application products. This approach not only fosters immense code reusability but also empowers development teams to work in parallel with greater efficiency and clarity.
The AshBike application serves as a powerful testament to the architecture’s success. It seamlessly integrates a host of complex capabilities — from real-time GPS tracking and weather service integration to sophisticated BLE and Health Connect communications — without compromising performance or stability. Each feature is cleanly implemented within its own module, demonstrating the practical power of the project’s design principles.
Ultimately, the BasePro architecture is engineered for the future. Its modularity ensures that it can evolve with emerging technologies and business needs, allowing for the effortless addition of new features or even entirely new applications with minimal friction. It represents a durable and future-proof asset, providing a robust foundation for any ambitious mobile development initiative.
~Ash
