DEV Community

Cover image for Tutorial: android fore basics
Eric Donovan
Eric Donovan

Posted on • Edited on • Originally published at dev.to

Tutorial: android fore basics

This is part of a series on android fore

Let’s write a simple slot machine game. We’ll use the tiny fore library to demonstrate:

  • a way to truly separate your app from its UI layer
  • a way to get rock solid UI consistency
  • a way to handle asynchronous code with no memory leaks

unlikely slot machine game

unlikely slot machine game

Because we’re using fore: rotation support and testability will drop out at the end automatically, and we will also be writing surprisingly little code.

We’ll write this one in Kotlin, because Kotlin.


More simple: For even simpler examples see the fore github repo. It comes with a number of tiny app examples covering just: reactive-ui basics, asynchronous code, adapters, networking (with Retrofit2, but other samples in the fore repo use Ktor and also Apollo for graphQL APIs) and db driven apps (with Room)

More complicated: A lot of architectures start to show the strain once you move beyond trivial complexity. Complexity at the UI layer is something that fore handles particularly well, and there is a larger more complex app discussed here to help you investigate that


Let’s get started

MVO

fore is ideal for implementing the Model View Observer architecture, we'll tackle each component in turn:

Model

We need a model to drive our slot machine, we’re going to call it SlotMachineModel. It’s going to have a spin() method which will set the wheels spinning. The 3 wheels are going to either be spinning or showing a cherry, a dice, or a bell. (Ok so I’ve never actually played a real slot machine, sucks to be me ;p).

Let’s keep it simple and have three methods called: getState1(), getState2(), getState3(). And they can return an enum for each wheel with one of these states: SPINNING, CHERRY, DICE, BELL

We’ll also have an isWon() method that’s always going to return false, unless the three wheels are: ALL identical AND NOT SPINNING (eg CHERRY, CHERRY, CHERRY would be a win)

To build up the suspense (and let you check rotation support for yourselves) we will do the spinning calculations asynchronously and we will take our time about it, by randomly adding a few seconds delay to each spin.

