Bind ViewModel to a Fragment (Game)

Kotlin and Android Development featuring Jetpack — by Michael Fazio (28 / 125)

The Pragmatic Programmers
The Pragmatic Programmers
8 min readSep 30, 2021

--

👈 Create Another ViewModel (Game) | TOC | Summary and Next Steps 👉

We did most of the UI work here last chapter in Build Another Fragment (Game), so there’s less to worry about now. The first thing we need is to add a <variable> for GameViewModel called vm.

​ <layout xmlns:android=​"http://schemas.android.com/apk/res/android"​
​ xmlns:app=​"http://schemas.android.com/apk/res-auto"​
​ xmlns:tools=​"http://schemas.android.com/tools"​>

» <data>
» <variable
» name=​"vm"​
» type=​"dev.mfazio.pennydrop.viewmodels.GameViewModel"​ />
» </data>

​ <androidx.constraintlayout.widget.ConstraintLayout
​ android:layout_width=​"match_parent"​
​ android:layout_height=​"match_parent"​
​ android:animateLayoutChanges=​"true"​
​ tools:context=​".GameFragment"​>
​ ​<!-- Previously created views are in here. -->​
​ </androidx.constraintlayout.widget.ConstraintLayout>
​ </layout>

Now that the vm <variable> exists in our layout, we can go back and complete the binding inside GameFragment. This will look similar to what we did in Bind PickPlayersViewModel. We need to get an instance of GameViewModel, then apply the GameViewModel binding.

While we’re here, we’ll also set a lifecycleOwner, which ensures our LiveData will update properly. We can send in the viewLifecycleOwner value that comes from the Fragment class. This tells the LiveData inside GameViewModel to follow the same life cycle (creation/disposal/and so on) as the entered life-cycle owner. In our case, that’s the GameFragment.

Finally, the textCurrentTurnInfo.movementMethod assignment is still in here, too.

​ ​class​ GameFragment : Fragment() {
» ​private​ ​val​ gameViewModel ​by​ activityViewModels<GameViewModel>()

​ ​override​ ​fun​ ​onCreateView​(
​ inflater: LayoutInflater, container: ViewGroup?,
​ savedInstanceState: Bundle?
​ ): View? {
​ ​val​ binding = FragmentGameBinding
​ .inflate(inflater, container, ​false​)
» .apply {
» vm = gameViewModel
»
» textCurrentTurnInfo.movementMethod = ScrollingMovementMethod()
»
» lifecycleOwner = viewLifecycleOwner
» }

​ ​return​ binding.root
​ }
​ }

With the GameViewModel now bound to GameFragment, we can get all the bindings in place inside the layout. We’re going to update the slots at the end since that requires updates in multiple files. Let’s get everything done in here first before we move anywhere else.

Update fragment_game.xml Bindings

We’re going to work our way down the layout (skipping the Slot views), updating each of the views to use the GameViewModel. First stop is the textCurrentPlayerName <TextView>. Here, we’re going to use the name of the current player from the ViewModel by referencing vm.currentPlayer.playerName. In case we don’t have a value for currentPlayer (like before a game’s been started), we’re also going to add a fallback string resource value called na (with the value N/A). The binding syntax (remember, this isn’t Kotlin code, it’s the syntax language) will look like this:

​ android:text='@{vm.currentPlayer.playerName ?? @string/na}'

Interestingly enough, if vm.currentPlayer is null, this expression is still fine. We don’t have to include any additional checks; it’ll just fall back to the @string/na string resource value.

The text for the textCurrentPlayerCoinsLeft <TextView> will be similar, though make sure to turn it into a String, as an Int will try to reference a string resource rather than the actual Int value.

​ android:text='@{vm.currentPlayer.pennies + ""}'

No binding is needed for textCoinsLeft; just make sure to create a string resource for the coin(s) left text, then use that instead of a hardcoded String.

​ android:text='@string/coins_left'

Skipping to the end of the layout file, the textCurrentTurnInfo box will have this binding:

