Android application: Difference between revisions

From Aquarium-Control
Jump to navigation Jump to search
Line 92: Line 92:
└── build.gradle                  // Plugins, setting, dependencies
└── build.gradle                  // Plugins, setting, dependencies
</code>
</code>
== Example workflow for adding API communication ==
=== Implementation ===
Create the model: RefillState as enum
Add a reader class RefillStateReader which uses the injected interface (Retrofit)
Update the RefillStateApiService: Add a function to load the state via Retrofit
Add the label for the JSON property in WebApiParams
Add the URL for the end point in WebApiUrls
Add a wrapper RefillStateResponse for the model to allow Retrofit parsing the JSON
Update the repository interface and the repository interface implementation:
* Add a flow that delivers a DataFetchResult of the RefillState
* Add a function that allows the view model to trigger a refresh
Add a wrapper class RefillStateUiState
Update the view model
* Add a flow that delivers a RefillStateUiState based on the DataFetchResult from repository
* Handle the new UI event for refreshing
Add composable RefillStateComposable for displaying the RefillState
Update RefillUiEvent with an object for the refresh
Update RefillScreen:
* additional parameter (RefillStateUiState) in composable function and in previews
* Added PullToRefreshBox
* Added parsing of RefillUiState and display of either
    * RefillStateComposable in case of success
    * Text-based error message in case of error
    * CircularProgressIndicator in case of loading
Inject RefillUiState into RefillScreen in the MainActivity and RefillScreenTest as well as in previews of RefillScreen
(Optional) Check if RefillDataModule needs update (not required in this case)
=== Testing ===
Update existing screenshot testing
Add screenshot testing for new composable


== Test strategy ==
== Test strategy ==

Revision as of 09:41, 18 April 2026

The app is available in the Play store.

Requirements

Architecture

The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.

Module structure

Besides the main app module, the app consists of the following library modules:

  • balling
  • common
  • controller
  • feed
  • heating
  • info
  • overview
  • refill
  • schedule
  • timedata
  • ventilation

The main app module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity). The activity contains the navigation NavHost in onCreate.

None of the other modules has a dependency to the main app module.

The common module provides low-level functionalities and layout elements used in various places. The following functionalities are located in common module:

  • DataFetchResult: A wrapper class indicating the status of data fetching operation (Success, Error, Loading, Empty).
  • WebApiParams, WebApiPostRequest, WebApiPostRequestExecution, WebApiUrls: Classes and objects used for communication from the app to the server.
  • ControllerProfileDao, ControllerProfileDatabase: Classes which provide Room database functionality.
  • Various classes used for dependency injection (Hilt)
  • SetValsSanityCheckResult: Class used for to communicate status of set values for heating and ventilation module.
  • ControllerProfile: Class containing the profile data of the server (address, port, credentials, ...)
  • GlobalConstants: Mainly UI-related strings (non-context-related) and some minor functional constants
  • Composable functions for the main drop down menu
  • Composable functions for the theme
  • Composable functions for hyperlinks, text edit fields, headline
  • ViewNavigationRoutes: Object containing the routing information processed in main app module
  • ScreenshotTestHelper: Helper class for executing the instrumented snapshot testing using the emulator.

Layer structure

The code is separated into the following layers:

[module]/

├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/

│ ├── remote/ // Folder for data retrieval functionality

│ │ └── dto/ // Folder for Data Transfer Objects

│ │ └── [module][name]Import.kt

│ │ └── [module][name]Reader.kt // Implementation of GET server request

│ ├── [module][name]RepositoryImpl.kt // Repositories (implementation of interface)

├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/

│ ├── model/ // Folder for data definition classes

│ │ └── [module][name].kt. // Classes for data definition

│ ├── repository/ // Folder containing repository interface definitions

│ │ └── [module][name]Repository.kt. // Interface defining the repository, used by view model

├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/

│ ├── entry/ // First screen of feature

│ ├── [components]/ // Shared code between different screens

│ ├── [screen name]/ // Folder for each screen

