0

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?

Tessan
  • 49
  • 1
  • 9

1 Answers1

1

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) {
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Ok it is working now. So basically, you override ViewModelProvider.Factory with @JvmOverloads constructor, but I do not call ViewModelProvider.Factory in my code when instantiating MainActivityViewModel so how does it works? Also, I've seen in your other answer that I could just declare my parameters outside the constructor, directly inside the class { } and it seems working too. Which method should I prioritise ? – Tessan Mar 23 '23 at 15:51
  • 1
    There is a default factory that is used when you don’t provide your own. It can handle only the constructor arguments as described in my linked answer. You can solve this either way. The JvmOverloads way makes it more flexible for unit testing. – Tenfour04 Mar 23 '23 at 16:15
  • I'll ask another question if you allow me. Is it possible to observe my liveData object isAdmin from a Composable like my HomeScreen and if yes how could I do? – Tessan Mar 24 '23 at 00:26
  • I found it myself! For everyone who want to know: look here https://developer.android.com/jetpack/compose/state and the function observeAsState() – Tessan Mar 24 '23 at 14:51
  • Yes, sorry I forgot to come back and answer you. It's all explained in the documentation there. – Tenfour04 Mar 24 '23 at 14:51