Create Another ViewModel (Game)

This class will look similar to PickPlayersViewModel on a high level, but there’s going to be a lot more going on here since this will handle the connection between the Game view and the GameHandler we’ll be building in Chapter 4, Update LiveData with Conditional Game Logic.

We’re going to start with an empty GameViewModel class, and we can add the pieces in later on. It’ll look like this:

 package​ ​dev.mfazio.pennydrop.viewmodels
 
 import​ ​androidx.lifecycle.ViewModel
 
 class​ GameViewModel: ViewModel() {
 //Logic will go in here.
 }

Once GameViewModel is available, we can add a List<Player> to hold everyone we added in the Pick Players view. We’ll need a little setup before we can do that, though.

Add Players to the Game

To get the players into a game, we’re going to need to do a few things: we need a Player class, we need the List<Player> that I mentioned a bit ago, and finally, we need a way to get players from the Pick Players view into GameViewModel. Let’s walk through each of these steps now.

Create the Player Class

It may seem silly that we’re going to create an entirely separate class, Player, when NewPlayer already exists. Why didn’t I just make the Player class in the first place?

The reason for the two classes is that they serve two different purposes. The NewPlayer class is intended to figure out who’s in the game and who they are (human vs. AI, what’s their name or which AI), while Player will be for tracking the player’s status in a game (currently rolling, pennies left, and so on). While we could have created a single large Player class with all these fields, going with the two separate classes is a better separation of concerns.

With that being said, the Player class will contain five properties: the player’s name, whether or not they’re human, the selected AI, whether or not they’re currently rolling, and how many pennies they currently have.

All of the properties will have a default value, but only selectedAI can be null. Also, we’re going to keep pennies and isRolling outside of the constructor since neither is likely to be set to something other than its default value upon initialization.

Finally, we want to add a function called addPennies to the Player class. While it may seem overkill now, we’ll have a situation later where we get an instance of a Player?, and trying to add to a property of a nullable type is a pain.

The full Player class code looks like this:

 package​ ​dev.mfazio.pennydrop.types
 
 import​ ​dev.mfazio.pennydrop.game.AI
 
 data class​ Player(
 val​ playerName: String = ​""​,
 val​ isHuman: Boolean = ​true​,
 val​ selectedAI: AI? = ​null
 ) {
 var​ pennies: Int = defaultPennyCount
 
 fun​ ​addPennies​(count: Int = 1) {
  pennies += count
  }
 
 var​ isRolling: Boolean = ​false
 
 companion​ ​object​ {
 const​ ​val​ defaultPennyCount = 10
  }
 }

Note how we’re using the defaultPennyCount value here inside our companion object like we would use a static read-only or constant value in Java.

To get our List<Player>, we need a way to convert a NewPlayer into a Player. The best spot to do this is a new function inside the NewPlayer class. This function will get the player’s name (or name of the AI, if applicable), then map a few more values from NewPlayer:

 fun​ ​toPlayer​() = Player(
 if​ (​this​.isHuman.​get​()) {
 this​.playerName
  } ​else​ {
  (​this​.selectedAI()?.name ?: ​"AI"​)
  },
 this​.isHuman.​get​(),
 this​.selectedAI()
 )

With the Player class ready, we can move on to the GameViewModel and its List<Player>. We’ll be both adding the property and getting the data from the Pick Players view.

Add Players to GameViewModel

Adding the players variable is the quick part, though it does use a new function, emptyList. This function (unsurprisingly) gives us an empty instance of whatever type of List we’re expecting. We could have just used listOf here, but emptyList is slightly more efficient and makes it clearer that we wanted an empty list and didn’t just forget to populate a listOf call. Plus, it’s good for you to know that it exists.

Also, to plan ahead, we can create a new function inside GameViewModel called startGame, which brings in the List<Player> and gets a game set up.

 import​ ​androidx.lifecycle.MutableLiveData
 import​ ​androidx.lifecycle.ViewModel
 import​ ​dev.mfazio.pennydrop.types.Player
 
 class​ GameViewModel : ViewModel() {
 private​ ​var​ players: List<Player> = emptyList()
 
 fun​ ​startGame​(playersForNewGame: List<Player>) {
 this​.players = playersForNewGame
 // More logic will be added here later.
  }
 }

With that added, we can head into PickPlayersFragment to get the data sent over. Thankfully, using a ViewModel class makes this process much simpler. Inside PickPlayersFragment, we can bring in an instance of GameViewModel (just as we did with PickPlayersViewModel) and transfer over the players when someone clicks the Play button. We do this by mapping each NewPlayer to a Player via the toPlayer function we created.