│ │ └── [module][name]Screen.kt // Composable showing the complete screen

│ │ └── [module][name]Composable.kt // Composable showing elements inside the screen

│ │ └── [module][name]UiEvent.kt // Sealed interface describing the communication from the composable to the view model

│ │ └── [module][name]UiState.kt // Data class describing the content to be rendered by the composable

│ │ └── [module][name]ViewModel.kt // Class implementing the view model

│ ├── navigation/ // Folder containing class and functionality related to screen navigation

│ │ └── [module]NavigationEvent.kt // One sealed class per module containing the navigation intent of the user

│ │ └── [module]Navigator.kt // Module-specific navigation functionality

│ └── build.gradle // Plugins, setting, dependencies

Example workflow for adding API communication

Implementation

Create the model: RefillState as enum Add a reader class RefillStateReader which uses the injected interface (Retrofit) Update the RefillStateApiService: Add a function to load the state via Retrofit Add the label for the JSON property in WebApiParams Add the URL for the end point in WebApiUrls Add a wrapper RefillStateResponse for the model to allow Retrofit parsing the JSON Update the repository interface and the repository interface implementation:

  • Add a flow that delivers a DataFetchResult of the RefillState
  • Add a function that allows the view model to trigger a refresh

Add a wrapper class RefillStateUiState Update the view model

  • Add a flow that delivers a RefillStateUiState based on the DataFetchResult from repository
  • Handle the new UI event for refreshing

Add composable RefillStateComposable for displaying the RefillState Update RefillUiEvent with an object for the refresh Update RefillScreen:

  • additional parameter (RefillStateUiState) in composable function and in previews
  • Added PullToRefreshBox
  • Added parsing of RefillUiState and display of either
   * RefillStateComposable in case of success
   * Text-based error message in case of error
   * CircularProgressIndicator in case of loading

Inject RefillUiState into RefillScreen in the MainActivity and RefillScreenTest as well as in previews of RefillScreen (Optional) Check if RefillDataModule needs update (not required in this case)

Testing

Update existing screenshot testing Add screenshot testing for new composable

Test strategy

The app uses different testing approaches:

  • Compose preview screenshot testing
  • Screenshot tests running as instrumented tests on emulator
  • Unit tests

Compose preview screenshot tests

These tests run comparably fast and device-independent.

The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).

Limitations:

  • Two tests within feed module can only run in landscape mode (FeedProfileScheduleScreenTest, FeedHistoryScreenTest) due to excessive heap memory utilisation.
  • Info screen is not tested due to LibrariesContainer (dynamic content which cannot be mocked).

The tests are located in [module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/.

The references are located in [module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/.

The test reports are located in build/reports/screenshotTest/preview/debug/. The test reports for the compose screenshot tests contain reference, actual and delta images.

The images are generated with ./gradlew [module]:updateDebugScreenshotTest.

The images are validated with ./gradlew [module]:validateDebugScreenshotTest.

Omitting the module will execute all tests.

Instrumented tests

The tests run comparably long and are bound to an emulator device. As of November 2025, a Pixel 5 API S is used for the instrumented test. The instrumented tests only test the complete screens.

Limitations:

  • Info screen is not tested due to LibrariesContainer (dynamic content which cannot be mocked and also does not allow execution of instrumented test).

The test reports are located in build/reports/androidTests/connected/debug/. The test reports for the compose screenshot tests do not contain any images.

The tests are executed with ./gradlew [module]:connectedDebugAndroidTest. Omitting the module will execute all tests.

A switch called recordMode inside each test class determines if the image is generated (true) or if the image is validated (false). The images are generated on the device (/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/) and need to be downloaded to the repository.

Unit tests

As of November 2025, there are no unit tests.

Release procedure

  1. Update versionCode and versionName in build.gradle.kts of app module.
  2. Create a release branch (release/[version]), checkout, commit and push.
  3. Change to release build variant.
  4. Build
  5. Generate a signed App bundle
  6. Upload to play store