​ android:text="@{vm.currentTurnText}"

and the textCurrentStandingsInfo will have this binding:

​ android:text="@{vm.currentStandingsText}"

With all those bindings done, our screen looks a bit more put together than it did before, as shown in the image.

images/pd.bindDataViewModels/game-field-bindings.png

The last piece here that we want to handle is the Roll and Pass buttons. I’m calling those out on their own since we have a few changes to make for each one.

Update Button Bindings

Each button requires similar changes: we want to change the text and image colors to white, set whether or not the button is enabled, and set up conditional background color logic. We also need to add click handlers to each button.

Making things white will be straightforward enough, as we just need to add two attributes: android:textColor and android:drawableTint to each button. Both of those will have the value @android:color/white. The android:enabled attribute will use the values from vm.canRoll or vm.canPass, depending on which button we’re working with.

The background logic will be similar, as we’re going to also use those Slot properties. However, here we’ll be using a ternary statement to decide if the button is gray or a specific app color from res/values/colors.xml. The following code block is for the Roll button, but both buttons will be basically the same. The only real differences are which Slot property to use and that the Pass button is using colorAccent (instead of colorPrimary) for the background.

​ <Button
​ android:layout_width=​"0dp"​
​ android:layout_height=​"wrap_content"​
​ android:layout_weight=​"3"​
​ android:background=
​ ​"@{vm.canRoll ? @color/colorPrimary : @color/plainGray}"​
​ android:drawableEnd=​"@drawable/mdi_dice_6_black_24dp"​
​ android:drawableTint=​"@android:color/white"​
​ android:enabled=​"@{vm.canRoll}"​
​ android:onClick=​"@{() -> vm.roll()}"​
​ android:padding=​"10dp"​
​ android:text=​"@string/roll"​
​ android:textColor=​"@android:color/white"​ />

You’ll need to add the @color/plainGray entry to res/values/colors.xml. I’m using #CCC, but you’re free to use whatever you think will look good.

images/pd.bindDataViewModels/game-fragment-button-binding.png

The last piece with the buttons is to add the roll and pass functions to GameViewModel. You probably saw in the code block for the Roll <Button> the android:onClick”@{() -> vm.roll()}” attribute. This is a listener binding, which means an expression that runs when an event occurs.

Inside a listener binding, we can add whatever we like, including references to other <variable> references in the binding. But as that’s not necessary right now, we’re left with a minimal expression.

Since the roll and pass functions don’t yet exist, Android Studio is probably displaying an error right now, so let’s create those quickly.

Both functions inside will be empty to start since we’re going to add in the functionality during More GameViewModel Functions: roll() and pass():

​ ​fun​ ​roll​() {
​ ​// Implementing later​
​ }

​ ​fun​ ​pass​() {
​ ​// Implementing later​
​ }

The next step will be getting all the penny slots updated to use the data from the GameViewModel. We can even set up some test values in there to confirm things are working once all the bindings are complete.

Update Slot Bindings

Each of our slot <include> tags will change from something like app:slotNum”@{1}” to app:slot”@{vm.slots[0]}”. The full <include> tags will now look like this:

​ <include
​ android:id=​"@+id/coinSlot1"​
​ layout=​"@layout/layout_coin_slot"​
​ android:layout_width=​"0dp"​
​ android:layout_height=​"wrap_content"​
​ android:layout_weight=​"1"​
​ app:slot=​"@{vm.slots[0]}"​ />

Then, in layout_coin_slot.xml, we can replace the slotNum <variable> with slot. This <variable> will reference our Slot class.

​ <data>
» <variable
» name=​"slot"​
» type=​"dev.mfazio.pennydrop.types.Slot"​ />
​ </data>

With the new <variable> in place, we can start updating each item in layout_coin_slot.xml. We’re going to go in reverse order since the slotNumberCoinSlot <TextView> is the quickest change. Here, we just need to update android:text to use slot.number instead of slotNum and set the text color. We want all pieces of a slot to be our app’s primary color (from res/values/colors.xml) if that was the slot last rolled by the player. Otherwise, everything should be black.

