android fore
(click here if you're reading this on github)
fore helps you move code out of the view layer. Because once you do that on Android, magical things start to happen!
Quick Start
the kotlin API, running coroutines under the hood:
implementation("co.early.fore:fore-kt:1.4.4")
the original java API:
implementation("co.early.fore:fore-jv:1.4.4")
(and you now need mavenCentral() listed as a repository in your build files as jcenter is closing)
See the updgrade guide if you're coming from an older version.
New to fore
This repo includes 10 tiny example apps, any updates to fore are immediately reflected in the example apps and all their tests need to pass before new versions of fore are released, so they tend to remain current and are a good place to start if you're trying to figure out how things fit together:
git clone [email protected]:erdo/android-fore.git
There are also a few tutorials on dev.to like this one which demonstrates how the syncView() convention helps you to write less code, while removing a whole class of UI consistency bugs from the UI layer. Or this one which details the whys and the hows of converting the Android Architecture Blueprint Todo sample app from MVP to MVO using fore.
Overview
The main innovation in fore is its radically simplified observer implementation. This lets you separate architectural layers to a degree that would not normally be possible. It also encourages the development of genuinely reactive code at the view layer (you can't use the API to replace a callback with a Single<Boolean> and think you're done for example - what you would have there is just another callback, not reactive code per se).
Fore's observable classes let you make anything observable (usually it's repositories or classes in the domain layer that are made observable, and things in the view layer like activities, fragments or custom views do the observing).
public class AccountRepository extends ObservableImp {
public AccountRepository(WorkMode workMode) {
super(workMode);
}
...
}
class AccountRepository
: Observable by ObservableImp() {
...
}
Somewhere in the view layer (Activity/Fragment/View or ViewModel) there will be a piece of code like this:
Observer observer = this::syncView;
val observer = Observer { syncView() }
And that observer is typically added and removed from the observable in line with lifecycle methods so that we dont get any memory leaks. In the case below, a fragment is observing a Wallet model representing the details of a user's wallet.
@Override
protected void onStart() {
super.onStart();
wallet.addObserver(observer);
syncView(); // <- don't forget this
}
@Override
protected void onStop() {
super.onStop();
wallet.removeObserver(observer);
}
override fun onStart() {
super.onStart()
wallet.addObserver(observer)
syncView() // <- don't forget this
}
override fun onStop() {
super.onStop()
wallet.removeObserver(observer)
}
(There are a few ways to cut out the add and remove boiler plate by the way)
All that's left to do now is to implement syncView() which will be called on the UI thread whenever the state of the observables change. You'll probably notice that syncView() shares some characteristics with MVI's render() or MvRx's invalidate(), though you might be surprised to learn that syncView() has been used in commercial android apps since at least 2013!
public void syncView(){
increaseMobileWalletBtn.setEnabled(wallet.canIncrease());
decreaseMobileWalletBtn.setEnabled(wallet.canDecrease());
mobileWalletAmount.setText("" + wallet.getMobileWalletAmount());
savingsWalletAmount.setText("" + wallet.getSavingsWalletAmount());
}
override fun syncView() {
wallet_increase_btn.isEnabled = wallet.canIncrease()
wallet_decrease_btn.isEnabled = wallet.canDecrease()
wallet_mobileamount_txt.text = wallet.mobileWalletAmount.toString()
wallet_savingsamount_txt.text = wallet.savingsWalletAmount.toString()
wallet_balancewarning_img.showOrGone(wallet.mobileWalletAmount<2)
}
Here's a very basic example from one of the example kotlin apps included in the fore repo: View and Model code, and the tests: a Unit Test for the Model, and an Espresso Test for the View
The view layer tends to be particularly sparse when implementing MVO with fore, and the apps are highly scalable from a complexity standpoint, so fore works for both quick prototypes, and large complex commercial projects with 100K+ lines of code.
Specifically why it is that apps written this way are both sparse and scalable is not always immediately obvious. This discussion gets into the design of the fore api and why it drastically reduces boiler plate for a typical android app compared with alternatives. Some of the dev.to tutorials (see above) also touch on this. But these are subtle, advanced topics that are not really necessary to use fore at all - most of the actual code in the fore library is quite simple.
In fact fore itself is tiny: just over 500 lines of code for the core package The java version references 128 methods in all, and adds just 12.5KB to your apk before obfuscation.
MVO implemented with fore addresses issues like testability; lifecycle management; UI consistency; memory leaks; and development speed - and if you're spending time dealing with any of those issues in your code base or team, it's well worth considering.
Still reading?
In a nutshell, developing with fore means writing:
"Observable Models; Views doing the observing; and some Reactive UI tricks to tie it all together"
In MVO (like with most MV* architectures) the model knows nothing about the View. When the view is destroyed and recreated, the view re-attaches itself to the model in line with the observer pattern and syncs its view. Any click listeners or method calls as a result of user interaction are sent directly to the relevant model. With this architecture you remove a lot of problems around lifecycle management and handling rotations, it also turns out that the code to implement this is a lot less verbose (and it's also very testable and scalable).
There are a few important things in MVO that allow you an architecture this simple:
- The first is a very robust but simple Observer API that lets views attach themselves to any model they are interested in
- The second is the syncView() convention
- The third is writing models at an appropriate level of abstraction, something which comes with a little practice
- The fourth is making appropriate use of DI
If you totally grok those 4 things, that's pretty much all you need to use fore successfully, the code review guide should also come in handy as you get up to speed, or you bring your team up to speed.
The fore library also includes some testable wrappers for AsyncTask (that Google should have provided, but didn't): Async and AsyncBuilder - which support lambdas, making using them alot nicer to use.
If you've moved over to using coroutines already, a few fore extension functions are all you need to use coroutines in a way that makes them completely testable (something that is still pending in the official release).
There are also optional extras that simplify adapter animations and abstract your networking layer when using Retrofit2, Ktor or Apollo. fore works really well with RoomDB too, checkout the sample app for details.
Sample Apps
The apps here are deliberately sparse and ugly so that you can see exactly what they are doing. These are not examples for how to nicely structure XML layouts or implement ripple effects - all that you can do later in the View layers and it should have no impact on the stability of the app.
These apps are however, totally robust and comprehensively tested (and properly support rotation). And that's really where you should try to get to as quickly as possible, so that you can then start doing the fun stuff like adding beautiful graphics and cute animations.
For these example apps, all the View components are located in the ui/ package and the Models are in the feature/ package. This package structure gives the app code good glanceability and should let you find what you want easily.
For the sample apps there is a one-to-one relationship between the sub-packages within ui/, and the sub-packages within feature/ but it needn't be like that and for larger apps it often isn't. You might have one BasketModel but it will be serving both a main BasketView and a BasketIconView located in a toolbar for instance. A more complex view may use data from several different models at the same time eg a BasketModel and an AccountModel.
video | source code (java) | source code (kotlin)
This app is a bare bones implementation of fore reactive UIs. No threading, no networking, no database access - just the minimum required to demonstrate Reactive UIs. It's still a full app though, supports rotation and has a full set of tests to go along with it.
In the app you move money from a "Savings" wallet to a "Mobile" wallet and then back again. It implements a tiny section of the diagram from the architecture section.
video | source code (java) | source code (kotlin)
This one demonstrates asynchronous programming, and importantly how to test it. The java version uses (Async and AsyncBuilder), the kotlin version uses coroutines (with some fore extensions that make the coroutines unit testable). Again, it's a bare bones (but complete and tested) app - just the minimum required to demonstrate asynchronous programming.
This app has a counter that you can increase by pressing a button (but it takes time to do the increasing - so you can rotate the device, background the app etc and see the effect). There are two methods demonstrated: one which uses lambda expressions (for java), and one which publishes progress.
video | source code (java) | source code (kotlin)
This one demonstrates how to use adapters with fore (essentially call notifyDataSetChanged() inside the syncView() method).
To take advantage of the built in list animations that Android provides. Once you have set your adapter up correctly, you can instead call notifyDataSetChangedAuto() inside the syncView() method and fore will take care of all the notify changes work. (You could also use fore's notifyDataSetChangedAuto() to do this for you from your render() function if you're using MVI / MvRx or some flavour of Redux).
The java sample has two lists side by side so you can see the how the implementation differs depending on if you are backed by immutable list data (typical in architectures that use view states such as MVI) or mutable list data. As usual it's a complete and tested app but contains just the minimum required to demonstrate adapters.
The kotlin version has three lists, adding an implementation of google's AsyncListDiffer. All three implementations have slightly different characteristics, most notably the google version moves logic out of the model and into the adapter (that's why it doesn't automatically support rotation - but it could be added easiy enough by passing an external list copy to the adapter). Check the source code for further infomation.
video | source code (java) | source code (kotlin)
Clicking the buttons in this app will perform network requests to some static files that are hosted on Mocky (have you seen that thing? it's awesome). The buttons make various network connections, various successful and failed responses are handled in different ways. It's all managed by the CallProcessor class which is the main innovation in the fore-network library, the kotlin implementation of CallProcessor is implemented with coroutines and has an API better suited to kotlin and functional programming.
As you're using the app, please notice:
- how you can rotate the device with no loss of state or memory leaks. I've used Mocky to add a delay to the network request so that you can rotate the app mid-request to clearly see how it behaves (because we have used fore to separate the view from everything else, rotating the app makes absolutely no difference to what the app is doing, and the network busy spinners remain totally consistent). Putting the device in airplane mode also gives you consistent behaviour when you attempt to make a network request.
As usual this is a complete and tested app. In reality the tests are probably more than I would do for a real app this simple, but they should give you an idea of how you can do unit testing, integration testing and UI testing whilst steering clear of accidentally testing implementation details.
A To-do list on steroids that lets you:
- manually add 50 random todos at a time
- turn on a "boss mode" which randomly fills your list with even more todos over the following 10 seconds
- "work from home" which connects to the network and downloads 25 extra todos (up to 9 simultaneous network connections)
- randomly delete about 10% of your todos
- randomly change 10% of your outstanding todos to done
It's obviously ridiculously contrived, but the idea is to implement something that would be quite challenging and to see how little code you need in the view layer to do it.
It is driven by a Room db, and there are a few distinct architectural layers: as always there is a view layer and a model layer (in packages ui and feature). There is also a networking and a persistence layer. The UI layer is driven by the model which in turn is driven by the db.
All the database changes are done away from the UI thread, RecyclerView animations using DiffUtil are supported (for lists below 1000 rows), the app is totally robust and supports rotation out of the box. There is a TodoListModel written in Java and one in Kotlin for convenience, in case you are looking to use these as starting points for your own code.
There is only one test class included with this app which demonstrates how to test Models which are driven by a Room DB (using CountdownLatches etc). For other test examples, please see sample apps 1-4
Other Full App Examples
- Many of the dev.to tutorials have full sample apps associated with them, mostly kotlin
- There is a full app example hosted in a separate repo written in Kotlin here
Contributing
Please read the Code of Conduct, and check out the issues :)
License
Copyright 2015-2021 early.co
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.