Pro Modern Android Dev (MAD)

This doc tries to manage the complexity of a MAD*

Siamak (Ash) Ashrafi
18 min readJan 13, 2024
Building Android App
Modern Android Development (MAD) in the Wild

*Modern Android Development (MAD)

Welcome to Modern Android Development (MAD) Skills, a series of videos and articles that teach you how to use the latest technologies to build better apps, more quickly and easily.

YLabZ Reference Manual for MAD Architecture.

At YLabZ, we craft exceptional Android apps that embody innovation, fluidity, and user delight. Our meticulous approach to app development hinges on a robust foundation of cutting-edge technologies and architectural patterns. This commitment empowers us to deliver apps that seamlessly blend visual appeal, intuitive interactions, and robust performance.

Read on to discover the key components we meticulously weave together to create modern mobile experiences that surpass expectations.

This is building on our previous article:

Modern Android development, or MAD, isn’t just about writing code; it’s about crafting exceptional experiences. It’s about harnessing the power of the latest tools and best practices to build apps that are not only visually stunning but also effortlessly smooth and performant.

At YLabZ, we embrace the MAD philosophy wholeheartedly. We meticulously select and blend essential components to weave together mobile experiences that surpass expectations. In this document, we’ll unveil the key ingredients in our MAD recipe, showcasing the technologies and patterns that empower us to deliver:

  • Intuitive Interfaces: Jetpack Compose and Material 3 work in harmony to create beautiful, user-friendly UIs that feel like natural extensions of your fingertips.
  • Reactive Performance: Kotlin Coroutines and Flow unleash the power of concurrency and reactivity, keeping your apps responsive and efficient, even under heavy loads.
  • Seamless Data Management: From robust networking with Ktor to offline persistence with Room, we ensure your app has access to the data it needs, whenever and wherever it needs it.
  • Modular Architecture: We leverage the Clean Architecture and MVVM patterns to decouple your app’s logic, fostering scalability and maintainability.
  • Modern Tooling: From automated dependency management with TOML Version Catalog to CI/CD with GitHub Actions, we leverage cutting-edge tools to streamline the development process and keep your app humming along.

In essence, MAD development is about creating apps that are a joy to use. Apps that anticipate your needs, adapt to your context, and keep you engaged — seamlessly and effortlessly. Dive deeper into the following sections to discover the specific technologies and patterns that power our MAD approach.

Naming the App.

Important Info About Naming Apps for the Apple Play Store

Getting the URL/Domain name is also very important but at this date&age we need to use Generative AI (ChatGPT/Bard) to help find an undiscovered name (different languages / play on words)

Example:

Photo To-Do = PhoToDo

  • After we started using it … many other copied the name.
  • Use this name years ago on Android but now we can’t get it on iOS :-(
  • We never wanted the URL — But now we should have reserved it anyway!

Video:

PhoToDo app with MAD architecture using AI/ML to generate task details.

AI/ML powered Todo App

Create Project

When generating the project we use Android Studio Jellyfish Canary 3.

Create a new Jetpack Compose MAD Application

Use the Empty Compose Activity

Use the build.gradel.kts this will build the libs.versions.toml TOML file.

libs.versions.toml TOML file for version control

Android TOML Configurations

Streamlining Android Build Management with TOML Files

Harnessing the Power of TOML for Centralized Version Handling

Using Android Studio to build a project it will create a TOML file. An Android build TOML file is a configuration file used to manage dependency versions and plugins for Android projects. It’s a convenient way to centralize version definitions and avoid repeating them across multiple build files.

In the world of Android development, ensuring consistency and avoiding version conflicts across multiple build files can be a challenge. Enter the Android build TOML file, a powerful configuration tool designed to streamline this process.

What is a TOML File?

  • TOML (Tom’s Obvious, Minimal Language) is a lightweight, human-readable format for configuration files.
  • Its simple structure and clear syntax make it easy to understand and maintain.

Key Advantages of Using an Android Build TOML File:

  • Centralized Version Management: Define dependency and plugin versions in a single file, eliminating the need for repetitive declarations in multiple build files.
  • Dependency Aliases: Create clear and concise aliases for lengthy dependency coordinates, simplifying references and reducing error-prone typos.
  • Dependency Bundles: Group related dependencies into logical bundles, promoting reusability and easier management.
  • Consistent Plugin Usage: Define plugin versions centrally, ensuring consistent application across different modules.

Structure

1. [versions] Section:

  • Stores variables for dependency and plugin versions.
  • Defines variables that hold the versions of your dependencies and plugins.

2. [libraries] Section:

  • Declares aliases for dependency coordinates, simplifying dependency references in build files.

3. [bundles] Section:

  • Defines bundles of related dependencies.
  • Groups related dependencies together as bundles, making it easier to manage and reuse them.

4. [plugins] Section:

  • Holds variables for plugin versions.
  • Defines variables for plugin versions, allowing consistent plugin usage across modules.
[versions]
kotlin = "1.7.20"
androidx_core = "1.8.0"
[libraries]
kotlin_stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
[bundles]
ui_dependencies = [
"androidx.appcompat:appcompat:1.4.2",
"com.google.android.material:material:1.6.1",
]
[plugins]
android_gradle = "7.2.1"

By embracing the benefits of TOML files, Android developers can achieve a more organized, efficient, and maintainable approach to dependency and plugin management.

MAD Android Arch

Multi-module sample code from Google Android Team

We use this as our template (file structure / features) for MAD Android Development:

These samples showcase different architectural approaches to developing Android apps. In its different branches you’ll find the same app (a TODO app) implemented with small differences.

In this branch you’ll find:

  • User Interface built with Jetpack Compose
  • A single-activity architecture, using Navigation Compose.
  • A presentation layer that contains a Compose screen (View) and a ViewModel per screen (or feature).
  • Reactive UIs using Flow and coroutines for asynchronous operations.
  • A data layer with a repository and two data sources (local using Room and a fake remote).
  • Two product flavors, mock and prod, to ease development and testing.
  • A collection of unit, integration and e2e tests, including “shared” tests that can be run on emulator/device.
  • Dependency injection using Hilt.

YLabZ App Development Features

In the realm of modern mobile development, a carefully curated symphony of technologies and practices orchestrates the creation of exceptional apps. Each component plays a vital role in shaping an experience that’s not only visually captivating but also effortlessly fluid, performant, and secure.

Join us as we unveil the essential ingredients that empower us to craft apps that stand at the forefront of innovation:

  • Gradle modularised project by features (UseCases when appropriate)
  • The Clean Architecture with MVVM pattern in presentation layer
  • Jetpack Compose / 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
  • TOML 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

References

We are using the article below as a guide.

File Structure

Knowing where to put what and why it’s there is the bases of a well designed mobile app …

Purpose of a Well-Structured File System:

  • Organization: Provides a clear framework for storing and managing code components, enhancing maintainability and scalability.
  • Modularity: Enables independent development and testing of features, reducing code complexity and promoting code reuse.
  • Flexibility: Facilitates updates and expansions without disrupting other parts of the project.
  • Separation of Concerns: Enforces clear boundaries between different areas of functionality, improving code readability and understanding.

File Structure Module Breakdown

App Modules

  • :app:mobile: Contains code specific to the mobile phone app.
  • :app:wear: Contains code specific to the wearable app, tailored for wearable devices.

Build Logic

  • :build:logic:convention: Houses conventions and plugins that streamline build configurations and automate tasks.

Core Libraries

  • :core:testing: Provides reusable testing utilities for efficient quality control.
  • :core:ui: Offers a collection of common Jetpack Compose UI widgets, promoting consistency and avoiding code duplication.
  • :core:util: Encapsulates general-purpose utility functions that aren't tied to Android-specific features.

Data Layer

  • :data: Manages data access and handling, encapsulating data-related logic and interactions with data sources.

Dynamic Delivery

  • :dynamic: Enables features to be delivered on demand, reducing initial app size and enhancing user experience.

Feature Modules

  • :feature:details: Encapsulates the details feature, likely responsible for displaying detailed information about specific items.
  • :feature:list: Encapsulates the list feature, likely responsible for presenting lists of items or data.
  • :feature:wear:home: Contains the home screen feature for the wearable app, specifically designed for wearable devices.

Testing Module

  • :test:navigation: Hosts navigation-related tests, ensuring smooth navigation between app screens and features.

Key Considerations

  • Granularity: Choose the appropriate level of granularity for modules based on feature complexity and dependencies. Over the development cycle start with fewer modules and expand as needed.
  • Naming Conventions: Use consistent and descriptive naming to enhance code clarity and navigation. Please see our naming documentation and only deviate under extreme conditions.
  • Dependency Management: Carefully manage dependencies between modules to avoid circular references and maintain modularity.
  • Best Practices: Adhere to recommended file structure patterns and guidelines to ensure project maintainability and scalability.

Adding Modules (Features)

In the Feature dir make a new module using Android Studio

Add a new module to the list of module features

Build the new module remembering to set the correct package name.

We now have our features (cat, list, next, photodo) in modules.

Every feature has a unique directory with it’s own build environment

Understanding State-Driven Interactions

Key Concepts:

  • State-Driven UI: The UI’s appearance and behavior are dynamically controlled by the application’s state, ensuring a consistent and reactive user experience.
  • Events: User interactions and system actions trigger events, which signal the need for state updates.
  • View Model: Acts as a mediator between the UI and app logic, managing state and events to ensure a seamless flow.

Files:

  • <Feature>UIState.kt — The data for the composable
  • <Feature>Event.kt — All events the UI can generate
  • <Feature>Route.kt — Compose UI
  • <Feature>ViewModel — Connects the UI to the data.

State Representation

<Feature>UiState.kt — State

Contains the data that is passed to the UI

sealed interface FeatureUiState {
object Loading : FeatureUiState
object Error : FeatureUiState
data class Success(
val data: List<Photodo> = emptyList(),
val audioFiles: List<String> = emptyList(),
val photoFiles: List<String> = emptyList()
) : FeatureUiState
// Else it is a failure ???
}

A sealed interface defining possible states:

  • Loading: Indicates a pending operation.
  • Error: Signals an error condition.
  • Success: Holds current task data (title, description, photo path).

The UI is set not by a sequence of events, but by setting the state!

Events Orchestration

<Feature>Event.kt — Events

Sealed class that is all the possible events the composable can handle.

sealed class AddPhotodoEvent {
object PhoEvStart : AddPhotodoEvent()
//data class PhoEvent(val value:Int) : AddPhotodoEvent()
object SavePhotodo : AddPhotodoEvent() // the save happens in the viewmodel by a usecase
object SavePhoto : AddPhotodoEvent() // the save happens in the viewmodel by a usecase
data class EnteredTitle(val value: String) : AddPhotodoEvent()
data class EnteredDescription(val value: String) : AddPhotodoEvent()
data class ChangeTitleFocus(val focusState: FocusState) : AddPhotodoEvent()
data class TakePhoto(val fromFile: Uri?): AddPhotodoEvent()
}

A sealed class encapsulating various events:

  • SavePhotodo: Triggers a save operation.
  • SavePhoto: Saves a captured photo.
  • EnteredTitle, EnteredDescription: Updates title or description fields.
  • ChangeTitleFocus: Manages title field focus.
  • TakePhoto: Handles photo capture.

Events change the state.

State-Driven UI Updates

UI Elements: Observe state changes and render accordingly.

The change in state update the UI

if (photoState.photoPath.path.isNullOrEmpty()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current
.data(photoState.photoPath)
...
)
} else {
Image(
Place holder image ...
)
}