Last but by no means least: the Model needs to be Observable, whenever this model’s state changes, it needs to let its observers know that it’s changed. (If you're on your phone you might want to put it in landscape mode to read the following code snippets)

class SlotMachineModel constructor(
        private val stateFetcher: RandomStateFetcher,
        private val workMode: WorkMode)
: Observable by ObservableImp(workMode) {

    enum class State {
        SPINNING,
        CHERRY,
        DICE,
        BELL
    }

    private val rnd = Random()
    private val wheel1 = Wheel(State.CHERRY)
    private val wheel2 = Wheel(State.DICE)
    private val wheel3 = Wheel(State.BELL)

    fun spin() {
        spinWheel(wheel1)
        spinWheel(wheel2)
        spinWheel(wheel3)
    }

    private fun spinWheel(wheel: Wheel) {

        //if wheel is already spinning, just ignore
        if (wheel.state != State.SPINNING) {
            wheel.state = State.SPINNING
            notifyObservers()
            AsyncBuilder<Void, State>(workMode)
                .doInBackground {
                    stateFetcher.fetchRandom(randomDelayMs())
                }
                .onPostExecute { state ->
                    wheel.state = state; notifyObservers()
                }
                .execute()
        }
    }

    fun getState1(): State {
        return wheel1.state
    }

    fun getState2(): State {
        return wheel2.state
    }

    fun getState3(): State {
        return wheel3.state
    }

    fun isWon(): Boolean {
        return (wheel1.state == wheel2.state
                && wheel2.state == wheel3.state
                && wheel1.state != State.SPINNING)
    }

    //anywhere between 1 and 10 seconds
    private fun randomDelayMs(): Long {
        return (1000
              + rnd.nextInt(8) * 1000
              + rnd.nextInt(1000)).toLong()
    }

    internal inner class Wheel(var state: State)
}

Enter fullscreen mode Exit fullscreen mode

That should do for our model, a few things to notice:

  • We are calling notifyObservers() immediately, whenever any of our model’s state changes
  • We are passing in a WorkMode dependency that controls how the notifications are sent and makes things easy to test (this shows what's happening very clearly, but it's not necessary with more recent versions of fore)
  • Everything is designed to run on the UI thread apart from when we explicitly jump to a background thread to fetch the wheel states (this is where networking usually happens)
  • We’re using fore’s AsyncBuilder for that (which is just a wrapper around AsyncTask that supports lambdas). This lets us easily run the code in Synchronous mode if we want to (e.g. for tests), but you can use coroutines for this too: see the sample apps in the fore repo for a testable way to use coroutines.
  • This Model knows nothing about android view layer classes or contexts
  • We’re also using a RandomStateFetcher dependency to fetch the actual state (nothing to do with fore, this just makes things easier to test, it’s also where you might put networking code in future, see here for a simple app with a networking layer)

Much more information and a big checklist for writing models when you’re using fore is here

View

We’ll write a custom view class to display our slot machine, we’re going to call it SlotMachineView, and it’s NOT going to be in the same package as the model.

We can do it in a standard XML layout as follows, the root of this one happens to be RelativeLayout, so our SlotMachineView will extend that.

<?xml version="1.0" encoding="utf-8"?>
<foo.bar.example.foreintro.ui.SlotMachineView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/slot_spacer">

    <ImageView
        android:id="@+id/slots_machine"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginLeft="@dimen/slot_spacer"
        android:layout_marginStart="@dimen/slot_spacer"
        android:contentDescription="slot machine background"
        android:scaleType="fitCenter"
        android:src="@drawable/machine" />

    <!-- central slot -->

    <foo.bar.example.foreintro.ui.widget.SlotView
        android:id="@+id/slots_2_slotview"
        android:layout_width="@dimen/slot_icon_size"
        android:layout_height="@dimen/slot_icon_size"
        android:layout_centerInParent="true"
        android:background="@drawable/slots_dice"
        android:contentDescription="2nd slot" />

    <!-- left most slot -->

    <foo.bar.example.foreintro.ui.widget.SlotView
        android:id="@+id/slots_1_slotview"
        android:layout_width="@dimen/slot_icon_size"
        android:layout_height="@dimen/slot_icon_size"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/slots_2_slotview"
        android:background="@drawable/slots_bell"
        android:contentDescription="1st slot" />

    <!-- right most slot -->

    <foo.bar.example.foreintro.ui.widget.SlotView
        android:id="@+id/slots_3_slotview"
        android:layout_width="@dimen/slot_icon_size"
        android:layout_height="@dimen/slot_icon_size"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@+id/slots_2_slotview"
        android:background="@drawable/slots_cherry"
        android:contentDescription="3rd slot" />

    <!-- slot machine handle -->

    <ImageView
        android:id="@+id/slots_handle_imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@+id/slots_machine"
        android:src="@drawable/slot_handle_selector" />

    <!-- slot machine win banner -->

    <ImageView
        android:id="@+id/slots_win"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:src="@drawable/slots_win" />

</foo.bar.example.foreintro.ui.SlotMachineView>
Enter fullscreen mode Exit fullscreen mode

Our custom view is going to handle the following standard things:

  • get a reference to all the view elements we need
  • get a reference to the SlotMachineModel which will inform what state is displayed on the UI
  • hook up the button listeners so that they interact with the model
  • sync our view so that it matches the state of the SlotMachineModel (set all the SlotView backgrounds, and the win banner each time the model notifies us that it’s changed)
class SlotMachineView @JvmOverloads constructor(
        context: Context?, 
        attrs: AttributeSet? = null, 
        defStyleAttr: Int = 0, 
        defStyleRes: Int = 0) :
        RelativeLayout(context, attrs, defStyleAttr), SyncableView {


    //models that we need
    private lateinit var slotMachineModel: SlotMachineModel

    //single observer reference
    val observer = Observer { syncView() }


    override fun onFinishInflate() {
        super.onFinishInflate()

        //(getting view references is handled for us by kotlin tools)

        getModelReferences()

        setClickListeners()
    }


    private fun getModelReferences() {   
        slotMachineModel = OG[SlotMachineModel::class.java]
    }

    private fun setClickListeners() {
        slots_handle.setOnClickListener { slotMachineModel.spin() }
    }


    //fore data binding stuff below

    override fun syncView() {
        slots_1_slotview.setState(slotMachineModel.getState1())
        slots_2_slotview.setState(slotMachineModel.getState2())
        slots_3_slotview.setState(slotMachineModel.getState3())
        slots_win.showOrInvisible(slotMachineModel.isWon())

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        slotMachineModel.addObserver(observer)
        syncView() //  <- don't forget this
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        slotMachineModel.removeObserver(observer)
    }
}
Enter fullscreen mode Exit fullscreen mode

By the way, that syncView() method is surprisingly powerful as long as you stick to 2 rules when you write it:

-If a model being observed changes in any way, then the entire view is refreshed.
-Where there is an if, there must also be an else

That’s explained in detail here

Observer

This is the last part of implementing MVO, and we’ve kind of already done it.

All the Model needed to do to become observable was to extend fore’s ObservableImp (or to implement the Observable interface) and to make sure to call notifyObservers() each time any of the state changed.

For the View, all it had to do to observe the Model was to add and remove its Observable in line with the Android lifecycle methods, and to call syncView() whenever the model notified it of a change.

Threading issues are taken care of here as everything operates on the UI thread (including the observer notifications) which is exactly how you need UI code to be run anyway. Asynchronous code is run away from the UI layer, inside the model and is completed before the lightweight notifications are fired in the UI thread.

We didn’t mention DI yet, but it’s key. The line you see in the View:

slotMachineModel = App.get(SlotMachineModel::class.java)
Enter fullscreen mode Exit fullscreen mode

provides the view with the same instance of the model each time, so that everything remains consistent, even when you rotate the screen. You might use Dagger2 for that, the example here uses a pure DI solution.

automatically supports rotation

as promised… this already supports rotation

For completeness and so that you can see how tiny it is, this is the activity code we use to launch the view (you might want to combine the view and the activity classes, but we've separated them here for clarity)

Testing

Because there is so much separation between the view layer and the rest of the app, this code is particularly testable. The main fore repo has a lot of sample apps, many of which are tested extensively if want to study test examples.

Now for the really cool stuff…

This is a simple example: one view observing one model, sooooo what.

But there’s something clever in that very basic looking observer code. It has no parameter. This means that if your view suddenly needs to observe not just a SlotMachineModel, but also a UserRepository, or an AccountModel, or NetworkStatusModel, ChatInboxModel, WeatherModel— or all 6 models at the same time. It can. Easily. The observer interface is identical for all the models, so all a view needs to do is to get hold of a reference to the models using DI, and then:

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    slotMachineModel.addObserver(observer)
    userRepository.addObserver(observer)
    accountModel.addObserver(observer)
    networkStatusModel.addObserver(observer)
    chatInboxModel.addObserver(observer)
    weatherModel.addObserver(observer)
    syncView() //  <- don't forget this
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    slotMachineModel.removeObserver(observer)
    userRepository.removeObserver(observer)
    accountModel.removeObserver(observer)
    networkStatusModel.removeObserver(observer)
    chatInboxModel.removeObserver(observer)
    weatherModel.removeObserver(observer)
}
Enter fullscreen mode Exit fullscreen mode

And if you have a strong objection to that level of boiler plate, you can use an ObservableGroup here (which is only possible because the observer api in each case is identical)

As for the Models themselves, they don’t even know how many views might be observing them, it makes no difference to their code at all.

Let’s say the view wants to: disable the slot machine handle if the account has no money in it. But, we want to remain reactive and keep that enabled state correct when the balance increases or decreases, or if the phone screen rotates etc. One line inside syncView:

slots_handle.isEnabled = accountModel.balance > 0
Enter fullscreen mode Exit fullscreen mode

Maybe the view should: show a weather forecast text at the top of the screen. But again we need to keep our UI nice and reactive so that if the weather forecast changes or the phone is rotated, everything remains consistent. One extra line in syncView:

weather_text.text = weatherModel.getForecastText()
Enter fullscreen mode Exit fullscreen mode

That text should probably: be red if the windspeed is above 50mph. But we don't want the testers finding any UI consistency bugs when they rotate the phone, or leave the phone in a drawer for a week or whatever else it is they'll try. One extra line in syncView:

weather_text.setTextColor(resources.getColor(
                             if (weatherModel.windSpeed>50)
                                   R.color.red else R.color.blue))
Enter fullscreen mode Exit fullscreen mode

Or if you want that small piece of logic unit testable, then put it in a model (a viewModel would be ideal) and the simply call it from your view: model.getWeatherColor()

This code might make things look very easy, but there are many ways to get things wrong of course, if there’s something in your app that doesn’t feel right or you’re having any performance issues - chances are you’ll find some guidance in the check list here.


Well thanks for reading this far!

Hopefully this gives you some ideas about how you could make use of the fore library or just MVO to write quicker, cleaner, more testable apps. I use it all the time during my work as it is production ready (it’s a very small library so there is not a lot to go wrong). There are lots more considerations to discuss when you look at networking, adapters, databases etc but they are all treated in the same standard way using the syncView() convention. For the moment the best place to look for clear examples is in the fore github repo.

here’s the complete code for the tutorial

Top comments (0)