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
}
Related
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)
}
}
The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Am trying to implement this way and its not compiling :
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<String>> by lazy {
return MutableLiveData().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<String>> {
return users
}
private fun loadUsers() {
users.postValue(listOf("Tarun", "Chawla"))
}
}
Mostly am not understanding the by lazy here. The example on android website seems wrong as loadUsers() is not returning anything which can be a delegate for
users. can you please help me understanding above piece of code.
=======================================================
This is how I implemented:
private val users : MutableLiveData<List<String>> by lazy {
MutableLiveData<List<String>>().also {
loadUsers(it)
}
}
init {
Log.e("Tarund", "View Model created")
}
override fun onCleared() {
super.onCleared()
Log.e("Tarund", "View Model deleted")
}
fun getUsers(): LiveData<List<String>> {
return users
}
private fun loadUsers(users : MutableLiveData<List<String>>) {
users.postValue(listOf("Tarun", "Chawla"))
}
}
But if anyone can confirm if first example code above which I copy pasted from : https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin is wrong
The code in the Android documentation is wrong.
The lazy construction itself is fine: loadUsers() doesn't need to return anything because the function also is defined as:
inline fun <T> T.also(block: (T) -> Unit): T
that means that in here:
private val sources: String by lazy {
String().also {
loadSources()
}
}
the block also {} will return the empty String created with String() that can be assigned using lazy initialization to the val users
The error trying to compile the Android docs code is:
Type inference failed: Not enough information to infer parameter T in constructor MutableLiveData()
that means that the compiler is not able to infer the type of the MutableLiveData instance created using the constructor wihtout type.
Without the apply block the compiler will be able to compile it because it can easily infer the type from the val definition:
private val sources: MutableLiveData<List<User>> by lazy {
MutableLiveData()
}
but adding the apply block goes back to the generic type and the compiler cannot infer it. So the solution, as you did, is specifying the type hold in the MutableLiveData container:
private val sources: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadSources()
}
}
Tell me, please, how to make it more correct so that the ViewModel supports working with the desired repository, depending on the viewmodel's parameter? Android application should display a list of requests, requests are of different types. I want to use one fragment for request of different types and in one model I want universally work with a repository that will pull out requests of the required type from the database (Room).
I made a common interface for repositories:
interface RequestRepository<T> {
fun getRequests(): LiveData<List<T>>
fun getRequestById(requestId: String): LiveData<T>
suspend fun insertRequests(requests: List<T>)
suspend fun deleteRequest(request: T)
suspend fun deleteAllRequests()
}
This is one of the repositories:
class PaymentRequestRepository private constructor(private val paymentRequestDao: PaymentRequestDao) : RequestRepository<PaymentRequest> {
override fun getRequests() = paymentRequestDao.getRequests()
override fun getRequestById(requestId: String) = paymentRequestDao.getRequestById(requestId)
override suspend fun insertRequests(requests: List<PaymentRequest>) {
paymentRequestDao.deleteAll()
paymentRequestDao.insertAll(requests)
}
override suspend fun deleteRequest(request: PaymentRequest) = paymentRequestDao.delete(request)
override suspend fun deleteAllRequests() = paymentRequestDao.deleteAll()
companion object {
// For Singleton instantiation
#Volatile private var instance: PaymentRequestRepository? = null
fun getInstance(paymentRequestDao: PaymentRequestDao) =
instance ?: synchronized(this) {
instance ?: PaymentRequestRepository(paymentRequestDao).also { instance = it }
}
}
}
How in the ViewModel to work with the necessary repository depending on the type of request?
class RequestListViewModel(application: Application, val requestType: RequestType): AndroidViewModel(application) {
//lateinit var paymentRequestRepository: PaymentRequestRepository
//lateinit var serviceRequestRepository: ServiceRequestRepository
lateinit var requestRepository: RequestRepository<BaseRequestDao<Request>>
...
init {
val database = AgreementsDatabase.getDatabase(application)
when (requestType) {
RequestType.MONEY -> {
val paymentRequestDao = database.paymentRequestsDao()
requestRepository = PaymentRequestRepository.getInstance(paymentRequestDao)
}
RequestType.SERVICE -> {
val serviceRequestDao = database.serviceRequestsDao()
requestRepository = ServiceRequestRepository.getInstance(serviceRequestDao)
}
RequestType.DELIVERY -> {
val deliveryRequestsDao = database.deliveryRequestsDao()
requestRepository = DeliveryRequestRepository.getInstance(deliveryRequestsDao)
}
}
_requests = requestRepository.getRequests()
updateRequests();
}
}
** When creating a repository, I get a type mismatch error: **
requestRepository = PaymentRequestRepository.getInstance(paymentRequestDao)
Tell me how is this done correctly?
I'm in the process of wrapping my head around Architecture Components / MVVM.
Let's say I have a repository, a ViewModel and a Fragment. I'm using a Resource class as a wrapper to expose network status, like suggested in the Guide to architecture components.
My repository currently looks something like this (simplified for brevity):
class MyRepository {
fun getLists(organizationId: String) {
var data = MutableLiveData<Resource<List<Something>>>()
data.value = Resource.loading()
ApolloClient().query(query)
.enqueue(object : ApolloCall.Callback<Data>() {
override fun onResponse(response: Response<Data>) {
response.data()?.let {
data.postValue(Resource.success(it))
}
}
override fun onFailure(exception: ApolloException) {
data.postValue(Resource.exception(exception))
}
})
}
Then in the ViewModel, I also declare a MutableLiveData:
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myLiveData = myRepository.getLists(organizationId)
}
Finally, the Fragment:
viewModel.getLists.observe(this, Observer {
it?.let {
if (it.status.isLoading()) showLoading() else hideLoading()
if (it.status == Status.SUCCESS) {
it.data?.let {
adapter.replaceData(it)
setupViews()
}
}
if (it.status == Status.ERROR) {
// Show error
}
}
})
As you see, there will be an issue with the observer not being triggered, since the LiveData variable will be reset in the process (the Repository creates a new instance).
I'm trying to figure out the best way to make sure that the same LiveData variable is used between the Repository and ViewModel.
I thought about passing the LiveData from the ViewModel to the getLists method, so that the Repository would be using the object from the ViewModel, but even if it works, it seems wrong to do that.
What I mean is something like that:
ViewModel
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myRepository.getLists(myLiveData, organizationId)
}
Repository
fun getLists(data: MutableLiveData<Resource<List<Something>>>, organizationId: String) {
...
}
I think I figured out how to do it, thanks to #NSimon for the cue.
My repository stayed the same, and my ViewModel looks like this:
class MyViewModel : ViewModel() {
private val myRepository = MyRepository()
private val organizationIdLiveData = MutableLiveData<String>()
private val lists = Transformations.switchMap(organizationIdLiveData) { organizationId -> myRepository.getLists(organizationId) }
fun getLists() : LiveData<Resource<MutableList<Something>>> {
return lists
}
fun fetchLists(organizationId: String, forceRefresh: Boolean = false) {
if (organizationIdLiveData.value == null || forceRefresh) {
organizationIdLiveData.value = organizationId
}
}
}
I observe getLists() in my fragment, and call viewModel.fetchLists(id) when I want the data. Seems legit?