1

I'm building a very simple game with Jetpack Compose where I have 3 screens:

  1. HeroesScreen - where I display all heroes in the game. You can select one, or multiple of the same character.
  2. 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.
  3. 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.

Joan P.
  • 2,368
  • 6
  • 30
  • 63
  • 1
    The composition solution would be to write a wrapper class for managing a particular repo value and expose that in each of the repositories for the values that are incrementable, instead of directly exposing increment/decrement functions. The inheritance solution would be to make a repo superclass defining the incrementing and decrementing of some generic type that is defined in the subclasses. Composition is usually preferable to inheritance. – Tenfour04 Sep 07 '22 at 13:28
  • @Tenfour04 Ok, composition sounds good. Can you please provide me an example in an answer, so I can understand it better? Besides that, please note that increment/decrement a value of quantity on a server. Thanks, in advanced. – Joan P. Sep 07 '22 at 13:40
  • @Tenfour04 If you have time, I will be very grateful if you find some time to answer my question. I really don't know how to create a wrapper class for managing a particular repo. – Joan P. Sep 07 '22 at 18:08
  • 1
    It really depends on how your repo works, such as how it interacts with your server. If you provide sample code of one of your classes with its increment and decrement functions, I can make an example. – Tenfour04 Sep 07 '22 at 18:12
  • Hey @Tenfour04 Please check my updated question. If you need anything else, please let me know. – Joan P. Sep 08 '22 at 06:54
  • @Tenfour04 Is there anything else you need? – Joan P. Sep 09 '22 at 05:23

1 Answers1

3

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:

  1. 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().

  1. 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
}
Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you so much. It's all clear now. I stared a bounty too, to reward you for the time you took to answer my question. – Joan P. Sep 10 '22 at 08:59
  • One more quick question. Why is this solution (composition) preferred over inheritance? Is inheritance a bad practice? – Joan P. Sep 10 '22 at 09:37
  • 1
    It’s not universally true, but composition is more often a preferable solution. If you do a Web search for “composition over inheritance”, you’ll find articles explaining why. – Tenfour04 Sep 10 '22 at 11:43
  • Hey. Do you think you can also help me with this [question](https://stackoverflow.com/questions/73719810/how-to-increment-a-counter-in-realtime-database-using-transactions-with-kotlin-c)? – Joan P. Sep 15 '22 at 06:23