Many mobile applications need to fetch data to display on the screen when a screen is shown. Additionally, when the application configuration changes, it may not always be necessary to send new API requests, read from the database, etc.
In Android application development, we have several options for handling this. In this article, I will discuss some of them along with my personal preference.
I no longer use Fragments in my projects. However, these days, the likelihood of encountering both Fragments and Jetpack Compose together is higher than encountering Fragments alone. Therefore, I have included both in this article. Lastly, I will provide an example of fetching data, but there may be other possible approaches as well.

Article image made by Caner Kaşeler.
Table of contents:
- Example Project Structure 🏗️
- Possible Ways 🛣️
- My Personal Preference 👀
- Last Words 📝
1) Example Project Structure 🏗️
Firstly, let’s have a quick look at the project structure. I used Hilt for dependency injection in this article.
Application
It is basic application class with Hilt annotation.
package com.canerkaseler import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class App: Application()
MainActivity
Project has single fragment which is HomeFragment:
package com.canerkaseler import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.add import androidx.fragment.app.commit import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } if (savedInstanceState == null) { supportFragmentManager.commit { setReorderingAllowed(true) // I added single fragment which is HomeFragment. add<HomeFragment>(R.id.fragment_container_view) } } } }
Fragment
HomeFragment includes a compose screen:
package com.canerkaseler import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels class HomeFragment: Fragment(R.layout.home_fragment) { private val homeViewModel: HomeViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val view = inflater.inflate(R.layout.home_fragment, container, false) val composeView = view.findViewById<ComposeView>(R.id.compose_view) composeView.apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) setContent { HomeScreen() } } return view } @Composable private fun HomeScreen() { Box( modifier = Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "Hello Caner", fontSize = 24.sp ) } } }
ViewModel
HomeViewModel has a function to act like data fetching:
package com.canerkaseler import android.util.Log import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor() : ViewModel() { fun fetchArticleList() { Log.i("CANER", "--> fetchArticleList") } }
I will not share gradle, gradle.settings, xml, etc. kind of files in here but if you would like to look at them, please check out the repository.
2) Possible Ways 🛣️
There are some possible approaches to send request with screen opening.
class HomeFragment: Fragment(R.layout.home_fragment) { private val homeViewModel: HomeViewModel by viewModels() // Approach A. override fun onCreate(savedInstanceState: Bundle?) { ... // Approach A - fetch data. homeViewModel.fetchArticleList(from = "27-Fragment onCreate") } // Approach B. override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { ... // Approach B - fetch data. homeViewModel.fetchArticleList(from = "38-Fragment onCreateView") composeView.apply { ... setContent { HomeScreen() } } return view } @Composable private fun HomeScreen() { // Approach C. LaunchedEffect(Unit) { // Approach C- fetch data. homeViewModel.fetchArticleList(from = "55-Compose LaunchedEffect") } ... } }
Almost all of us used these approaches before Flow and Compose. However, the main question is:
What is the problem with these approaches?
The main issue is that they cannot prevent sending new requests when a configuration change occurs.
Configuration of android application can change by many ways, such as:
- Screen orientation
- Font size and weight
- Locale
- Dark mode versus light mode
- Etc.
You can find more in here. There is my example:

The example above shows that the app sends requests again when the theme changes. Therefore, if you follow this approach, your app may send multiple requests due to users who frequently change configurations while the screen is open.
How about “init” function of ViewModel?
Yes, it survives configuration changes. However, it creates a dependency on ViewModel creation. For instance, if you want to write a unit test without triggering the initial loading during ViewModel creation, you can’t avoid it because the init
function runs as soon as the ViewModel is created.
If this is not an issue for your project or test cases, then in my opinion, this approach is still valid.
3) My Personal Preference 👀
Instead of sending request from Fragment or Compose sides, my choice is fetching data from ViewModel side:
data class ScreenState( val text: String = "", val counter: Int = 1, ) @HiltViewModel class HomeViewModel @Inject constructor() : ViewModel() { private val screenState = MutableStateFlow(value = ScreenState()) val uiState = screenState .onStart { fetchArticleList(from = "ViewModel") } .stateIn( scope = viewModelScope, started = SharingStarted .WhileSubscribed(stopTimeoutMillis = 5000L), initialValue = screenState.value, ) suspend fun fetchArticleList(from: String) { delay(timeMillis = 2000L) Log.i("CANER", "--> fetchArticleList from: $from") screenState.update { state -> state.copy( text = "Fetch Data ${state.counter}", counter = state.counter + 1 ) } } }
Then, UI side can collect state:
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle() HomeScreen( text = uiState.value.text )
As summary, below gif shows that app does not send a new request when configuration changes. However, app sends a new request when it waits more than 5 seconds on background.

Final result.
More details are that we have screenState
as StateFlow
which has ScreenState
data class with default values. When app would like to collect state in compose function, uiState
provides value of screenState
. First time, default value of text is empty string. So, we see just “Hello Caner,” text in first time.
Then, onStart
function runs. You can call suspend
function in onStart
. In our example, we can see that screenState
is updated in fetchArticleList()
function. Thus, uiState
provides new value and app shows “Hello Caner, Fetch Data 1”.
Next, gif shows that app goes background and foreground under 5 seconds, so app does not send a request in view model. It is happening because of giving 5 seconds in here: WhileSubscribed(stopTimeoutMillis = 5000L)
. Basically, if the compose function still subscribed for 5 seconds, the flow provides same result. However, when app waits more than 5 seconds, onStart
function re-run. So, app shows “Hello Caner, Fetch Data 2” text.
Because of above explanation, theme changing (one of the configuration change ways) does not trigger screen loading because it happens under 5 seconds.
Hence, 5 seconds are important duration. If your app must need to show fresh data every time, you should not use 5 seconds. There can be zero duration: WhileSubscribed(stopTimeoutMillis = 0L)
. In this way, you will always have fresh data with screen opening.
Job Offers
Before reasons of my preference, as you know code reviewing is crucial especially when topic is about architecture, state management, etc. However, if you are a pull request reviewer, how you should communicate with your colleagues in multicultural team? How should you start your reviewing? What do you need to check? How can you avoid conflicts before they start? How should you write your comments?
For answers and more tips, please check out my new 9 pages article, Professional Code Review Skills on Amazon Kindle and Gumroad. 📚
Why I prefer this approach?
- Screen state in single place.
- You can use
combine()
for more complex works. - No need to think about configuration changes.
- There is flexibility about screen refreshing with
sstopTimeoutMillis()
. - When you cannot use
init
because of architecture or any other reason, you can use this approach.
I use this approach in my personal projects and open-source projects.
Important: 5 seconds is ANR timeout duration. So, if you follow this approach, play safe and do not enter more than 5 seconds. You can read more about it in official document.
4) Last Words 📝
Although this article touches on many topics, it does not cover all the details, such as Flow types, Compose design, Hilt, etc. Instead of presenting a long text with every detail, this article serves as a starting point for your research on state management and initial loading approaches. Plus, if you would like to run your own tests, I have prepared a GitHub repository for you. 📚
I hope this article is helpful to you! If you would like to support my work, coffee is my best friend when writing code and articles: https://buymeacoffee.com/canerkaseler ☕️
Don’t forget to check out my other Medium articles. You can reach me on social media platforms — stay tuned! 🤝
This article was previously published on proandroiddev.com.