From recent release on revamped guide to app architecture by Android team, I have built an android app with Compose which follows those architecture principles. I will highlight UI layer and Data layer by taking my sample app as an example which fetches data from network, the code is available in GitHub and below is app preview.

You can take reference of the source code from GitHub repository for the app which I will take as an example.

Below image clearly outlines the architecture of the app.

Right now I’m using compose version 1.2.0-beta02 which is latest release.

1. Layers in architecture

It is recommended to have atleast 2 layers for any app. I have implemented UI & Data layer and ignoring Domain layer for now.

1. UI Layer

  1. UI elements built with Jetpack Compose.
  2. ViewModels that holds & handles the data.
  3. Data for UI is exposed by ViewModels as State.

In this sample I have 3 screens and each screen have their own ViewModel to manage data, and built with compose not views.

2. Data Layer

Data layer contains Repository class which holds data from different data sources in one place.

  1. Exposes data to ViewModels
  2. Contains business logic.
  3. We can have many repositories based on data categories.

I have implemented repository pattern with single data source, where data is fetched from remote api. This data exposed to ViewModels.

2. Building Repository and Data layer

Okay let’s start from data layer to build repository class.

AppRepository.kt

Repository takes NetworkDataSource as constructor, at this point we will able to access any data which is exposed from the NetworkDataSource. We will initialize repository later along with passing source to it as a constructor.

class AppRepository(
    private val source: NetworkDataSource
) {
    
}

Now let’s add suspend function in repository which will access data from it’s source which will return list of popular actors from api.

class AppRepository(
    private val source: NetworkDataSource
) {
    // Suspend function executes network call.
    suspend fun getPopularActorsData(): List<Actor> {
        val popularActorsList: List<Actor>
        withContext(Dispatchers.IO) {
            popularActorsList = source.getPopularActors()
        }
        return popularActorsList
    }
}
  1. Functions in this repository executes on an IO-optimized thread pool, makes main-safe.
  2. Repository class executes network calls from [NetworkDataSource] to return data.
  3. Data returned from this functions will be exposed to ViewModels.

I have created Actor data class with single name property.

data class Actor(
    val actorName: String
)

NetworkDataSource.kt

In repository we are calling function with name getPopularActors( ) from network source. Let’s expose that function which will execute the network call and returns data.

class NetworkDataSource {

    // Contains all url endpoints to json data
    private val requestUrls by lazy { RequestUrls }
    // Returns all json responses
    private val jsonData by lazy { JsonRemoteData(requestUrls) }
    // To make http request calls
    private val queryUtils by lazy { NetworkQueryUtils() }

    /**
     * @return the result of latest list of all popular actors fetched from the network.
     */
    fun getPopularActors(): List<Actor> {
        val requestUrl = requestUrls.getPopularActorsUrl()
        val response = queryUtils.getResponseFromHttpUrl(requestUrl)
        return jsonData.fetchActorsJsonData(response)
    }
}

Just like function getPopularActors( ) in source class, other functions can expose themselves for repository to access it. Those functions in repository can expose to ViewModels later.

For other sources such as database you can create a separate repository class and later merge all those sources into single repository, since I have single network source I don’t need more than one repository.

Single source

At this point our repository remains single source because it directly communicates and gets data from source directly, also repository is only place ViewModels can have access to any data.

All the functions in source providers exposed are immutable, this will prevent other classes such as Repository and ViewModel from changing their value. This makes managing flow of data easier to handle and flows in unidirectional.

That’s it for Data layer.

3. Initialize repository with source

Let’s initialize the global state of our Repository once in application class, initialized property will be shared to ViewModels constructor.

ComposeActorsApp.kt

class ComposeActorsApp : Application() {

    // Only place where repository has initialized and passed across viewModels.
    lateinit var repository: AppRepository

    override fun onCreate() {
        super.onCreate()

        // This is the only data source for whole app.
        val networkDataSource = NetworkDataSource()
        repository = AppRepository(networkDataSource)
    }
}

We can totally ignore this way of creating repository instance by doing it directly inside ViewModels. But making use of application class will have single instance state for repository in whole app. Make sure to link the application class with name attribute in manifest file.

