I am learning MVVM.
I added an Applicaation class like this codelab code.
Then a problem occurred that the data of the property in the Repository was continuously maintained.
Looking for the cause, the Application class is a Singleton object, so the members are also singletons.
Someone told me to match the lifecycle of the repository with the viewmodel as a solution.
But I don't know how.
Since i'm using MVVM pattern, we have Fragment, ViewModel and Repository.
Speaking of the flow of the app, i create a list on the screen and add an item by pressing a button.
And when the save button is pressed, it is saved to the DB and navigates to another screen.
Repeat this process.
How can I properly reset the List data in the Repository?
Application
class WorkoutApplication : Application() {
val database by lazy { WorkoutDatabase.getDatabase(this) }
val detailRepo: DetailRepository by lazy { DetailRepository(database.workoutDao()) }
}
Repository
class DetailRepository(private val workoutDao : WorkoutDao) {
private var setInfoList = ArrayList<WorkoutSetInfo>()
private lateinit var updatedList : List<WorkoutSetInfo>
fun add() {
setInfoList.let { list ->
val item = WorkoutSetInfo(set = setInfoList.size + 1)
list.add(item)
updatedList = setInfoList.toList()
}
}
fun delete() {
if(setInfoList.size != 0) {
setInfoList.let { list ->
list.removeLast()
updatedList = list.toList()
}
}
return
}
fun save(title: String) {
private val workout = Workout(title = title)
val workoutId = workoutDao.insertWorkout(workout)
val newWorkoutSetInfoList = setInfoList.map { setInfo ->
setInfo.copy(parentWorkoutId = workoutId)
}
workoutDao.insertSetInfoList(newWorkoutSetInfoList)
}
}
Related
I'm building a very simple game with Jetpack Compose where I have 3 screens:
HeroesScreen - where I display all heroes in the game. You can select one, or multiple of the same character.
HeroDetailsScreen - where I display more info about a hero. You can select a hero several times, if you want to have that character multiple times.
ShoppingCartScreen - where you increase/decrease the quantity for each character.
Each screen has a ViewModel, and a Repository class:
HeroesScreen -> HeroesViewModel -> HeroesRepository
HeroDetailsScreen -> HeroDetailsViewModel -> HeroDetailsRepository
ShoppingCartScreen -> ShoppingCartViewModel -> ShoppingCartRepository
Each repository has between 8-12 different API calls. However, two of them are present in each repo, which is increase/decrease quantity. So I have the same 2 functions in 3 repository and 3 view model classes. Is there any way I can avoid those duplicates?
I know I can add these 2 functions only in one repo, and then inject an instance of that repo in the other view models, but is this a good approach? Since ShoppingCartRepository is not somehow related to HeroDetailsViewModel.
Edit
All 3 view model and repo classes contain 8-12 functions, but I will share only what's common in all classes:
class ShoppingCartViewModel #Inject constructor(
private val repo: ShoppingCartRepository
): ViewModel() {
var incrementQuantityResult by mutableStateOf<Result<Boolean>>(false)
private set
var decrementQuantityResult by mutableStateOf<Result<Boolean>>(false)
private set
fun incrementQuantity(heroId: String) = viewModelScope.launch {
repo.incrementQuantity(heroId).collect { result ->
incrementQuantityResult = result
}
}
fun decrementQuantity(heroId: String) = viewModelScope.launch {
repo.decrementQuantity(heroId).collect { result ->
decrementQuantityResult = result
}
}
}
And here is the repo class:
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
val heroIdRef = db.collection("shoppingCart").document(heroId)
override fun incrementQuantity(heroId: String) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(1)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
override fun decrementQuantity(heroId: String) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(-1)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
}
All the other view model classes and repo classes contain their own logic, including these common functions.
I don't use Firebase, but going off of your code, I think you could do something like this.
You don't seem to be using the heroId parameter of your functions so I'm omitting that.
Here's a couple of different strategies for modularizing this:
For a general solution that can work with any Firebase field, you can make a class that wraps a DocumentReference and a particular field in it, and exposes functions to work with it. This is a form of composition.
class IncrementableField(
private val documentReference: DocumentReference,
val fieldName: String
) {
private fun increment(amount: Float) = flow {
try {
emit(Result.Loading)
heroIdRef.update(fieldName, FieldValue.increment(amount)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
fun increment() = increment(1)
fun decrement() = increment(-1)
}
Then your repo becomes:
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
val heroIdRef = db.collection("shoppingCart").document(heroId)
val quantity = IncrementableField(heroIdRef, "quantity")
}
and in your ViewModel, can call quantity.increment() or quantity.decrement().
If you want to be more specific to this quantity type, you could create an interface for it and use extension functions for the implementation. (I don't really like this kind of solution because it makes too much stuff public and possibly hard to test/mock.)
interface Quantifiable {
val documentReference: DocumentReference
}
fun Quantifiable.incrementQuantity()(amount: Float) = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(amount)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
fun Quantifiable.incrementQuantity() = incrementQuantity(1)
fun Quantifiable.decrementQuantity() = incrementQuantity(-1)
Then your Repository can extend this interface:
interface ShoppingCartRepository: Quantitfiable {
//... your existing definition of the interface
}
class ShoppingCartRepositoryImpl(
private val db: FirebaseFirestore,
): ShoppingCartRepository {
private val heroIdRef = db.collection("shoppingCart").document(heroId)
override val documentReference: DocumentReference get() = heroIdRef
}
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
I want to store some preferences using DataStore. But the problem is that my application can have multiple users and therefor needs to store these preferences in separate files. I got a working example using only one user but I'm struggling to support multiple users.
Here is an example of my code:
class DataStorageRepository(private val context: Context, private val userRepository: UserRepository) {
private object PreferencesKeys {
val SETTING_ONE = intPreferencesKey("setting_one")
}
// retrieve datastore for currently logged in user.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name)
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { preferences ->
val settingOne = preferences[PreferencesKeys.SETTING_ONE] ?: 0
UserPreferences(settingOne)
}
suspend fun storeSettingOne(settingOne: Int) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SETTING_ONE] = settingOne
}
}
data class UserPreferences(val lastUsedToAccountTab: Int)
}
I'm using Koin and I tried unloading the DataStorageRepository on logout and recreating it on login but the DataStore seems to stay alive until the app is killed and I get the following crash:
java.lang.IllegalStateException: There are multiple DataStores active
for the same file: [...] You should either maintain your DataStore as
a singleton or confirm that there is no two DataStore's active on the
same file (by confirming that the scope is cancelled).
I also tried to use a CoroutineScope and kill that when I log out, but after recreating the scope on login the DataStore doesn't seem to get recreated.
Does DataStore support a way to close the connection or to handle multiple files?
Put this line inside companion object { }
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
My Code
class SettingPrefs(private val context: Context) {
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
private val soundKey = booleanPreferencesKey("sound")
private val vibrateKey = booleanPreferencesKey("vibrate")
}
val getSound: Flow<Boolean>
get() = context.dataStore.data.map {
it[soundKey] ?: true
}
suspend fun setSound(value: Boolean) {
context.dataStore.edit { it[soundKey] = value }
}
val getVibration: Flow<Boolean>
get() = context.dataStore.data.map {
it[vibrateKey] ?: true
}
suspend fun setVibration(value: Boolean) {
context.dataStore.edit { it[vibrateKey] = value }
}
}
You can use different key for different user or manual keep DataStore singleton.
For exception:
java.lang.IllegalStateException: There are multiple DataStores active for the same file: [...] You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).
androidx.datastore:datastore-*:1.0.0-alpha07 is released.
Put this at the top level of your kotlin file so there is only one instance of it.
private val Context.dataStore by preferencesDataStore("settings")
class Xxx{
}
https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0-alpha07.
The Context.createDataStore extension function has been removed and replaced with globalDataStore property delegate. Call globalDataStore once at the top level in your kotlin file. For example:
val Context.myDataStore by dataStore(...)
Put this at the top level of your kotlin file so there is only one instance of it. (I57215, b/173726702)
At the moment I was posting this question I found a solution to this problem. In order to solve my problem I needed to combine my previous two solutions. So on logout I unload the DataStorageRepository and on login I reload it again. I also needed to create a CoroutineScope that I cancel on logout.
My Module
val loggedInModule = module {
single { DataStorageRepository(get(), get()) }
}
I created a scope and passed it to the DataStore
var loggedInScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name, scope = loggedInScope)
On Login
loggedInScope = CoroutineScope(Dispatchers.Default)
loadKoinModules(loggedInModule)
On Logout
loggedInScope.cancel()
unloadKoinModules(loggedInModule)
Just put your declaration datastore out of your DataStorageRepository class
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name="settings")
class DataStorageRepository(context: Context) {
private var appContext = context.applicationContext
val mData: Flow<String?> = appContext.dataStore.data.map { preferences ->
preferences[YOUR_KEY]
}
suspend fun insertData(value: String) {
appContext.dataStore.edit { preferences ->
preferences[YOUR_KEY] = authToken
}
}
companion object {
private val KEY = stringPreferencesKey("data")
}
}
This is what I'm using in my project:
private object UserIdBasedPrefDs {
val lock = Any()
#GuardedBy("lock")
#Volatile
var currentId: String = ""
#GuardedBy("lock")
#Volatile
var INSTANCE: DataStore<Preferences>? = null
}
fun Context.happyStore( // rename what ever you like.
userId: String,
// below 3 optional params are same as int the `preferencesDataStore`.
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
produceMigrations: (Context) -> List<DataMigration<Preferences>> =
{ listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> = UserIdBasedPrefDs.run {
// check if current user id has been changed.
if (userId != currentId && INSTANCE != null) {
synchronized(lock) {
// release previous saved.
INSTANCE = null
// reset new user id.
currentId = userId
}
}
// below is the same logic inside the `preferencesDataStore` delegate.
INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(userId)
}
}
INSTANCE!!
}
}
Hope it can be helpful to you.
I have two fragments.
One activity which is ideal, I use nav Host to navigate from 1st fragment to another. I do not have a database as of now. I make a call in the repository using retrofit which returns the result as OutPut which is sealed class, I got a result of API call into first fragment ViewModel and I can observe it in first fragment. Now how do I send that to Second fragment or second ViewModel. what is the best solution here? I do not want to implement the database. I do not want to make another call by creating a repository for the second ViewModel and call the same method. I also want to observe any changes in list which I can do by DiffUtil if am I correct? What is the best solution in this case? Below is my code. How can I send wordResponse live data in the second fragment adapter and also observe changes.
My Repository
class DictionaryRepository internal constructor(private val networkService: NetworkService) {
companion object {
#Volatile
private var dictionaryRepoInstance: DictionaryRepository? = null
fun getInstance(dictionaryService: NetworkService) =
dictionaryRepoInstance ?: synchronized(this) {
dictionaryRepoInstance
?: DictionaryRepository(dictionaryService).also { dictionaryRepoInstance = it }
}
}
/**
* Fetch a new searched word from the network
*/
suspend fun fetchRecentSearchedWord(term: CharSequence) = try {
val response = networkService.retrofitClient().makeCallForWordDefinition(term)
OutputResult.Success(response.list)
} catch (t: Throwable) {
OutputResult.Error(t)
}
}
MyViewModel
class SearchFragmentViewModel internal constructor(
private val dictionaryRepository: DictionaryRepository) : ViewModel() {
/** Show a loading spinner if true*/
private val _spinner = MutableLiveData(false)
val spinner: LiveData<Boolean> get() = _spinner
/**take the data into the result live data*/
private val _wordResponse = MutableLiveData<OutputResult>()
val wordResponse: LiveData<OutputResult> get() = _wordResponse
fun makeAPICallWithSuspendFunction(term: CharSequence) {
_spinner.value = true
viewModelScope.launch(Dispatchers.Main) {
val result = dictionaryRepository.fetchRecentSearchedWord(term)
_wordResponse.value = when (result) {
is OutputResult.Success -> {
OutputResult.Success(result.output)
}
is OutputResult.Error -> {
OutputResult.Error(result.throwable)
}
}
}
_spinner.value = false
}
}
In this Repository class there's only one public function called getMovies called directly from the useCase class .
Now the problem is i want this function to return moviesData multiple times from single call ,At first I want to fetch data locally from db and return it then fetch from remote and do some logic & after that logic ends i want to return also the final value how can i do this with coroutines .
The whole solution is in the return line in getMovies func .
Note : this can be done using liveData or rx but i don't wanna go with any of them as it's not good idea to pass livedata through all these layers ending to viewModel .
Here's the repo class :
#Singleton
class MovieRepository #Inject constructor(
private val movieDao: MovieDao,
private val movieApi: MovieApi,
private val trailerApi: TrailerApi) : BaseRepository() {
suspend fun getMovies(): ArrayList<Movie> {
val localData = fetchMoviesLocal()
val remoteData = fetchMoviesRemote()
val syncedData = storeMoviesLocal(remoteData)
return localData then syncedData
}
private fun fetchMoviesLocal(): ArrayList<Movie> = movieDao.fetchAllMovies()
private suspend fun fetchMoviesRemote(): ArrayList<Movie>? {
val data = safeApiCall({ movieApi.getMostPopular(AppConstants.API_KEY) },
"fetching movies")
return if (data != null) data.results as ArrayList<Movie> else null
}
private fun storeMoviesLocal(results: List<Movie>?): ArrayList<Movie>? {
return if (!results.isNullOrEmpty()) syncFavWithDb(results) else null
}
private fun syncFavWithDb(movies: List<Movie>): ArrayList<Movie> {
val tempList = ArrayList<Movie>()
movies.forEach { movie -> movie.isFav = if (isMovieLiked(movie.id)) 1 else 0; tempList.add(movie) }
movieDao.insertAll(tempList)
return tempList
}}
Kotlin 1.3 introduced the stable version of its own cold asynchronous streams - Flows:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun getMovies(): Flow<List<Movie>> = flow {
val localData = fetchMoviesLocal()
emit(localData)
val remoteData = fetchMoviesRemote()
val syncedData = storeMoviesLocal(remoteData)
emit(syncedData)
}
you can use any asynchronous and event-based library to do it.
You can use rx-java publish subject : subjects rx-java
val subject = PublishSubject();
fun getMovies(): Flow<List<Movie>> = flow {
val localData = fetchMoviesLocal()
emit(localData)
val remoteData = fetchMoviesRemote()
val syncedData = storeMoviesLocal(remoteData)
subject.onNext(syncedData)
}
you can subscribe to this subject where ever you want the data
you can use flows as already mentioned bu #jsamol
you can use live data as well.