Showing posts with label Clean Architecture. Show all posts
Showing posts with label Clean Architecture. Show all posts

Building a Scalable Architecture for an eCommerce App with Jetpack Compose

Design a scalable architecture for an eCommerce app built with Jetpack Compose. This architecture will support key features like offline product caching, real-time inventory updates, paginated product listings, and modular UI with feature separation. We’ll focus on best practices for scalability, maintainability, and modularity, ensuring the app can handle future growth efficiently.

Overview of the App Architecture

The architecture for this app will be based on Clean Architecture, separating concerns into Presentation, Domain, and Data layers. We will also modularize the app to ensure flexibility, and each feature (e.g., Product, Cart, Inventory) will be handled in a separate module.

We'll incorporate Jetpack Compose for UI, Room for offline caching, Paging 3 for efficient product listing, and Firebase/Realtime Database or WebSocket for real-time inventory updates.

Layered Architecture Breakdown

1. Presentation Layer (UI)

The Presentation Layer is responsible for the user interface and user interactions. With Jetpack Compose, we can easily build reactive and dynamic UIs. The UI will be composed of Composables, while ViewModels will handle the UI state and interact with the Domain layer.

Key Components:

  • Jetpack Compose: For building the user interface in a declarative way.

  • ViewModel: Handles state management and communicates with the Domain layer.

  • StateFlow/LiveData: For managing UI state like loading, success, and error states.

  • Navigation: Jetpack Navigation Compose to manage the app's navigation.

Example Composables:

@Composable
fun ProductListScreen(viewModel: ProductListViewModel) {
    val products by viewModel.products.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val isError by viewModel.isError.collectAsState()

    if (isLoading) {
        CircularProgressIndicator()
    } else if (isError) {
        Text("Error fetching products")
    } else {
        LazyColumn {
            items(products) { product ->
                ProductItem(product = product)
            }
        }
    }
}

2. Domain Layer

The Domain Layer holds the business logic and use cases. This layer abstracts the data layer and provides clean interfaces for the Presentation layer to interact with. The domain layer consists of Use Cases and Repository interfaces.

Key Components:

  • Use Cases: Define business logic, such as fetching products, pagination, and handling inventory.

  • Repositories: Interface that defines data-fetching operations like fetching products, updating inventory, and more.

Example Use Case:

class GetProductListUseCase(private val productRepository: ProductRepository) {
    suspend operator fun invoke(page: Int): Result<List<Product>> {
        return productRepository.getPaginatedProducts(page)
    }
}

3. Data Layer

The Data Layer handles data fetching, caching, and the communication with external services (like APIs and Firebase). This layer includes repositories for both remote data (API calls) and local data (Room Database). We’ll use Room for offline caching and Paging 3 for efficient data loading.

Key Components:

  • Room: Used for offline caching of products and inventory data.

  • API Services: Retrofit or Ktor for interacting with remote APIs for products and real-time updates.

  • Firebase/Realtime Database: Used for real-time inventory updates.

  • Paging 3: Efficiently handles pagination for product lists.

Offline Caching Example with Room:

@Entity(tableName = "product")
data class ProductEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val price: Double,
    val stockQuantity: Int
)

@Dao
interface ProductDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProducts(products: List<ProductEntity>)

    @Query("SELECT * FROM product")
    suspend fun getAllProducts(): List<ProductEntity>
}

Repository Example:

class ProductRepositoryImpl(
    private val apiService: ApiService,
    private val productDao: ProductDao
) : ProductRepository {

    override suspend fun getPaginatedProducts(page: Int): Result<List<Product>> {
        val productsFromCache = productDao.getAllProducts()
        if (productsFromCache.isNotEmpty()) {
            return Result.success(productsFromCache.map { it.toDomain() })
        }

        try {
            val response = apiService.getProducts(page)
            productDao.insertProducts(response.products.map { it.toEntity() })
            return Result.success(response.products.map { it.toDomain() })
        } catch (e: Exception) {
            return Result.failure(e)
        }
    }
}

4. Real-Time Inventory Updates

For real-time inventory updates, we can use Firebase Realtime Database or WebSocket. When the stock quantity of a product changes, the app will update the product's data in real time, and the UI will reflect the updated information.

Firebase Example:

class FirebaseInventoryRepository {
    private val database = FirebaseDatabase.getInstance().getReference("inventory")

    fun observeInventoryUpdates(productId: Int, callback: (Int) -> Unit) {
        database.child("products").child(productId.toString()).child("stockQuantity")
            .addValueEventListener(object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    val stockQuantity = snapshot.getValue(Int::class.java) ?: 0
                    callback(stockQuantity)
                }

                override fun onCancelled(error: DatabaseError) {
                    // Handle error
                }
            })
    }
}