AppNavigation.kt

Now we have to pass repository instance to ViewModels, doing so exposed data in repository can be accessed by ViewModels.
This can be done when destinations are composed in NavHost, and then we can pass those ViewModels to their respective destinations along with repository.

We need application context and type cast it to android Application, so that we can access initialized repository property.

val application = LocalContext.current.applicationContext as Application
val repository = (application as ComposeActorsApp).repository

Pass the repository to destination screens as a constructor parameter in ViewModels.

HomeScreen(
    viewModel = viewModel(
        factory = HomeViewModel.provideFactory(
            application, repository
        )
    )
)

Since we are providing ViewModel with arguments we need to implement factory for it’s instance to be created properly, and that’s why I have added factory property which provides those arguments to ViewModel. This is final changes.

@Composable
fun AppNavigation(
    //
) {
    val application = LocalContext.current.applicationContext as Application
    val repository = (application as ComposeActorsApp).repository

    NavHost(
        //
    ) {
        composable(AppDestinations.HOME_ROUTE) {
            HomeScreen(
                viewModel = viewModel(
                    factory = HomeViewModel.provideFactory(
                        application, repository
                    )
                )
            )
        }
    }
}

We still did not create HomeViewModel for HomeScreen, we will do that in UI layer next. The composable function destination screen HomeScreen has ViewModel parameter.

// Composable destination
@Composable
fun HomeScreen(
    viewModel: HomeViewModel
) {
    // Screen content
}

Now let’s move to UI layer, we will build composable elements and ViewModel with factory which have State holders.

4. Building UI layer

UI layer contains composable UI elements which appear on screen and ViewModel which hold and manage state of data.

Let’s continue on building ViewModel for HomeScreen destination.

HomeViewModel.kt

class HomeViewModel(
    application: Application,
    private val repository: AppRepository
) : AndroidViewModel(application) {

}

Factory

Create ViewModel instance and provide arguments with factory using AndroidViewModelFactory.

fun provideFactory(
    application: Application,
    repository: AppRepository
): ViewModelProvider.AndroidViewModelFactory {
    return object : ViewModelProvider.AndroidViewModelFactory(application) {
        @Suppress("unchecked_cast")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return HomeViewModel(application, repository) as T
        }
    }
}

State holders

ViewModels access data from functions and properties which are exposed in repositories.
We need to manage and hold that data so that UI elements can consume it. This data can be encapsulated in a data class.

Declare a data class which holds the data. Initially it will empty. We can add more than one property for this data class, such as a boolean state whether to show or hide a loading progress bar.

data class HomeUiState(
    var popularActorList: List<Actor> = emptyList()
)

The property inside the data class is mutable because it’s value will be updated inside ViewModel.
Each ViewModel can have their own data holder and later their state will be managed using MutableState.

Declare a MutableState property which exposes the data to UI elements and holds the updated value of HomeUiState in ViewModel.

// Holds the state for values in HomeUiState
var uiState by mutableStateOf(HomeUiState())
    private set

Launch a Coroutine with viewModelScope, inside the coroutine call and execute the suspend function from the repository which fetches the data from network.
Update the MutableState property uiState in ViewModel with updated value.

init {
    viewModelScope.launch {
        uiState = HomeUiState(
            popularActorList = repository.getPopularActorsData()
        )
    }
}

That’s it, now our ViewModel gets data from repository and exposes that data to UI elements with StateHolders.

Final HomeViewModel changes :

class HomeViewModel(
    application: Application,
    private val repository: AppRepository
) : AndroidViewModel(application) {

    // Holds the state for values in HomeViewState
    var uiState by mutableStateOf(HomeUiState())
        private set

    init {
        viewModelScope.launch {
            uiState = HomeUiState(
                popularActorList = repository.getPopularActorsData()
            )
        }
    }

    companion object {
        
        fun provideFactory(
            application: Application,
            repository: AppRepository
        ): ViewModelProvider.AndroidViewModelFactory {
            return object : ViewModelProvider.AndroidViewModelFactory(application) {
                @Suppress("unchecked_cast")
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    return HomeViewModel(application, repository) as T
                }
            }
        }
    }
}

