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()
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 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'
I'm trying to use the new liveData builder referenced here to retrieve my data, then transform it into view models. However, my repository code isn't being invoked (at least I'm not able to see it being triggered when I use my debugger). Am I not supposed to use two liveData{ ... } builders? (one in my repository, one in my view model)?
class MyRepository #Inject constructor() : Repository {
override fun getMyContentLiveData(params: MyParams): LiveData<MyContent> =
liveData {
val myContent = networkRequest(params) // send network request with params
emit(myContent)
}
}
class MyViewModel #Inject constructor(
private val repository: MyRepository
) : ViewModel() {
val viewModelList = liveData(Dispatchers.IO) {
val contentLiveData = repository.getContentLiveData(keyParams)
val viewModelLiveData = contentToViewModels(contentLiveData)
emit(viewModelLiveData)
}
private fun contentToViewModels(contentLiveData: LiveData<MyContent>): LiveData<List<ViewModel>> {
return Transformations.map(contentLiveData) { content ->
//perform some transformation and return List<ViewModel>
}
}
}
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val myViewModel: MyViewModel by lazy {
ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
}
lateinit var params: MyParams
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
myViewModel.params = params
myViewModel.viewModelList.observe(this, Observer {
onListChanged(it)
})
}
You could try with the emitSource:
val viewModelList = liveData(Dispatchers.IO) {
emitSource(
repository.getContentLiveData(keyParams).map {
contentToViewModels(it)
}
}
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.
I want to write a unitTest for my viewModel class :
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Mock
private lateinit var context: Application
#Mock
private lateinit var api: SuperHeroApi
#Mock
private lateinit var dao: HeroDao
private lateinit var repository: SuperHeroRepository
private lateinit var viewModel: MainViewModel
private lateinit var heroes: List<Hero>
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val localDataSource = SuperHeroLocalDataSource(dao)
val remoteDataSource = SuperHeroRemoteDataSource(context, api)
repository = SuperHeroRepository(localDataSource, remoteDataSource)
viewModel = MainViewModel(repository)
heroes = mutableListOf(
Hero(
1, "Batman",
Powerstats("1", "2", "3", "4", "5"),
Biography("Ali", "Tehran", "first"),
Appearance("male", "Iranian", arrayOf("1.78cm"), arrayOf("84kg"), "black", "black"),
Work("Android", "-"),
Image("url")
)
)
}
#Test
fun loadHeroes() = runBlocking {
`when`(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
with(viewModel) {
showHeroes(anyString())
assertFalse(dataLoading.value!!)
assertFalse(isLoadingError.value!!)
assertTrue(errorMsg.value!!.isEmpty())
assertFalse(getHeroes().isEmpty())
assertTrue(getHeroes().size == 1)
}
}
}
I receive following Exception :
java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at |b|b|b(Coroutine boundary.|b(|b)
at com.sample.android.superhero.data.source.SuperHeroRepository.getHeroes(SuperHeroRepository.kt:21)
at com.sample.android.superhero.MainViewModelTest$loadHeroes$1.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
And here is my RemoteDataSource class :
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(Dispatchers.IO) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When we use Rxjava we can create an Observable as simple as :
val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)
How about Deferred in Coroutines? How can I test my method :
fun searchHero(#Path("name") name: String): Deferred<Response<HeroWrapper>>
The best way I've found to do this is to inject a CoroutineContextProvider and provide a TestCoroutineContext in test. My Provider interface looks like this:
interface CoroutineContextProvider {
val io: CoroutineContext
val ui: CoroutineContext
}
The actual implementation looks something like this:
class AppCoroutineContextProvider: CoroutineContextProvider {
override val io = Dispatchers.IO
override val ui = Dispatchers.Main
}
And a test implementation would look something like this:
class TestCoroutineContextProvider: CoroutineContextProvider {
val testContext = TestCoroutineContext()
override val io: CoroutineContext = testContext
override val ui: CoroutineContext = testContext
}
So your SuperHeroRemoteDataSource becomes:
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val coroutineContextProvider: CoroutineContextProvider,
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(coroutineContextProvider.io) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When you inject the TestCoroutineContextProvider you can then call methods such as triggerActions() and advanceTimeBy(long, TimeUnit) on the testContext so your test would look something like:
#Test
fun `test action`() {
val repository = SuperHeroRemoteDataSource(testCoroutineContextProvider, context, api)
runBlocking {
when(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
}
// NOTE: you should inject the coroutineContext into your ViewModel as well
viewModel.getHeroes(anyString())
testCoroutineContextProvider.testContext.triggerActions()
// Do assertions etc
}
Note you should inject the coroutine context provider into your ViewModel as well. Also TestCoroutineContext() has an ObsoleteCoroutinesApi warning on it as it will be refactored as part of the structured concurrency update, but as of right now there is no change or new way of doing this, see this issue on GitHub for reference.