5. Modularization

To ensure that the app remains maintainable as it grows, we will modularize the codebase. Each feature, such as the Product module, Cart module, and Inventory module, will be developed in separate modules.

This separation ensures that each module is responsible for one feature and can be developed and tested independently. It also improves build times and allows for easier team collaboration.

Modularization Example:

// In build.gradle for 'product' module
dependencies {
    implementation project(":core")
    implementation "androidx.compose.ui:ui:$compose_version"
}

6. Offline Handling and Connectivity

The app should handle offline scenarios gracefully, providing users with cached data when they are not connected to the internet. We can use the ConnectivityManager to check the network status and display cached products when offline. When the network is available, the app should fetch real-time data.

Offline Strategy:

  • Room Database: Cache products and inventory locally.

  • Network Status: Use ConnectivityManager to determine if the app is online or offline.

7. Real-Time Sync with Firebase

Firebase can be used for real-time syncing of inventory data. Using Firebase Realtime Database, the app can listen for changes to inventory quantities and update the UI instantly. Alternatively, WebSocket can be used to get real-time updates from the backend.

My thoughts

This architecture leverages modern Android tools like Jetpack Compose, Room, Paging 3, Firebase, and Clean Architecture to build a scalable and maintainable eCommerce app. The use of modularization ensures that each feature is self-contained, while the domain-driven design keeps the business logic separated from the UI.

By incorporating offline caching, real-time updates, and pagination, this architecture provides a robust foundation for building a seamless, scalable eCommerce experience that performs well even in scenarios with slow or no network connectivity.

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

Happy coding! 💻

MVVM vs MVI vs MVP: Which Architecture Fits Your Android Kotlin Compose Project?

When developing Android apps using Kotlin and Jetpack Compose, the architecture you choose should align with your application's needs, scalability, and maintainability. Let's explore the best architecture and discuss other alternatives with examples to help you make the best decision.

1. MVVM (Model-View-ViewModel) Architecture

Overview:

MVVM is the most commonly recommended architecture for Android apps using Jetpack Compose. It works seamlessly with Compose’s declarative UI structure and supports unidirectional data flow.

  • Model: Represents the data and business logic (e.g., network requests, database calls, etc.).
  • View: Composed of composable functions in Jetpack Compose. It displays the UI and reacts to state changes.
  • ViewModel: Holds UI-related state and business logic. It is lifecycle-aware and acts as a bridge between the View and Model.

How MVVM Works:

  • The View is responsible for presenting data using Compose. It observes the state exposed by the ViewModel via StateFlow or LiveData.
  • The ViewModel holds and processes the data and updates the state in response to user actions or external data changes.
  • The Model handles data fetching and business logic and communicates with repositories or data sources.

Benefits:

  • Separation of concerns: The View and Model are decoupled, making the app easier to maintain.
  • Reactivity: With Compose's state-driven UI, the View updates automatically when data changes in the ViewModel.
  • Scalability: MVVM works well for larger, complex apps.

Example:

// ViewModel
class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> get() = _state

    fun fetchData() {
        // Simulate network request
        _state.value = _state.value.copy(data = "Fetched Data")
    }
}

// Composable View
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Column {
        Text(text = state.data)
        Button(onClick = { viewModel.fetchData() }) {
            Text("Fetch Data")
        }
    }
}

Best For:

  • Real-time applications (e.g., chat apps, social media, etc.)
  • Apps with dynamic and complex UI requiring frequent backend updates.
  • Enterprise-level applications where clear separation of concerns and scalability are required.

2. MVI (Model-View-Intent) Architecture

Overview:

MVI focuses on unidirectional data flow and immutable state. It's more reactive than MVVM and ensures that the View always displays the latest state.

  • Model: Represents the application’s state, typically immutable.
  • View: Displays the UI and reacts to state changes.
  • Intent: Represents the actions that the View triggers (e.g., button clicks, user input).

How MVI Works:

  • The View sends Intents (user actions) to the Presenter (or ViewModel).
  • The Presenter updates the Model (state) based on these actions and then triggers a state change.
  • The View observes the state and re-renders itself accordingly.

Benefits:

  • Unidirectional data flow: The state is always predictable as changes propagate in one direction.
  • Immutable state: Reduces bugs associated with mutable state and ensures UI consistency.
  • Reactive: Well-suited for applications with frequent UI updates based on state changes.

Example:

// MVI - State, ViewModel
data class MyState(val data: String = "")

class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> get() = _state

    fun processIntent(intent: MyIntent) {
        when (intent) {
            is MyIntent.FetchData -> {
                _state.value = MyState("Fetched Data")
            }
        }
    }
}