data class HomeUiState(
    var popularActorList: List<Actor> = emptyList()
)

Let’s move to composable UI elements.

HomeScreen.kt

Since our HomeViewModel class exposes data, let’s consume those and update the UI elements.

fun HomeScreen(
    viewModel: HomeViewModel
) {
    // Access data from uiState
    val uiState = viewModel.uiState
    val actorsList = uiState.popularActorList
    ShowActorsList(actorsList)
}

// Shows list of actors in screen.
@Composable
private fun ShowActorsList(
    actorsList: List<Actor>,
) {
    LazyColumn {
        items(actorsList) { actor ->
            Text(text = actor.actorName)
        }
    }
}

With this UI layer implementation is completed. Refer to my app in GitHub repository linked below for advanced UI elements.

5. App Navigation

We have so far implemented the Data and UI layer with single ViewModel and Screen.
Now let’s take a look at app navigation and add more screens.

Let’s start from the app entry point which is MainActivity class. We will trigger navigation to occur.

MainActivity.kt

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeActorsTheme {
                AppNavigation()
            }
        }
    }
}

AppDestinations.kt

All destination routes in one place.

object AppDestinations {
    const val HOME_ROUTE = "home" // Default destination
    const val SEARCH_ROUTE = "search" // destination for searching actors
    const val ACTOR_DETAIL_ROUTE = "actor detail" // list to details destination
    const val ACTOR_DETAIL_ID_KEY = "actorId" // list to selected actor detail
}

AppNavigation.kt

@Composable
fun AppNavigation(
    startDestination: String = AppDestinations.HOME_ROUTE,
    routes: AppDestinations = AppDestinations
) {
    val navController = rememberNavController()
    val application = LocalContext.current.applicationContext as Application
    val repository = (application as ComposeActorsApp).repository

    // Composables declared in NavHost can be controlled within by NavController.
    NavHost(
        navController = navController,
        startDestination = startDestination
    ) {
        // Default destination
        composable(
            AppDestinations.HOME_ROUTE
        ) {
            HomeScreen(
                viewModel(
                    HomeViewModel.provideFactory(application, repository)
                )
            )
        }

        // Search screen destination
        composable(
            AppDestinations.SEARCH_ROUTE
        ) {
            SearchScreen(
                viewModel(
                    SearchViewModel.provideFactory(application, repository)
                )
            )
        }

        // Detail screen destination
        composable(
            route = "ACTOR_DETAIL_ID_KEY",
            arguments = listOf()
        ) { backStackEntry ->
            val arguments = requireNotNull(backStackEntry.arguments)
            val actorId = arguments.getInt(routes.ACTOR_DETAIL_ID_KEY)
            DetailScreen(
                viewModel(
                    DetailsViewModel.provideFactory(application, actorId, repository)
                )
            )
        }
    }
}
  1. NavHost contains three composable destinations (Home, Search, Detail).
  2. Each Screen has its own ViewModels.
  3. Every ViewModel has access to data in repository.

Screens :

// UI for home screen
@Composable
fun HomeScreen(
    viewModel: HomeViewModel
) {
    val uiState = viewModel.uiState
    // Screen content
}

// UI for search screen
@Composable
fun DetailScreen(
    viewModel: DetailsViewModel
) {
    val uiState = viewModel.uiState
    // Screen content
}

// UI for search screen
@Composable
fun SearchScreen(
    viewModel: SearchViewModel
) {
    val uiState = viewModel.uiState
    // Screen content
}

That’s it !
Take a look at GitHub repository which I have linked below has complete code example on building advanced layouts, fetching data from api, animations, draw on canvas, search functionality, loader, UI state handling, Image loading, light/dark theme with MD3, palette usage etc.

Read all the detailed guidelines from Android documentation, it covers everything in much depth.

6. Project code and Resources

Android Documentation on Architecture.

1. Guide to app architecture
2. UI layer Implementation
3. Data Layer Implementation
4. Rebuilding our guide to app architecture

Rajasekhar K E


Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.