Note that since players is actually LiveData, we need to use pickPlayersViewModel.players.value to get out the List<NewPlayer>. Also, all value calls return a nullable instance of whatever it contains, so we need to have a fallback in case it’s null, which will be an empty listOf. In our case, it’ll never be null, but we still need to handle the scenario.

Once we have the List<NewPlayer>, we want to filter each NewPlayer instance to make sure isIncluded is true, then convert each NewPlayer to a Player instance via the toPlayer function. That conversion is done inside a map block, which creates a new List from another List after performing some kind of transformation on each element. Please note that the original list is never modified during this process but instead you’re given a new List with all new items.

The last piece of the Play button functionality is to switch over to the GameFragment. Luckily, this is a single line call to grab the current NavController then navigate to the GameFragment via ID.

 class​ PickPlayersFragment : Fragment() {
 private​ ​val​ pickPlayersViewModel
 by​ activityViewModels<PickPlayersViewModel>()
»private​ ​val​ gameViewModel ​by​ activityViewModels<GameViewModel>()
 
 override​ ​fun​ ​onCreateView​(
  inflater: LayoutInflater, container: ViewGroup?,
  savedInstanceState: Bundle?
  ): View? {
 val​ binding = FragmentPickPlayersBinding
  .inflate(inflater, container, ​false​)
  .apply {
 this​.vm = pickPlayersViewModel
 
»this​.buttonPlayGame.setOnClickListener {
» gameViewModel.startGame(
» pickPlayersViewModel.players.value
» ?.filter { newPlayer ->
» newPlayer.isIncluded.​get​()
» }?.map { newPlayer ->
» newPlayer.toPlayer()
» } ?: emptyList()
» )
»
» findNavController().navigate(R.id.gameFragment)
» }
  }
 
 return​ binding.root
  }
 }

With those changes, we now have gone from displaying info about the current players to sending them into the GameViewModel. If you want to confirm this, you can either set a breakpoint inside the GameViewModel startGame function or add a Toast.makeText call in there (both approaches are covered in Appendix 2, Troubleshooting Your App). Now that we have players in the GameViewModel, we can work on getting things displayed on the screen from the ViewModel.

Add LiveData to GameViewModel

We’ve got a bunch of LiveData values to add here, which we’ll then bind to components in layout_game.xml.

The values are as follows:

  • slots: the six Slot objects with their current status.

  • currentPlayer: the player that’s currently rolling.

  • canRoll: whether or not the current player can roll.

  • canPass: whether or not the current player can pass (remember, players must roll at least once).

  • currentTurnText: the info text on the bottom of the screen.

  • currentStandingsText: the text of scores in the current game.

We’ll look at the GameViewModel with all these LiveData objects in just a second, but we need to create the Slot class first.

 package​ ​dev.mfazio.pennydrop.types
 
 data class​ Slot(
 val​ number: Int,
 val​ canBeFilled: Boolean = ​true​,
 var​ isFilled: Boolean = ​false​,
 var​ lastRolled: Boolean = ​false
 )

Whew, now that we’re done with all that work, we can add all eight LiveData values to the GameViewModel. Most of the LiveData will just be holding Boolean flags (with their starting values), while the slots variable is the one particularly interesting part. Here, we’re going to do something similar to what we did in Add NewPlayer Instances to PickPlayersViewModel, with the (1..6).map { } block. This will create six Slot instances with their given slot number and the flag of canBeFilled, which is true for all but the last slot. Finally, we’ll set the current player to the first player in the game, mark them as rolling, and set canRoll to true.

 class​ GameViewModel : ViewModel() {
 private​ ​var​ players: List<Player> = emptyList()
 
»val​ slots =
» MutableLiveData(
» (1..6).map { slotNum -> Slot(slotNum, slotNum != 6) }
» )
»
»val​ currentPlayer = MutableLiveData<Player?>()
»
»val​ canRoll = MutableLiveData(​false​)
»val​ canPass = MutableLiveData(​false​)
»
»val​ currentTurnText = MutableLiveData(​""​)
»val​ currentStandingsText = MutableLiveData(​""​)
 
 fun​ ​startGame​(playersForNewGame: List<Player>) {
 this​.players = playersForNewGame
»this​.currentPlayer.value =
»this​.players.firstOrNull().apply {
»this​?.isRolling = ​true
» }
»this​.canRoll.value = ​true
  }
 }

With all those LiveData values set up, we can head over and get everything bound together.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.186.164