// Composable View
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Column {
        Text(text = state.data)
        Button(onClick = { viewModel.processIntent(MyIntent.FetchData) }) {
            Text("Fetch Data")
        }
    }
}

Best For:

  • Complex UI interactions: Apps with multiple states and actions that must be tightly controlled.
  • Real-time data-driven apps where state changes must be captured and handled immutably.
  • Apps that require a highly reactive UI, such as games or media streaming apps.

3. MVP (Model-View-Presenter) Architecture

Overview:

MVP is a simpler architecture often used in legacy apps. In MVP, the Presenter controls the logic and updates the View, which is passive and only responsible for displaying data.

  • Model: Represents the data and business logic.
  • View: Displays UI and delegates user interactions to the Presenter.
  • Presenter: Acts as a middleman, processing user input and updating the View.

How MVP Works:

  • The View delegates all user actions (clicks, input, etc.) to the Presenter.
  • The Presenter fetches data from the Model and updates the View accordingly.

Benefits:

  • Simple and easy to implement for small applications.
  • Decouples UI logic from the data layer.

Example:

// MVP - Presenter
interface MyView {
    fun showData(data: String)
}

class MyPresenter(private val view: MyView) {
    fun fetchData() {
        // Simulate fetching data
        view.showData("Fetched Data")
    }
}

// Composable View
@Composable
fun MyScreen(view: MyView) {
    val presenter = remember { MyPresenter(view) }

    Column {
        Button(onClick = { presenter.fetchData() }) {
            Text("Fetch Data")
        }
    }
}

class MyViewImpl : MyView {
    override fun showData(data: String) {
        println("Data: $data")
    }
}

Best For:

  • Simple apps with minimal business logic.
  • Legacy projects that already follow the MVP pattern.
  • Applications with simple user interactions that don’t require complex state management.

Conclusion: Which Architecture to Choose?

Architecture Strengths Best For Example Use Cases
MVVM Seamless integration with Jetpack ComposeClear separation of concernsScalable and testable Large, complex appsReal-time appsTeam-based projects E-commerce apps, banking apps, social apps
MVI Immutable stateUnidirectional data flowReactive UI Highly interactive appsReal-time dataComplex state management Messaging apps, live score apps, media apps
MVP Simple to implementGood for small appsEasy to test Small appsLegacy appsSimple UI interactions Note-taking apps, simple tools, legacy apps

Best Recommendation:

  • MVVM is generally the best architecture for most Android Kotlin Compose apps due to its scalability, maintainability, and seamless integration with Compose.
  • MVI is ideal for apps that require complex state management and reactive UI updates.
  • MVP is still useful for simple apps or projects that already follow MVP.

Thanks for reading! 🎉 I'd love to know what you think about the article. Did it resonate with you? 💭 Any suggestions for improvement? I’m always open to hearing your feedback to improve my posts! 👇🚀. Happy coding! 💻✨

Implementing REST API Integration in Android Apps Using Jetpack Compose and Modern Architecture


Designing a robust, maintainable, and scalable Android application requires implementing solid architecture principles and leveraging modern tools and components. This article provides a comprehensive guide to building an app with MVVM (Model-View-ViewModel) and Clean Architecture using the latest Android components: Coroutines, Hilt, Jetpack Compose, Retrofit, and Gson. We'll use the Star Wars API (https://swapi.dev/api/people/) as an example.




Why MVVM and Clean Architecture?

  • MVVM: Separates UI (View) from business logic (ViewModel) and data (Model), making the codebase more manageable and testable.
  • Clean Architecture: Divides the app into layers (Presentation, Domain, and Data) to enforce clear separation of concerns, making the code more reusable and easier to modify.
  • Retrofit: A type-safe HTTP client for Android and Java, making it easy to fetch data from a REST API.
  • Gson: A library for converting Java objects into JSON and vice versa, which is ideal for handling API responses.
  • Jetpack Compose: The modern UI toolkit for building native Android apps with declarative syntax, providing a more intuitive way to design interfaces.
  • Hilt: It simplifies the DI process by generating the necessary components at compile-time, allowing us to inject dependencies such as the Retrofit service and the CharacterRepository without manually writing boilerplate code.

App Structure and Folder Format

Here's a sample folder structure for our app:

com.example.starwarsapp
├── data
│   ├── api
│   │   └── StarWarsApiService.kt
│   ├── model
│   │   └── Character.kt
│   ├── repository
│       └── CharacterRepository.kt
├── di
│   └── AppModule.kt
├── domain
│   ├── model
│   │   └── CharacterDomainModel.kt
│   ├── repository
│   │   └── CharacterRepositoryInterface.kt
│   └── usecase
│       └── GetCharactersUseCase.kt
├── presentation
│   ├── ui
│   │   ├── theme
│   │   │   └── Theme.kt
│   │   └── CharacterListScreen.kt
│   └── viewmodel
│       └── CharacterViewModel.kt
└── MainActivity.kt

