I persist time state in viewModel and need to store current state in preferences and load time state again when app is closed and opened again by user. Here is my current code.
ViewModel
class TimeViewModel(): ViewModel(){
private val _time = MutableLiveData<Long>()
val time: LiveData<Long> = _time
fun onTimeChange(newTime: Long) {
_time.value = newTime
}
}
Composable function
#Composable
fun Timer(timeViewModel:TimeViewModel = viewModel()){
LaunchedEffect(key1 = time ){
delay(1000L)
timeViewModel.onTimeChange(time + 1)
}
val time: Long by timeViewModel.time.observeAsState(0L)
val dec = DecimalFormat("00")
val min = time / 60
val sec = time % 60
Text(
text = dec.format(min) + ":" + dec.format(sec),
style = MaterialTheme.typography.body1
)
}
Try using dagger for dependency injection you could create a singleton with your store this way:
#Module
#InstallIn(SingletonComponent::class)
object MyModule {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
#Singleton
#Provides
fun provideDataStore(#ApplicationContext app: Context ) : DataStore<Preferences> = app.dataStore
}
then just inject in your viewModels and use it!
#HiltViewModel
class HomeViewModel #Inject constructor(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
private val myKey = stringPreferencesKey("USER_KEY")// init your key identifier here
fun mySuperCoolWrite(){
viewModelScope.launch {
dataStore.edit {
it[myKey] = body.auth
}
}
}
fun mySuperCoolRead(){
viewModelScope.launch {
val preferences = dataStore.data.first()
preferences[myKey]?.let {
// here access your stored value with "it"
}
}
}
}
Or just inject in your controller constructor with
#ApplicationContext app: Context
here you can find more info
Related
I am following this tutorial to write & read int:
https://betterprogramming.pub/using-jetpack-preferences-datastore-more-effectively-414e1126cff7
However, when using context.writeInt in viewmodel, I get unresolved reference error.
DataStoreHelper.kt
//Preference Name
const val PREFERENCE_NAME = "MyDataStore"
class DataStoreHelper {
//Instance of DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCE_NAME)
/**
* Add Integer to the data store
*/
suspend fun Context.writeInt(key: String, value: Int) {
dataStore.edit { pref -> pref[intPreferencesKey(key)] = value }
}
/**
* Reading the Int value from the data store
*/
fun Context.readInt(key: String): Flow<Int> {
return dataStore.data.map { pref ->
pref[intPreferencesKey(key)] ?: 0
}
}
}
WeeklyPlannerViewModel.kt
#HiltViewModel
class WeeklyPlannerViewModel #Inject constructor(
#ApplicationContext val appContext: Application
): ViewModel() {
companion object{
const val NAME_USER_KEY = "name"
}
val dataStoreHelper = DataStoreHelper()
fun saveRecipeId(id: Int) {
viewModelScope.launch(Dispatchers.IO) {
appContext.writeInt(NAME_USER_KEY, id)
}
}
Usign DataStoreHelper() to access the function, but that did not work either.
I am getting this error. I am using Koin for dependency injection.
I want my appointment repository to be alive as UserInfoContainer scope is alive.
No definition found for class:'com.flow.domain.repository.AppointmentsRepository'. Check your definitions!
UserInfoContainer class
class UserInfoContainer(private val encryptedLocalDatabase: EncryptedLocalDatabase) :
KoinScopeComponent {
override val scope: Scope get() = getOrCreateScope().value
var user: User?
get() = encryptedLocalDatabase.user
set(it) {
if (it != encryptedLocalDatabase.user) {
encryptedLocalDatabase.user = it
scope.close()
}
}
}
Koin file
single { UserInfoContainer(encryptedLocalDatabase = get()) }
scope<UserInfoContainer> {
scoped<AppointmentsRepository> {
AppointmentsRepositoryImplementation(
apiService = get(),
clinicId = get<UserInfoContainer>().user.let { it!!.clinicId }
)
}
}
AppointmentsUseCase class
class AppointmentsUseCase : KoinComponent {
private val appointmentsRepository: AppointmentsRepository by inject()
suspend fun getAppointments(startDate: LocalDateTime, endDate: LocalDateTime): List<Appointment> =
appointmentsRepository.getAppointments(startDate, endDate)
}
That is how I should inject my dependencies in the UseCase
class AppointmentsUseCase : KoinComponent {
private val userInfoContainer: UserInfoContainer by inject()
private val appointmentsRepository: AppointmentsRepository = userInfoContainer.scope.get()
suspend fun getAppointments(
startDate: LocalDateTime,
endDate: LocalDateTime
): List<Appointment> =
appointmentsRepository.getAppointments(startDate, endDate)
}
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
Good day all, am trying to test my ViewModel class and it has a dependency of datasource, I tried to mock this, but it won't work because it's an interface, I believe the interface implementation is generated at runtime, how do I unit test this class, below is my ViewModel class
class LoginViewModel #ViewModelInject constructor(#ApplicationContext private val context: Context,
private val networkApi: NetworkAPI,
private val dataStore: DataStore<Preferences>)
: ViewModel() {
val clientNumber = MutableLiveData<String>()
val clientPassword = MutableLiveData<String>()
private val _shouldNavigate = MutableLiveData(false)
val shouldNavigate: LiveData<Boolean>
get() = _shouldNavigate
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
private val _activateDeviceButton = MutableLiveData(false)
val activateButton : LiveData<Boolean>
get() = _activateDeviceButton
init {
populateApiWithFakeData()
}
suspend fun authenticateUsers(): Boolean {
val clientNumber = clientNumber.value
val clientPassword = clientPassword.value
requireNotNull(clientNumber)
requireNotNull(clientPassword)
val (userExist, token) = networkApi.doesUserExist(clientNumber.toLong(), clientPassword)
if (token.isNotBlank()) storeTokenInStore(token)
return if (userExist) {
true
} else {
_errorMessage.value = "Incorrect account details. Please try again with correct details"
false
}
}
private suspend fun storeTokenInStore(token: String) {
dataStore.edit { pref ->
pref[TOKEN_PREFERENCE] = token
}
}
and here is my ViewModel Test class
#Config(sdk = [Build.VERSION_CODES.O_MR1])
#RunWith(AndroidJUnit4::class)
class LoginViewModelTest{
private val context : Context = ApplicationProvider.getApplicationContext()
private val dataCentre = NetworkApImpl()
#Mock
private lateinit var dataStore: DataStore<Preferences>
#Before
fun setUpDataCenters(){
val loginData = DataFactory.generateLoginData()
for (data in loginData){
dataCentre.saveUserData(data)
}
}
#After
fun tearDownDataCenter(){
dataCentre.clearDataSet()
}
#Test
#ExperimentalCoroutinesApi
fun authenticateUser_shouldAuthenticateUsers(){
//Given
val viewModel = LoginViewModel(context, dataCentre, dataStore)
viewModel.clientNumber.value = "8055675745"
viewModel.clientPassword.value = "robin"
//When
var result : Boolean? = null
runBlocking {
result = viewModel.authenticateUsers()
}
//Then
Truth.assertThat(result).isTrue()
}
Any assistance rendered will be appreciated.
You can wrap your dependency in a class you own as Mockito suggests here. This also has the upside of letting you change your storage implementation latter without having and impact on every view model using it.
I create application based on the Database + Network paging and GitHub rest api.
Using various tutorials, I came to the conclusion that when creating the LivePagedListBuilder in ViewModel, I must pass my query retrieving data from Room, to make it works then with BoundaryCallback.
This query in my code looks like this:
#Query("SELECT * from repositories_table ORDER BY name DESC")
fun getPagedRepos(): DataSource.Factory<Int,Repository>
and its equivalent in the repository:
fun getPagedRepos(): DataSource.Factory<Int, Repository> {
return repositoriesDao.getPagedRepos()
}
However I would like to combine this with my own DataSource, not default one, which would also work with retrofitting data fetching.
Below are the relevant parts of my application:
DataSource
class ReposDataSource(private val contactsRepository: ContactsRepository,
private val scope: CoroutineScope, application: Application): PageKeyedDataSource<Int, Repository>() {
private var supervisorJob = SupervisorJob()
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Repository>
) {
Log.i("RepoBoundaryCallback", "initialTriggered")
val currentPage = 1
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, null, nextPage)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Repository>) {
val currentPage = params.key
val nextPage = currentPage + 1
executeQuery(currentPage, params.requestedLoadSize) {
callback.onResult(it, nextPage)
}
}
override fun invalidate() {
super.invalidate()
supervisorJob.cancelChildren()
}
private fun executeQuery(page: Int, perPage: Int, callback: (List<Repository>) -> Unit) {
scope.launch(getJobErrorHandler() + supervisorJob) {
savePage("current_page", page)
val repos = contactsRepository.fetchPagedRepos(page, perPage)
callback(repos)
}
}
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
Log.e(ReposDataSource::class.java.simpleName, "An error happened: $e")
}
private fun savePage(KEY_NAME: String, value: Int){
Log.i("RepoBoundaryCallback", value.toString())
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putInt(KEY_NAME, value)
editor.commit()
}
}
BoundaryCallback
class RepoBoundaryCallback (val repository: ContactsRepository, application: Application) :
PagedList.BoundaryCallback<Repository?>() {
private var callbackJob = Job()
private val coroutineScope = CoroutineScope(
callbackJob + Dispatchers.Main )
private val PREFS_NAME = "Paging"
private val sharedPref: SharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
override fun onZeroItemsLoaded() {
Log.i("RepoBoundaryCallback", "onzeroitemstriggered")
super.onZeroItemsLoaded()
fetchUsers(1)
}
override fun onItemAtEndLoaded(itemAtEnd: Repository) {
Log.i("RepoBoundaryCallback", "onitematendriggered")
super.onItemAtEndLoaded(itemAtEnd)
fetchUsers(getCurrentPage("current_page"))
}
private fun fetchUsers(page: Int) {
coroutineScope.launch {
try {
var newRepos = RepoApi.retrofitService.fetchRepos(page)
insertRepoToDb(newRepos)
}
catch (e: Exception){
Log.i("RepoBoundaryCallback", e.toString())
}
}
}
private suspend fun insertRepoToDb(reposList: List<Repository>){
reposList.forEach{repository.insertRepo(it)}
}
private fun getCurrentPage(KEY_NAME: String): Int{
return sharedPref.getInt(KEY_NAME, 0)
}
}
Api query
interface RepoApiService {
#GET("/orgs/google/repos")
suspend fun fetchRepos(#Query("page") page: Int,
#Query("per_page") perPage: Int = 15): List<Repository>
}
ViewModel
class RepositoryViewModel (application: Application) : AndroidViewModel(application) {
companion object{
private const val TAG = "RepositoryViewModel"
}
//var reposList: LiveData<PagedList<Repository>>
private var repoBoundaryCallback: RepoBoundaryCallback? = null
var reposList: LiveData<PagedList<Repository>>? = null
private val repository: ContactsRepository
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
init {
val contactsDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsDao()
val contactsExtrasDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).contactsExtrasDao()
val repositoriesDao = ContactsRoomDatabase.getDatabase(application, viewModelScope).repositoriesDao()
val service = RepoApi.retrofitService
repository = ContactsRepository(contactsDao, contactsExtrasDao, repositoriesDao, service)
initializedPagedListBuilder(application)
}
private fun initializedPagedListBuilder(application: Application) {
repoBoundaryCallback = RepoBoundaryCallback(
repository, application
)
val pagedListConfig = PagedList.Config.Builder()
//.setPrefetchDistance(5)
//.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
.setPageSize(15).build()
reposList = LivePagedListBuilder(
repository.getPagedRepos(),
pagedListConfig
).setBoundaryCallback(repoBoundaryCallback).build()
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
In addition, I save the relevant pages in SharedPreferences in the DataSource to then use it in the corresponding BoundaryCallback functions.
So how do you link your own DataSource to BoundaryCallback with Room and Retrofit? I will be grateful for any help.
BoundaryCallback is responsible for triggering invalidation on your current generation of DataSource. With DataSource.Factory generated by Room, this is automatically handled for you as Room will invalidate any DataSource it generates that is affected by writes to DB. This is why a DataSource.Factory is necessary over a single DataSource. Paging sees a single instance of DataSource as a "snapshot" of static data. If the data it's supposed to be loading changes in any way you must call DataSource.invalidate() to allow DataSource.Factory to generate a new up-to-date snapshot.
Since you're implementing your own DataSource, you'll also need to implement a DataSource.Factory and call invalidate() from your BoundaryCallback (doesn't necessarily need to be in the same class, but invalidate() must be triggered when your BoundaryCallback writes updates).