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.
Related
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 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 am new to dagger hilt (DI), And I Implement dagger hilt (DI) in my project. Now I am trying to inject the ViewModel. It works fine. But in the API result, I am using mutableLivedata for updating value from the view model to view with the observer. The observer listens and fetches the value works fine. But I read the observed data value; it cleared the value. I don't know why it happened. Can anyone help me to find this?
Login Fragment
#AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(FragmentLoginBinding::inflate) {
private val viewModel by viewModels<LoginViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewActionListeners()
setUpViewModelObserver()
}
private fun setUpViewModelObserver() {
viewModel.users.observe(viewLifecycleOwner) { response ->
AppLog.e("validateResponse", response.toString())
when (response.status) {
Status.SUCCESS -> {
binding.frmLayoutProgress.visible(false)
response.data?.let { users -> AppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }
}
Status.LOADING -> {
binding.frmLayoutProgress.visible(true)
}
Status.ERROR -> {
//Handle Error
binding.frmLayoutProgress.visible(false)
AppLog.e("Error:", response.message.toString())
}
}
}
}
}
If you comment the lineAppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }it return the value if uncomment response returns set to be null.
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val repository: LoginRepository,
private val networkHelper: NetworkHelper
) : ViewModel() {
val users: MutableLiveData<Resource<ResponseValidateUser>> = MutableLiveData()
fun loadValidateUser(email: String) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
if (networkHelper.isNetworkConnected()) {
repository.getValidateUser(email).let {
if (it.isSuccessful) {
users.postValue(Resource.success(it.body()))
} else users.postValue(Resource.error(it.errorBody().toString(), null))
}
} else users.postValue(Resource.error("No internet connection", null))
}
}
}
LoginRepository
class LoginRepository #Inject constructor(private val apiHelper: AuthApiHelper) {
suspend fun getValidateUser(email: String) = apiHelper.getValidateUser(email)
}
AuthApiHelper
interface AuthApiHelper {
suspend fun getValidateUser(email: String): Response<ResponseValidateUser>
}
ResponseValidateUser
data class ResponseValidateUser(
#SerializedName("clientResponse") val clientResponse: ClientResponse,
#SerializedName("status") val status: String,
#SerializedName("errorMessage") val errorMessage: String
)
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)
}
}
I have a LoginActivity, where ViewModel is injected using dagger. LoginActivity calls an API through ViewModel upon click of a button. Meanwhile, if the screen rotates, it triggers onDestroy of LoginActivity and there, I dispose that API call. After this, in onCreate(), new instance of ViewModel is injected and because of this, my state is lost & I need to tap again in order to make API call.
Here's my LoginActivity:
class LoginActivity : AppCompatActivity() {
#Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
val loginBinding = DataBindingUtil.setContentView<LoginBindings>(this, R.layout.activity_login)
loginBinding.loginVm = loginViewModel
loginBinding.executePendingBindings()
}
override fun onDestroy() {
super.onDestroy()
loginViewModel.onDestroy()
}
}
Here's my LoginViewModel:
class LoginViewModel(private val validator: Validator,
private val resourceProvider: ResourceProvider,
private val authenticationDataModel: AuthenticationDataModel) : BaseViewModel() {
val userName = ObservableField("")
val password = ObservableField("")
val userNameError = ObservableField("")
val passwordError = ObservableField("")
fun onLoginTapped() {
// Validations
if (!validator.isValidUsername(userName.get())) {
userNameError.set(resourceProvider.getString(R.string.invalid_username_error))
return
}
if (!validator.isValidPassword(password.get())) {
passwordError.set(resourceProvider.getString(R.string.invalid_password_error))
return
}
val loginRequest = LoginRequest(userName.get(), password.get())
addToDisposable(authenticationDataModel.loginUser(loginRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { isApiCallInProgress.set(true) }
.doOnDispose {
LogManager.e("LoginViewModel", "I am disposed, save me!!")
isApiCallInProgress.set(false) }
.subscribe({ user ->
isApiCallInProgress.set(false)
LogManager.e("LoginViewModel", user.name)
}, { error ->
isApiCallInProgress.set(false)
error.printStackTrace()
}))
}
}
My BaseViewModel:
open class BaseViewModel {
private val disposables = CompositeDisposable()
val isApiCallInProgress = ObservableBoolean(false)
fun addToDisposable(disposable: Disposable) {
disposables.add(disposable)
}
fun onDestroy() {
disposables.clear()
}
}
Here's the module which provides the ViewModel:
#Module
class AuthenticationModule {
#Provides
fun provideLoginViewModel(validator: Validator, resourceProvider: ResourceProvider,
authenticationDataModel: AuthenticationDataModel): LoginViewModel {
return LoginViewModel(validator, resourceProvider, authenticationDataModel)
}
#Provides
fun provideAuthenticationRepo(): IAuthenticationRepo {
return AuthRepoApiImpl()
}
}
How can I retain my ViewModel through orientation changes. (Note: I am NOT using Architecture Components' ViewModel). Should I make my ViewModel Singleton? Or is there any other way of doing it?