Step-by-Step Implementation

1. Dependencies in build.gradle

dependencies {
    // Retrofit for API requests
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    
    // Hilt for dependency injection
    implementation 'com.google.dagger:hilt-android:2.48'
    kapt 'com.google.dagger:hilt-compiler:2.48'
    
    // Jetpack Compose
    implementation 'androidx.compose.ui:ui:1.5.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
    
    // Kotlin Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}

2. API Service

StarWarsApiService.kt

package com.example.starwarsapp.data.api

import retrofit2.http.GET
import com.example.starwarsapp.data.model.Character

interface StarWarsApiService {
    @GET("people/")
    suspend fun getCharacters(): List<Character>
}

3. Model Classes

API Data Model

Character.kt

package com.example.starwarsapp.data.model

data class Character(
    val name: String,
    val gender: String
)

Domain Model

CharacterDomainModel.kt

package com.example.starwarsapp.domain.model

data class CharacterDomainModel(
    val name: String,
    val gender: String
)

4. Repository

CharacterRepository.kt

package com.example.starwarsapp.data.repository

import com.example.starwarsapp.data.api.StarWarsApiService
import com.example.starwarsapp.domain.model.CharacterDomainModel

class CharacterRepository(private val apiService: StarWarsApiService) {
    suspend fun fetchCharacters(): List<CharacterDomainModel> {
        return apiService.getCharacters().map {
            CharacterDomainModel(name = it.name, gender = it.gender)
        }
    }
}

5. Use Case

GetCharactersUseCase.kt

package com.example.starwarsapp.domain.usecase

import com.example.starwarsapp.data.repository.CharacterRepository
import com.example.starwarsapp.domain.model.CharacterDomainModel

class GetCharactersUseCase(private val repository: CharacterRepository) {
    suspend operator fun invoke(): List<CharacterDomainModel> {
        return repository.fetchCharacters()
    }
}

6. ViewModel

CharacterViewModel.kt

package com.example.starwarsapp.presentation.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.starwarsapp.domain.model.CharacterDomainModel
import com.example.starwarsapp.domain.usecase.GetCharactersUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CharacterViewModel(private val getCharactersUseCase: GetCharactersUseCase) : ViewModel() {
    private val _characters = MutableStateFlow<List<CharacterDomainModel>>(emptyList())
    val characters: StateFlow<List<CharacterDomainModel>> get() = _characters

    init {
        fetchCharacters()
    }

    private fun fetchCharacters() {
        viewModelScope.launch {
            _characters.value = getCharactersUseCase()
        }
    }
}

7. Dependency Injection

AppModule.kt

package com.example.starwarsapp.di

import com.example.starwarsapp.data.api.StarWarsApiService
import com.example.starwarsapp.data.repository.CharacterRepository
import com.example.starwarsapp.domain.usecase.GetCharactersUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl("https://swapi.dev/api/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides
    fun provideStarWarsApi(retrofit: Retrofit): StarWarsApiService =
        retrofit.create(StarWarsApiService::class.java)

    @Provides
    fun provideCharacterRepository(apiService: StarWarsApiService) =
        CharacterRepository(apiService)

    @Provides
    fun provideGetCharactersUseCase(repository: CharacterRepository) =
        GetCharactersUseCase(repository)
}

8. Compose UI

CharacterListScreen.kt

package com.example.starwarsapp.presentation.ui

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.starwarsapp.presentation.viewmodel.CharacterViewModel

@Composable
fun CharacterListScreen(viewModel: CharacterViewModel) {
    val characters = viewModel.characters.collectAsState().value

    LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        items(characters.size) { index ->
            val character = characters[index]
            Column(modifier = Modifier.padding(8.dp)) {
                Text(text = "Name: ${character.name}")
                Text(text = "Gender: ${character.gender}")
            }
        }
    }
}

9. Main Activity

MainActivity.kt

package com.example.starwarsapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.starwarsapp.presentation.ui.CharacterListScreen
import com.example.starwarsapp.presentation.viewmodel.CharacterViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    @Inject lateinit var viewModel: CharacterViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CharacterListScreen(viewModel = viewModel)
        }
    }
}

Conclusion

This app architecture demonstrates the seamless integration of MVVM and Clean Architecture principles using modern tools like Compose, Hilt, and Coroutines. By following this pattern, you ensure scalability, testability, and maintainability for your app. 

Happy coding!


What’s your favorite Kotlin string manipulation tip? Share in the comments below!