I'm building a Kotlin app that should be able to connect to Firebase Auth then store locally (with DataStore library) the state of the previous connection request (was it succesful or not). With this method, I want to allow the app to remember if the user was connected as Admin or not.
For that, I have implemented a DataStoreReposity
class, using the documention, that allow me to read and write an unique boolean value (isAdmin) in the datastore file:
//name of preferences file that will be stored that persist even if app stops
const val PREFERENCE_PRIVILEGE = "my_preference"
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = PREFERENCE_PRIVILEGE
)
class DataStoreRepository(private val context: Context) {
private object PreferenceKeys {
val adminKey = booleanPreferencesKey("isAdmin")
}
suspend fun saveToDataStore(isAdminPreference: Boolean) {
context.dataStore.edit { settings ->
settings[PreferenceKeys.adminKey] = isAdminPreference
}
}
val readFromDataStore: Flow<Boolean> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
exception.message?.let { stringMessage ->
Log.d("DataStore", stringMessage)
}
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val adminPrivilege = preferences[PreferenceKeys.adminKey] ?: false
adminPrivilege
}
}
Then, I use a MainActivityViewModel
class to manage the logic with authentication and acces to DataStore:
class MainActivityViewModel(
application: Application,
private var firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance(),
private val repository: DataStoreRepository = DataStoreRepository(application)
): AndroidViewModel(application) {
// liveData to manage Admin mode
var isAdmin = repository.readFromDataStore.asLiveData()
private fun saveToDataStore(adminPrivilege: Boolean) = viewModelScope.launch(Dispatchers.IO){
repository.saveToDataStore(adminPrivilege)
}
fun authenticate(){
firebaseAuth.signInWithEmailAndPassword("xxx@gmail.com", "xxx")
.addOnCompleteListener { task ->
if (task.isSuccessful) {
saveToDataStore(adminPrivilege = true)
Log.d("Authentication", "signInWithCustomToken: success")
} else {
saveToDataStore(adminPrivilege = false)
Log.d("Authentication", "signInWithCustomToken: Failed")
}
}
}
fun signOut(){
saveToDataStore(adminPrivilege = false)
firebaseAuth.signOut()
Log.d("User disconnected", "admin value is ${isAdmin.value}")
}
}
You should also be aware that this MainActivityViewModel
is a shared ViewModel. I want to use it inside my HomeScreen
and inside my MusicsScreen
. So I used CompositionLocalProvider
according to this post to share the same instance of my MainActicityViewModel across my navigation graph:
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val window = rememberWindowSizeClass()
KaraokeAppTheme(window) {
navController = rememberNavController()
SetupNavGraph(navController = navController)
}
}
}
}
@Composable
fun SetupNavGraph(
navController: NavHostController,
){
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
NavHost(
navController = navController,
startDestination = Screen.Home.route
){
composable(route = Screen.Home.route){
HomeScreen (
viewModel(viewModelStoreOwner = viewModelStoreOwner),
{ navController.navigate(route = Screen.Musics.route) },
{ navController.navigate(route = Screen.About.route) }
)
}
composable(route = Screen.Musics.route){
CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) {
MusicsScreen {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
}
}
}
composable(route = Screen.About.route){
AboutScreen{
navController.navigate(Screen.Home.route){
popUpTo(Screen.Home.route){ inclusive = true }
}
}
}
}
}
When running the app I'm getting this message error :
FATAL EXCEPTION: main
Process: com.example.karaokeapp, PID: 6208
java.lang.RuntimeException: Cannot create an instance of class com.example.karaokeapp.MainActivityViewModel
...
...
...
Caused by: java.lang.NoSuchMethodException: com.example.karaokeapp.MainActivityViewModel.<init> [class android.app.Application]
at java.lang.Class.getConstructor0(Class.java:2363)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:314)
Why can I not create an instance of MainActivityViewModel
?
The default ViewModelProvider.Factory can only handle certain specific constructors, as explained in this answer.
Your constructor has two extra arguments that cannot be handled by the default factory. Yes, you have provided default arguments, but since the code that instantiates your class is in Java, it cannot see those as optional arguments that have defaults. You need to make the compiler generate a Java constructor that takes only the Application argument, which you can do by using @JvmOverloads
like this:
class MainActivityViewModel @JvmOverloads constructor(
application: Application,
private var firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance(),
private val repository: DataStoreRepository = DataStoreRepository(application)
): AndroidViewModel(application) {