The UI tracks the changes by using

val state by viewModel.uiState.collectAsStateWithLifecycle()

Examples:

  • AsyncImage: Displays a photo if a path exists, otherwise a placeholder.
  • Image: Shows a placeholder image.
  • State Tracking: val state by viewModel.uiState.collectAsStateWithLifecycle() continuously monitors state updates.

Lifecycle Awareness:

  • collectAsStateWithLifecycle() ensures efficient resource management and state handling aligned with the UI lifecycle.

Reactive Composable UI

Connecting the UI, State and Events together.

The structure of a Composable UI file

This Composable is the entry point to our UI and has 3 stages:

  1. Route — get the ViewModel & Event Lambda and pass it down to the screen.
@Composable
fun AddPhotodoRoute(
modifier: Modifier = Modifier,
paddingValues: PaddingValues,
navHostCont: NavHostController,
addPhotoVM: AddPhotodoViewModel = hiltViewModel(),
) {
val onEvent = addPhotoVM::onEvent
val photoState by addPhotoVM.uiState.collectAsStateWithLifecycle()
val photoEvent by addPhotoVM.eventFlow.collectAsStateWithLifecycle(AddPhotodoEvent.PhoEvStart)
val alertML by addPhotoVM.showAlertEvent.collectAsState()
AddPhotodoScreen( ...

*Hilt provides the ViewModel to the Composable using DI

2. Screen — show loading, success or error and pass the success to the content

when (featureUiState) {
FeatureUiState.Loading -> Loading(modifier)
is FeatureUiState.Success -> FeatureContent(
modifier,
onEvent = onEvent,
navTo = navTo,
data = ....
)
FeatureUiState.Error -> TODO()
}

3. Content — show the UI with no dependence to the ViewModel (or anything else).

Completely testable and reusable.

@Composable
internal fun FeatureContent(
modifier: Modifier = Modifier,
onEvent: (FeatureEvent) -> Unit,
navTo: (String) -> Unit,
) {

Checking your views

Always run the UI Check inside Android Studio!

New version of Android Studio can check the UI for you !!!

Connecting everything with the ViewModel.

ViewModel

Base Features

  1. Dependency Injection: Dependency is injected through the constructor, ensuring loose coupling and testability.
  2. StateFlow Creation: The photodoRepo's allGetPhotodos() method is used to retrieve a list of Photodo items, which is then mapped to a StateFlow using stateIn(). This provides a reactive state holder that emits updates to the UI when the underlying data changes.
  3. Initial State: The FeatureUiState.Loading state is set as the initial value for the uiState StateFlow. This indicates that the data is being fetched initially.
  4. Use collectAsStateWithLifecycle:

Do not use SharingStarted.WhileSubscribed(5000):

  1. Event Handling: The onEvent() method allows the ViewModel to handle user interactions or events that occur in the UI. Currently, it only handles the SetFeatureEvent.DeleteAllPhotos event, which triggers the deletion of all photos through the photodoRepo.deleteAll() method.

onEvent

Using onEvent in ViewModels allows you to encapsulate event handling within the ViewModel and avoid passing all actions directly to Composables. This leads to cleaner and more organized code, as the ViewModel becomes the central point for managing user interactions and triggering state updates.

When you pass every action directly to Composables, the Composables become responsible for handling user interactions and updating the UI state accordingly. This can lead to a tangled and cluttered codebase, making it difficult to maintain and understand.

By using onEvent, you can separate event handling from UI rendering. The ViewModel handles user interactions and emits events, while the Composables simply observe the UI state and update themselves accordingly. This separation of concerns promotes modularity and makes the code easier to test and reuse.

Additionally, using onEvent allows you to perform complex event handling logic within the ViewModel, such as validation, data processing, or side effects, without exposing these details to the Composables. This keeps the Composables focused on UI rendering and presentation, while the ViewModel handles the underlying business logic.

In summary, utilizing onEvent in ViewModels is a recommended practice for maintaining clean, organized, and maintainable code. It promotes separation of concerns, encapsulates event handling, and allows for more complex event handling logic within the ViewModel.

Facebook hit this issue and causes them to have messy code*.

Facebook needs to use the ‘onEvent’ in the ViewModel

All @Composables just need two args. Nav and Event.

@Composable
ComposeScreen(onEvent, navOn){
...
FloatingActionButton(onClick = {
onEvent(Event.Save)
navTo.navigate(Screen.Route.route)
})
...
}

Just place all event calls inside the ViewModel in the onEvent function and pass the onEvent function to the @Composable.

ViewModel ...

fun onEvent(event: Event) {
when (event) {
is Event.Somethign -> {}
is Event.SomethingElse -> {}
}
}

@Facebook: Thanks for sharing* and hope you learn something :-) Commend @Facebook Android Threads team for moving to Kotlin/Compose. The iOS Treads Team did some minor movement from Objective-C(rap) to Swift but never made the transition to SwiftUI / Swift Threading and SwiftData … And now they will never update their tech stack ??? https://www.youtube.com/watch?v=Q6vkIrhGhFs

Dev NOTE: Replace produceState with collectAsStateWithLifecycle

Replacing produceState with collectAsStateWithLifecycle is recommended for several reasons:

  1. Lifecycle Awareness:collectAsStateWithLifecycle automatically handles lifecycle events, ensuring that the StateFlow only emits updates when the UI is active. This prevents memory leaks and unnecessary updates when the UI is not visible.
  2. Improved Performance:collectAsStateWithLifecycle optimizes the state collection process, minimizing performance overhead and ensuring efficient state updates.
  3. Simplified Syntax:collectAsStateWithLifecycle offers a more concise and readable syntax compared to produceState, reducing code complexity and enhancing code maintainability.
  4. Error Handling:collectAsStateWithLifecycle provides built-in error handling mechanisms, simplifying error handling and improving the overall stability of the application.

In summary, switching to collectAsStateWithLifecycle from produceState offers several advantages, including lifecycle awareness, improved performance, simplified syntax, and better error handling. It aligns with Jetpack Lifecycle recommendations and promotes a more robust and maintainable codebase.

Modern Android Components

Hilt

Hilt is a dependency injection (DI) library for Android that reduces the boilerplate of doing manual dependency injection in your project. It is built on top of the popular DI library Dagger, providing the same benefits of compile-time correctness, runtime performance, scalability, and Android Studio support.

  • Dependency Injection (DI) framework: Manages dependencies between objects in your Android app.
  • Built on top of Dagger 2: Simplifies DI setup and configuration, especially for Android apps.

Provides

  • Automatic code generation for common DI tasks
  • Clearer scoping rules for Android components

Key Components

@Module:

  • Annotation for classes that provide dependencies.
  • Contains methods annotated with @Provides to create and configure instances of dependencies.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideApolloClient(
@ApplicationContext context: Context
): ApolloClient {
return apolloClient(context)
}
}

@Inject:

  • Annotation for constructors or fields that request injection of a dependency.
@HiltViewModel
class NextTaskViewModel @Inject constructor(
//private val photodoUseCases: PhotodoUseCases,
private val photodoRepo: PhotodoRepo,
private val yelpRepo: YelpRepo,
private val audioFun: AudioSystem, //TODO move to Repo
savedStateHandle: SavedStateHandle
) : ViewModel() {

How It Works

  1. Define Modules:
  • Create modules using @Module to provide dependencies.
  • Use @Provides methods within modules to create instances of dependencies.

2. Install Modules:

  • Annotate modules with @InstallIn to specify their scope.
  • Hilt automatically installs modules in appropriate components.

3. Inject Dependencies:

  • Use @Inject in constructors or fields to request injection of dependencies.
  • Hilt automatically provides the required instances at runtime.

Benefits

  • Reduced boilerplate code: Hilt handles common DI tasks automatically.
  • Improved testability: Easy to mock dependencies for testing.
  • Enhanced maintainability: Clearer dependency relationships and easier to refactor code.
  • Android-specific features: Built-in support for Android lifecycles and components.

In summary, Hilt simplifies DI in Android apps, making it easier to manage dependencies and promote code modularity, testability, and maintainability.

Usage

  1. Dependency Injection: Manages object creation and supply for classes, promoting loose coupling.
  2. ViewModels: Provides automatic lifecycle management and injection for ViewModels.
  3. Testing: Facilitates easy mocking and injection of dependencies for unit tests.
  4. Compose: Integrates seamlessly with Jetpack Compose for dependency injection in UI components.
  5. WorkManager: Supports injecting dependencies into WorkManager workers for background tasks.
  6. Room: Manages database access and entity creation through Hilt for easier database interactions.
  7. Retrofit: Injects Retrofit instances and services for network calls, managing their lifecycles.

Add Hilt to the TOML file*

hilt = "2.48"
# Hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }

*Android Studio will automatically update the version to the latest.

Local Reactive Database with RoomDB

RoomBD with DAO and Kotlin Flow for Reactive UI: An Overview

  • Android Jetpack library for managing local databases in your app.
  • Simplifies schema definition, data access, and query execution.
  • Provides type safety and compile-time checks for queries.

Datebase

@Database(entities = [PhotodoEntity::class], version = 1, exportSchema = false)
abstract class PhotodoDB : RoomDatabase() {

abstract val photodoDao: PhotodoDao
companion object {
const val DATABASE_NAME = "photodo_db"
@JvmStatic
fun getDatabase(context: Context): PhotodoDB {
return Room.databaseBuilder(
context,
PhotodoDB::class.java,
DATABASE_NAME
).build()
}
}
}

and the structure of the data

Entity

@Entity(tableName = "photodo_table")
@TypeConverters(Converters::class)
data class PhotodoEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val todoId: Int = 0,
@ColumnInfo(name = "title") val title: String = "",
@ColumnInfo(name = "description") val description: String = "",
// @ColumnInfo(name = "timestamp") val timestamp : ZonedDateTime = ZonedDateTime.now(),
@ColumnInfo(name = "date") val timestamp: kotlinx.datetime.LocalDateTime? = null,
// kotlinx.datetime.Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
@ColumnInfo(name = "lat") val lat: Double = 0.0,
@ColumnInfo(name = "lon") val lon: Double = 0.0,
@ColumnInfo(name = "alarmOn") val alarmOn: Boolean = false,
@ColumnInfo(name = "completed") val isCompleted: Boolean = false,
@ColumnInfo(name = "image_path") val imgPath: Uri? = null,
@ColumnInfo(name = "audio_path") val audioPath: Uri? = null

)

DAO (Data Access Object)

  • Interface with specific methods for accessing and manipulating data in the database.
  • Abstracts database interactions from the rest of your application.
  • Room automatically generates implementations for DAOs based on annotations.
@Dao
interface PhotodoDao {
// The flow always holds/caches latest version of data. Notifies its observers when the
// data has changed.
@Query("SELECT * FROM photodo_table ORDER BY date ASC")
fun getAllPhotodos(): Flow<List<PhotodoEntity>> // convert to Flow in the implementation.

// Inserting a new photodo
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(todo: PhotodoEntity)

@Query("SELECT * FROM photodo_table WHERE id = :id")
suspend fun findById(id: Int): PhotodoEntity?

// Deleting a photodo
@Delete
suspend fun delete(todo: PhotodoEntity)

// Deleting all photodos
@Query("DELETE FROM photodo_table")
suspend fun deleteAll()

...
}

Kotlin Flow

  • Stream of data that emits values over time.
  • Encapsulates asynchronous data retrieval and updates.
  • Enables reactive UI updates based on data changes.
interface PhotodoRepo {
fun allGetPhotodos(): Flow<List<Photodo>> // NOTE: wrap in Flow<Resource<<>>>
suspend fun insert(photodo: Photodo)
//suspend fun addTodoPhoto(photodo: Photodo)
suspend fun delete(photodo: Photodo)
suspend fun getPhotodoById(PhotodoId: Int): Photodo?
suspend fun deleteAll()
}

Combining these three

  • DAOs define operations (CRUD) for interacting with specific data entities.
  • Room facilitates efficient data access and query execution based on annotations.
  • Kotlin Flow streams live data changes from the database to the UI.

Benefits

  • Simpler data access: DAOs offer a clean interface for interacting with the database.
  • Type safety and compile-time checks: Room eliminates runtime errors in queries.
  • Reactive UI: Kotlin Flow enables automatic UI updates based on data changes.
  • Improved performance: Room optimizes queries and minimizes database roundtrips.

Example workflow

  1. Define a DAO interface with methods for fetching, inserting, updating, and deleting data.
  2. Annotate the entity classes with @Entity and related annotations (e.g., @PrimaryKey).
  3. Create an instance of the DAO using a RoomDatabase subclass.
  4. Use the DAO methods within your code to perform data operations.
  5. Return Kotlin Flow from DAO methods to stream data changes.
  6. In your UI, collect the Flow and update the UI based on emitted values.

Additional Points

  • Use suspending functions in DAO methods to execute queries asynchronously.
  • Consider leveraging Jetpack Compose with Flow to achieve reactive UI updates.
  • Implement proper error handling and data validation within DAOs and UI.

Resources

Add Room to the TOML file

# Room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }

Build the Database.

@Database(entities = [PhotodoEntity::class], version = 1, exportSchema = false)
abstract class PhotodoRoomDB : RoomDatabase() {

After setting up the build system. We add navigation to MainActivity.

More to come soon …

This document is under active development.

This document serves as a guide for building modern Android apps using MAD (Modern Android Development) principles. It focuses on Jetpack Compose, Material 3, Kotlin Coroutines, Flow, Ktor, Hilt, Room, and other cutting-edge tools and patterns.

Key elements of the MAD approach:

  • Intuitive Interfaces: Jetpack Compose and Material 3 combine to create beautiful and user-friendly UIs.
  • Reactive Performance: Kotlin Coroutines and Flow ensure fluid and responsive apps, even under heavy loads.
  • Seamless Data Management: Robust networking with Ktor and offline persistence with Room keep your app’s data readily available.
  • Modular Architecture: Clean Architecture and MVVM patterns decouple logic for scalability and maintainability.
  • Modern Tooling: Tools like TOML version catalog and CI/CD pipelines streamline development and keep your app running smoothly.

The document explores several aspects of MAD development, including:

  • Naming conventions: Tips for choosing app and file names.
  • Project setup: Utilizing Android Studio Jellyfish Canary and TOML files for dependency management.
  • Architecture: Leveraging the Android architecture samples and the Clean Architecture with MVVM pattern.
  • File structure: Organizing code modules and features for clarity and maintainability.
  • State-driven interactions: Understanding the core concepts of UI updates driven by state changes and events.
  • ViewModel: Handling logic, events, and state updates, keeping UI independent.
  • Modern Android components: Details on Hilt for dependency injection, Room for local database, and other tools.

This document is under active development as we detail our development process …

Thanks & Best,

~Ash

--

--