​ android:text='@{slot.number + ""}'
​ android:textColor=
​ "@{slot.lastRolled ? @color/colorPrimary : @android:color/black}"

Next up is the bottomViewCoinSlot <View> and its android:background property. We do effectively the same thing as we did with the slot text:

​ android:background=
​ "@{slot.lastRolled ? @color/colorPrimary : @android:color/black}"

Now we can move back to the coinImageCoinSlot <ImageView>. This is basically set, but we don’t currently have a way to dynamically show and hide the coin. In our Slot class, we have the canBeFilled and isFilled variables that can control this, so now we need to use them in our layout.

We could add a ternary statement for the android:visibility attribute like we did with the colors, but this feels like a good time to introduce binding adapters.

Create Binding Adapters

Binding adapters allow you to effectively create new attributes on a layout view. Create a new package called binding and then a new file inside there called BindingAdapters. This shouldn’t be a class, since we’re just going to have stand-alone package-level functions in here. These are functions that aren’t associated with a particular class but instead just live as part of a package. We can bring these into other classes by importing dev.mfazio.pennydrop.binding (or your equivalent package name).

Back to the binding adapters: these functions will bring in a View object and a property, then do something with those values. It’s possible to avoid having custom logic in the binding adapter, but each of our functions will include custom logic, as we’ve got conditionals to consider.

For this block, we’re going to create a new binding adapter function called bindIsHidden, which will create a new app:isHidden attribute on all views. Note that the name of the new attribute comes from the @BindingAdapter annotation associated with the function. The logic is similar to what we did before in the ternary statement:

​ ​import​ ​android.view.View​
​ ​import​ ​androidx.databinding.BindingAdapter​

​ @BindingAdapter(​"isHidden"​)
​ ​fun​ ​bindIsHidden​(view: View, isInvisible: Boolean) {
​ view.visibility = ​if​ (isInvisible) View.INVISIBLE ​else​ View.VISIBLE
​ }

Now we can head back to our coinImageCoinSlot <ImageView> to add this new attribute. We’ll also set the color of the coin using the android:tint attribute.

​ android:tint="@{slot.lastRolled ? @color/colorPrimary : @android:color/black}"

​ app:isHidden="@{!slot.canBeFilled || !slot.isFilled}"

With all the binding for our layout_coin_slot.xml layout completed, this is a great time to run the app and check things out. But since we’re not doing anything with the Slot classes yet, the page won’t look too interesting. So we’re going to set up test data.

Inside the Slot, make two updates: isFilled should now default to true for even numbers, and lastRolled will be true for slots 2 and 5.

​ ​data class​ Slot(
​ ​val​ number: Int,
​ ​val​ canBeFilled: Boolean = ​true​,
​ ​var​ isFilled: Boolean = number % 2 == 0,
​ ​var​ lastRolled: Boolean = number % 3 == 2
​ )

With these hardcoded values in place, we can see everything working in the app:

images/pd.bindDataViewModels/game-fragment-slot-binding.png

Before we move on, go back to the Slot class and remove the test values we put in there. In the next chapter, we’re going to get the game logic working, so we won’t need them. Better to revert things before we forget!

​ ​data class​ Slot(
​ ​val​ number: Int,
​ ​val​ canBeFilled: Boolean = ​true​,
​ ​var​ isFilled: Boolean = ​false​,
​ ​var​ lastRolled: Boolean = ​false​
​ )

👈 Create Another ViewModel (Game) | TOC | Summary and Next Steps 👉

Kotlin and Android Development featuring Jetpack by Michael Fazio can be purchased in other book formats directly from the Pragmatic Programmers. If you notice a code error or formatting mistake, please let us know here so that we can fix it.

--

--

The Pragmatic Programmers
The Pragmatic Programmers

We create timely, practical books and learning resources on classic and cutting-edge topics to help you practice your craft and accelerate your career.