I use hilt for the first in my dictionary app, but when I run my app, it crash and show this log:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: ir.arinateam.dictionary, PID: 23787
java.lang.RuntimeException: Cannot create an instance of class ir.arinateam.dictionary.feature_dictionary.presentation.WordInfoViewModel
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:204)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:322)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:304)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:175)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:138)
this is my module class:
`#Module
#InstallIn(SingletonComponent::class)
object WordInfoModule {
#Provides
#Singleton
fun provideGetWordInfoUseCase(repository: WordInfoRepository): GetWordInfo {
return GetWordInfo(repository)
}
#Provides
#Singleton
fun provideWordInfoDatabase(app: Application): WordInfoDatabase {
return Room.databaseBuilder(
app.applicationContext, WordInfoDatabase::class.java, "word_db"
).addTypeConverter(Converters(GsonParser(Gson())))
.build()
}
#Provides
#Singleton
fun provideWordInfoRepository(
db: WordInfoDatabase,
api: DictionaryApi
): WordInfoRepository {
return WordInfoRepositoryImpl(api, db.dao)
}
#Provides
#Singleton
fun provideDictionaryApi(): DictionaryApi {
return Retrofit.Builder()
.baseUrl(DictionaryApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DictionaryApi::class.java)
}
}`
And this is my mainActivity class that init viewmodel and try to use it:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var bindingActivity: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindingActivity = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel: WordInfoViewModel by viewModels {
SavedStateViewModelFactory(application, this)
}
lifecycleScope.launch {
viewModel.eventFlow.collectLatest { event ->
when (event) {
is WordInfoViewModel.UIEvent.ShowSnackbar -> {
Snackbar.make(bindingActivity.root, event.message, Snackbar.LENGTH_SHORT)
.show()
}
}
}
}
}
}
And this is my ViewModel:
#HiltViewModel
class WordInfoViewModel #Inject constructor(
private val getWordInfo: GetWordInfo
) : ViewModel() {
private val _searchQuery = mutableStateOf<String>("")
val searchQuery: State<String> = _searchQuery
private val _state = mutableStateOf<WordInfoState>(WordInfoState())
val state: State<WordInfoState> = _state
private val _eventFlow = MutableSharedFlow<UIEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var searchJob: Job? = null
fun onSearch(query: String) {
_searchQuery.value = query
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500L)
getWordInfo(query)
.onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
_eventFlow.emit(UIEvent.ShowSnackbar(result.message ?: "Unknown Error"))
}
is Resource.Loading -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = true
)
}
}
}.launchIn(this)
}
}
sealed class UIEvent {
data class ShowSnackbar(val message: String) : UIEvent()
}
}
where is the problem and how can i solve it?
In your activity, you can now just use KTX viewModels() directly.
val viewModel: WordInfoViewModel by viewModels {
SavedStateViewModelFactory(application, this)
}
replace that part to
private val viewModel: MyViewModel by viewModels()
Hilt lets you to inject SavedStateHandle into viewmodels, you could use it as:
#HiltViewModel
class WordInfoViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getWordInfo: GetWordInfo
) : ViewModel() {
...
}
And in your activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: WordInfoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { ... }
}
If you feel comfortable with the still alpha library:
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
Related
I am trying to get a list from Firestore with dagger hilt in android .I got data in FirestoreServiceImpl but when I call it from repository class or viewmodel I get null.Is there any problem with my hilt implementation ? How can I get data in repository ?
AppModule
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton
#Provides
fun provideFirebaseAuth(): FirebaseAuth = Firebase.auth
#Singleton
#Provides
fun provideFireStore(): FirebaseFirestore=FirebaseFirestore.getInstance()
#Singleton
#Provides
fun provideMainActivity(): MainActivity = MainActivity.getInstance() as MainActivity
}
ServiceModule
#Module
#InstallIn(SingletonComponent::class)
abstract class ServiceModule {
#Binds
abstract fun bindAccountService(
authServiceImpl: AuthServiceImpl
): AuthService
#Binds
abstract fun bindFirestoreService(
firestoreServiceImpl: FirestoreServiceImpl
): FirestoreService
}
FirestoreService
interface FirestoreService {
val responseState: MutableStateFlow<Response>
suspend fun getAllCurrentBanner():MutableLiveData<List<BannerModel>>
}
FirestoreServiceImpl
class FirestoreServiceImpl #Inject constructor(private val firestore: FirebaseFirestore) :
FirestoreService {
private val BANNER_COLLECTION = "BANNER"
override val responseState: MutableStateFlow<Response>
get() = MutableStateFlow(Response.NotInitialized)
override suspend fun getAllCurrentBanner(): MutableLiveData<List<BannerModel>> {
val mutableLiveData: MutableLiveData<List<BannerModel>> = MutableLiveData()
firestore.collection(BANNER_COLLECTION).whereEqualTo("isShow", true)
.addSnapshotListener { value, error ->
val a=value?.toObjects(BannerModel::class.java)
Log.e("TAG impl", "getAllCurrentBanner: ${a?.get(0)?.image}", )
mutableLiveData.postValue(value?.toObjects(BannerModel::class.java))
//getting data here
Log.e("TAGbanner", "getAllCurrentBanner: ${value?.isEmpty}", )
}
return mutableLiveData
}
}
FireStoreRepository
class FireStoreRepository #Inject constructor(private val firestoreService: FirestoreService){
fun responseState()=firestoreService.responseState
suspend fun getAllBanner(): MutableLiveData<List<BannerModel>> {
Log.e("TAG repo", "getAllBanner: ${firestoreService.getAllCurrentBanner().value}", )
//here I get null
return firestoreService.getAllCurrentBanner()
}
}
ViewModel
#HiltViewModel
class HomeScreenViewModel #Inject constructor(val repository: FireStoreRepository):ViewModel() {
// val state: MutableStateFlow<Response> = repository.responseState()
// private val _banners=MutableLiveData<List<BannerModel>>()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("TAG1", "Error : ${throwable.localizedMessage}")
}
fun getAllBanner(): MutableLiveData<List<BannerModel>> {
val bannerLiveData = MutableLiveData<List<BannerModel>>()
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response=repository.getAllBanner()
withContext(Dispatchers.Main){
bannerLiveData.postValue(response.value)
// _banners.value=response.value
Log.e("TAGbannerAll", "getAllBanner: ${response.value}", )
//here I get null
}
}
return bannerLiveData
}
}
Model Class
class BannerModel(
val bannerId:String?="",
val image:String?="",
val isShow:Boolean?=false,
val title:String?="",
val shopId:String?="",
)
Home Screen
#Composable
fun HomeScreen(
navController: NavController,
homeScreenViewModel: HomeScreenViewModel = hiltViewModel()
) {
val bannerListState by homeScreenViewModel.getAllBanner().observeAsState()
val bannerlist = mutableListOf<BannerModel>()
bannerListState?.let { bannerlist.addAll(it) }
Log.e("TAGbanner", "HomeScreen: $bannerlist", )
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
HorizontalPagerBanner(bannerlist)
}
}
I followed this tutorial link
But I met a problem that "kotlin.UninitializedPropertyAccessException: lateinit property splashViewModel has not been initialized"
Here is my code
#Module
#InstallIn(SingletonComponent::class)
object MainModule {
#Provides
#Singleton
fun provideDataStoreRepository(
#ApplicationContext context: Context
) = DataStoreRepository(context = context)
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "on_boarding_pref")
class DataStoreRepository(context: Context) {
private object PreferencesKey {
val onBoardingKey = booleanPreferencesKey(name = "on_boarding_completed")
}
private val dataStore = context.dataStore
suspend fun saveOnBoardingState(completed: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKey.onBoardingKey] = completed
}
}
fun readOnBoardingState(): Flow<Boolean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false
onBoardingState
}
}
}
class SplashViewModel #Inject constructor(
private val repository: DataStoreRepository
) : ViewModel() {
private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
private val _startDestination: MutableState<String> = mutableStateOf(Screen.OnboardingFirstScreen.route)
val startDestination: State<String> = _startDestination
init {
viewModelScope.launch {
repository.readOnBoardingState().collect { completed ->
if (completed) {
_startDestination.value = Screen.MainScreen.route
} else {
_startDestination.value = Screen.OnboardingFirstScreen.route
}
}
_isLoading.value = false
}
}
}
And in my main activity
class MainActivity : ComponentActivity() {
#Inject
lateinit var splashViewModel: SplashViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen().setKeepOnScreenCondition {
!splashViewModel.isLoading.value
}
setContent{
BottomNavWithBadgesTheme {
val screen by splashViewModel.startDestination
....
}
}
It turned out MainModule object have never been used. Is that problem? I'm new to jetpack data store, I just followed it, so I don't know where is the problem and how to fix it. Thank you in advance.
Firstly, it's not about data store. It is about dependency injection. You are trying to get the data from viewmodel when it is not initialized.
To solve the problem:
Mark your viewmodel class with #HiltViewModel annotation
Remove lateinit var keyword and #Inject annotation from viewmodel in your MainActivity
Your viewmodel must be initialized in onCreate function like that:
viewModel: SplashViewModel = hiltViewModel()
I've been trying to instantiate and use a ViewModel inside two Composables, and I've created them a ViewModelFactory, but the project doesn't build for some reason, says Failed to instantiate a ViewModel. I tried to pass LocalContext.current as Application and also tried without passing that, with no success. I want to pass my MainViewModel to both composables and use ViewModelProvider at both of them.
#Composable
fun LoginScreen(navController: NavController){
val viewmodel: MainViewModel = viewModel(LocalContext.current as ComponentActivity)
#Composable
fun ListingScreen(){
val viewModel: MainViewModel = viewModel()
class MainViewModel(private val dataSource: Dao,
application: Application): ViewModel() {
val loginUser: MutableState<User?> = mutableStateOf(null)
private val lastLoginInfo: MutableState<LoginInfo?> = mutableStateOf(null)
lateinit var videoUrlList: List<String>
init {
viewModelScope.launch {
getLastLogin()
}
initializeVideoList()
}
private fun initializeVideoList(){
videoUrlList = listOf<String>("Some links here"
)
}
fun checkValidity(username: String, password: Int): Boolean{
viewModelScope.launch {
val userEntity = dataSource.getUserByLogin(username = username, password = password).collect {
loginUser.value = it
}
}
return loginUser.value != null
}
fun returnFilename(fileUrl: String): String {
return fileUrl.substringAfterLast("/")
}
fun addLogin(loginInfo: LoginInfo= LoginInfo()){
dataSource.addLogin(loginInfo)
}
private suspend fun getLastLogin(){
dataSource.getLastLogin().collect {
lastLoginInfo.value = it
}
}
#OptIn(ExperimentalTime::class)
private fun checkIfLoginRecent(): Boolean{
return convert((System.currentTimeMillis()-lastLoginInfo.value!!.loginEndTimeMilli)
.toDouble(),
DurationUnit.MILLISECONDS,
DurationUnit.MINUTES)< 5
}
}
class MainViewModelFactory(
private val dataSource: Dao,
private val application: Application
): ViewModelProvider.Factory{
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
After edit: Now the project builds but still gives the same error saying it couldn't instantiate viewmodel in composable.
class MainActivity : ComponentActivity() {
private lateinit var mainViewModel: MainViewModel
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mainViewModelFactory: MainViewModelFactory = MainViewModelFactory(Database.getInstance(this).Dao, application)
mainViewModel = ViewModelProvider(this, mainViewModelFactory).get(MainViewModel::class.java)
setContent {
MyAndroidApplicationTheme {
// A surface container using the 'background' color from the theme
navController= rememberNavController()
Navigation(mainViewModel)
}
}
}
}
Example Preview code:
#Preview
#Composable
fun LoginScreenPreview(){
val navController = rememberNavController()
val main: MainActivity = MainActivity()
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val dao = Database.getInstance(main.applicationContext).Dao
#Suppress("UNCHECKED_CAST")
return MainViewModel( dataSource = dao, main.application) as T
}
}
val viewModel: MainViewModel = viewModel(
factory =factory)
TextField(value ="can", onValueChange ={} )
LoginScreen(navController, viewmodel = viewModel)
}
Source code can be found at : https://github.com/AliRezaeiii/MVI-Architecture-Android-Beginners
I have following Unit test which is working fine :
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
#get:Rule
val rule: TestRule = InstantTaskExecutorRule()
#get:Rule
val coroutineScope = MainCoroutineScopeRule()
#Mock
lateinit var apiService: ApiService
#Mock
private lateinit var observer: Observer<MainState>
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runBlockingTest {
`when`(apiService.getUsers()).thenReturn(emptyList())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Users(emptyList()))
}
#Test
fun givenServerResponseError_whenFetch_shouldReturnError() {
runBlockingTest {
`when`(apiService.getUsers()).thenThrow(RuntimeException())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Error(null))
}
}
The idea of unit test for stateFlow is taken from alternative solution in this question : Unit test the new Kotlin coroutine StateFlow
This is my ViewModel class :
#ExperimentalCoroutinesApi
class MainViewModel(
private val repository: MainRepository,
private val contextProvider: ContextProvider
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch(contextProvider.io) {
userIntent.send(MainIntent.FetchUser)
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch(contextProvider.io) {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
As you see when fetchUser() is called, _state.value = MainState.Loading will be executed at start. As a result in unit test I expect following as well in advance :
verify(observer).onChanged(MainState.Loading)
Why unit test is passing without Loading state?
Here is my sealed class :
sealed class MainState {
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()
}
And here is how I observe it in MainActivity :
private fun observeViewModel() {
lifecycleScope.launch {
mainViewModel.state.collect {
when (it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
is MainState.Users -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this#MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}
Addendda: If I call userIntent.send(MainIntent.FetchUser) method after viewModel.state.asLiveData().observeForever(observer) instead of init block of ViewModel, Idle and Loading states will be verified as expected by Mockito.
MainActivity
class MainActivity : AppCompatActivity() {
#Inject
lateinit var mainViewModelFactory: mainViewModelFactory
private lateinit var mainActivityBinding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
)
mainActivityBinding.rvmainRepos.adapter = mainAdapter
AndroidInjection.inject(this)
mainViewModel =
ViewModelProviders.of(
this#MainActivity,
mainViewModelFactory
)[mainViewModel::class.java]
mainActivityBinding.viewmodel = mainViewModel
mainActivityBinding.lifecycleOwner = this
mainViewModel.mainRepoReponse.observe(this, Observer<Response> {
repoList.clear()
it.success?.let { response ->
if (!response.isEmpty()) {
// mainViewModel.saveDataToDb(response)
// mainViewModel.createWorkerForClearingDb()
}
}
})
}
}
MainViewModelFactory
class MainViewModelFactory #Inject constructor(
val mainRepository: mainRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>) =
with(modelClass) {
when {
isAssignableFrom(mainViewModel::class.java) -> mainViewModel(
mainRepository = mainRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
MainViewModel
class MainViewModel(
val mainRepository: mainRepository
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
val mainRepoReponse = MutableLiveData<Response>()
val loadingProgress: MutableLiveData<Boolean> = MutableLiveData()
val _loadingProgress: LiveData<Boolean> = loadingProgress
val loadingFailed: MutableLiveData<Boolean> = MutableLiveData()
val _loadingFailed: LiveData<Boolean> = loadingFailed
var isConnected: Boolean = false
fun fetchmainRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
mainRepository.getmainRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
saveDataToDb(response)
)
}
},
{ error ->
processResponse(Response(AppConstants.Status.SUCCESS, null, error))
}
)
)
} else {
fetchFromLocal()
}
}
private fun saveDataToDb(response: List<mainRepo>) {
mainRepository.insertmainUsers(response)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(object : DisposableCompletableObserver() {
override fun onComplete() {
Log.d("Status", "Save Success")
}
override fun onError(e: Throwable) {
Log.d("Status", "error ${e.localizedMessage}")
}
})
}
}
MainRepository
interface MainRepository {
fun getmainRepos(): Single<List<mainRepo>>
fun getAllLocalRecords(): Single<List<mainRepo>>
fun insertmainUsers(repoList: List<mainRepo>): Completable
}
MainRepositoryImpl
class mainRepositoryImpl #Inject constructor(
val apiService: GitHubApi,
val mainDao: AppDao
) : MainRepository {
override fun getAllLocalRecords(): Single<List<mainRepo>> = mainDao.getAllRepos()
override fun insertmainUsers(repoList: List<mainRepo>) :Completable{
return mainDao.insertAllRepos(repoList)
}
override fun getmainRepos(): Single<List<mainRepo>> {
return apiService.getmainGits()
}
}
I'm quite confused with the implementation of MVVM with LiveData and Rxjava, in my MainViewModel I am calling the interface method and implementing it inside ViewModel, also on the response I'm saving the response to db. However, that is a private method, which won't be testable in unit testing in a proper way (because it's private). What is the best practice to call other methods on the completion of one method or i have to implement all the methods inside the implementation class which uses the interface.
Your ViewModel should not care how you are getting the data if you are trying to follow the clean architecture pattern. The logic for fetching the data from local or remote sources should be in the repository in the worst case where you can also save the response. In that case, since you have a contact for the methods, you can easily test them. Ideally, you could break it down even more - adding Usecases/Interactors.