This already looks quite good.
Apart from some minor formatting issues I think the following things can be improved:
In the MainActivity the property dataStore can be removed. It isn't used anywhere.
When you only pass the app context to the view model's constructor you don't need to call ViewModelProvider, just use viewModel() from your compose code to obtain an instance like this:
MyApp(viewModel = viewModel())
The data store is like a small database and can store any number of key/value pairs. You should rename it to something more generic like SettingsDataStoreManager. It may only provide a single setting right now, but its nature is to potentially provide many settings.
The creation of the dataStore object should not be part of the class definition itself, it should be moved to the file level as described in the documentation. You should also rename the data store name from FirstTimeLaunch to settings.
Upper case is reserved for (enums and) constants that are actually declared with const. IS_FIRST_TIME_LAUNCH_KEY should therefore be renamed to isFirstTimeLaunchKey. Also make it private, it should only be accessible by the datastore itself. In addition, the companion object where you defined the key should be the last member of the class as defined in the Kotlin Coding Conventions.
There must only be one instance of any given the data store. Currently only FirstTimeViewModel uses it, but this may change in the future, especially if you add more settings. The easiest way to make sure there is only one instance is to annotate it with @Singleton and let a dependency injection framework like Hilt handle the rest. You won't create objects yourself anymore, your classes will have everything they need automatically passed by their constructor. This also makes view models with multiple parameters much more easy to use.
In Kotlin you usually don't use method names starting with get or set just to indicate the kind of property access. getFirstTimeLaunch in your data store should be renamed to isFirstTimeLaunch which corresponds better to its return value.
The dataStore property in the view model should be made private, it shoudn't be accessible from outside of the class.
The data store already returns a flow for getFirstTimeLaunch, but you decided to switch it to a LiveData in your view model. LiveData shouldn't be used anymore when you use Compose for your UI. Everything should either be a Flow or a MutableState. View models in general should just transform flows when the content needs to be updated, but when exposed (as in "made public" by a property) they should be converted into a StateFlow. Your view model's getFirstTime property (now also isFirstTime) should be defined like this:
val isFirstTime: StateFlow<Boolean> = dataStore.isFirstTimeLaunch()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = true,
)
To access this in your composable, you would now replace this:
val isFirstTime by viewModel.getFirstTime.observeAsState(initial = true)
by this:
val isFirstTime by viewModel.isFirstTime.collectAsStateWithLifecycle()
You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose for this.
As you can see the initial value true moved to the view model. This default value will be used until the data store provides its first value (which might take a moment because it needs to access the file system). So even when the button was clicked and isFirstTime is set to false, on app startup you will for a very brief moment still see the FirstTimeContent instead of NonFirstTimeContent. If this isn't what you want you can change the default value to whatever you want so the compose code can decide what to display. You can even set it to null and, in compose, display a CircularProgressIndicator() or something else for this case to indicate a loading state.
The view model provides the possibility with setFirstTime to set the value to true and false. This probably should only allow it to be set to false, though, because when the first time passed it will never come back. The method should then be changed to this:
fun onFirstTime() {
viewModelScope.launch {
dataStore.setFirstTime(false)
}
}
In your composables you shouldn't pass view model instances around. Only pass values and callbacks to a composable (also no Flows or States, which you don't do, but just to be clear; only their values should be passed). FirstTimeContent should look like this:
private fun FirstTimeContent(onFirstTime: () -> Unit) {
// ...
Button(onClick = onFirstTime) {
// ...
}
MyApp can then call it like this:
FirstTimeContent(viewModel::onFirstTime)
Put together (without the dependency injection recommendation in 6.) it will look like this:
MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp(viewModel = viewModel())
}
}
}
@Composable
private fun MyApp(viewModel: FirstTimeViewModel) {
val isFirstTime by viewModel.isFirstTime.collectAsStateWithLifecycle()
Surface(color = MaterialTheme.colorScheme.onPrimary) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
if (isFirstTime) {
FirstTimeContent(viewModel::onFirstTime)
} else {
NonFirstTimeContent()
}
}
}
}
@Composable
private fun FirstTimeContent(onFirstTime: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "First Time Launch")
Button(onClick = onFirstTime) {
Text(text = "Mark as Not First Time")
}
}
}
@Composable
private fun NonFirstTimeContent() {
Text(text = "Non First Time Launch")
}
FirstTimeViewModel
class FirstTimeViewModel(application: Application) : AndroidViewModel(application) {
private val dataStore = SettingsDataStoreManager(application)
val isFirstTime: StateFlow<Boolean> = dataStore.isFirstTimeLaunch()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = true,
)
fun onFirstTime() {
viewModelScope.launch {
dataStore.setFirstTime(false)
}
}
}
SettingsDataStoreManager.kt
// ...
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class SettingsDataStoreManager(context: Context) {
private val dataStore = context.dataStore
suspend fun setFirstTime(isFirstTime: Boolean) {
dataStore.edit { preferences ->
preferences[isFirstTimeLaunchKey] = isFirstTime
}
}
fun isFirstTimeLaunch(): Flow<Boolean> =
dataStore.data.map { pref ->
pref[isFirstTimeLaunchKey] ?: true
}
companion object {
private val isFirstTimeLaunchKey = booleanPreferencesKey("isFirstTime")
}
}