The Akamu Development for Android (Kotlin) follows the guidelines of the Model View Presenter Design Pattern. The basics of MVP can be found e.g. on this page: MVP for Android
This Wiki page should help to get an overivew of how the MVP pattern is embeded in our application.
Contract
When implementing a new activity e.g. the TitleActivity where you have the opportunity to change your title we start by providing an interface, the contract. It contains two further interfaces: one for the view and one for the presenter.
interface TitleContract {
/** interface containing functions of our activity **/
interface View {
}
/** interface containing functions of our presenter **/
interface Presenter {
}
}
In these interfaces we declare the functions used by the view and the presenter respectively. In our example we want to let the user know if setting a new title in the server was successful or not. So we declare the functions showUpdateTitleSuccess
and showUpdateTitleFailed
in the view interface:
interface View {
/** Let the user know that the title was updated**/
fun showUpdateTitleSuccess()
/** Let the user know that it was not possible to update the title **/
fun showUpdateTitleFailed()
}
The presenter should do the logic or pass data via an interactor to the model. Therefore we declare the function updateUserTitle
interface Presenter {
/** Tries to update the user title with a new title **/
fun updateUserTitle(titleAdapter: TitleAdapter, currentTitle: Title, clickedTitle: Title)
}
View
Our activity represents the view and therefore implements the view interface of the contract. It should be as dumb and simple as possible and should do no logic. We also instantiate the corresponding presenter which needs an instance of an interactor. This will be explained later on this page.
class TitleActivity :
AppCompatActivity(),
TitleContract.View
TitleAdapter.TitleOnClickHandler {
// Make an instance of the corresponding presenter for this view
private var presenter: TitlePresenter = TitlePresenter(TitleInteractor(TitleDataSourceImpl()), this)
private var user: User? = null
private var titleAdapter: TitleAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_title)
// Get the local app user by passing the activity's context
user = AppUserUtil.getLocalAppUser(this)
// The titles are represented in a list. For this list we need an adapter
titleAdapter = TitleAdapter(this)
// Connect the recycler view with this adapter
recyclerView_title.adapter = titleAdapter
}
override fun onClick(title: Title) {
presenter.updateUserTitle(titleAdapter!!, user!!.title, title)
}
override fun showUpdateTitleSuccess() {
val successful = Snackbar.make(findViewById(R.id.Coordinator_choose_title),
R.string.title_updated,
Snackbar.LENGTH_LONG)
successful.show()
}
override fun showUpdateTitleFailed() {
val unsuccessful = Snackbar.make(findViewById(R.id.Coordinator_choose_title),
R.string.could_not_update_title,
Snackbar.LENGTH_LONG)
unsuccessful.show()
}
}
Presenter
The presenter class for our TitleActivity implements the presenter interface and passes the data/name of the new title via an interactor to the model.
// Needs an interactor for passing the data to the model
class TitlePresenter(private val interactor: TitleInteractor, private val view: TitleContract.View) :
TitleContract.Presenter, BasePresenter<TitleContract.View>() {
override fun updateUserTitle(titleAdapter: TitleAdapter, currentTitle: Title, clickedTitle: Title) {
// some logic: only do an update call to the server if the titles differ
if(currentTitle != clickedTitle) {
interactor.updateUserTitle(clickedTitle, object : TitleDataSource.UpdateUserTitleCallback {
override fun onUpdateUserTitleSuccess(me: User) {
// more logic: tell the adapter to change the title
titleAdapter.currentTitle = titleAdapter.mTitles[titleAdapter.getIndexByTitleName(clickedTitle.name)]
// tell the view that it should show some feedback to the user
view.updateLocalAppUser(me)
view.showUpdateTitleSuccess()
titleAdapter.notifyDataSetChanged()
}
override fun onUpdateUserTitleFailed() {
// tell the view that it should show some feedback to the user
view.showUpdateTitleFailed()
}
})
}
}
}
Interactor
The interactor fetches the data from the data source. It's purpose is to forward the data from the source to the presenter e.g. via callbacks. It is used in order to separate logic (which is done in the presenter) and calls to the datasource which in turn accesses the AkamuAPI for network calls.
class TitleInteractor(private val dataSource: TitleDataSourceImpl) : TitleDataSource {
override fun updateUserTitle(title: Title, callback: TitleDataSource.UpdateUserTitleCallback) {
dataSource.updateUserTitle(title, object : TitleDataSource.UpdateUserTitleCallback {
override fun onUpdateUserTitleSuccess(me: User) {
callback.onUpdateUserTitleSuccess(me)
}
override fun onUpdateUserTitleFailed() {
callback.onUpdateUserTitleFailed()
}
})
}
}
Model
The model stores and delivers the data. We define an interface for our TitleDataSource. It holds another interface for callbacks from the model to the view via the interactor and the presenter.
interface TitleDataSource {
// Interface for callbacks
interface UpdateUserTitleCallback {
fun onUpdateUserTitleSuccess(me: User)
fun onUpdateUserTitleFailed()
}
// Does an API call in an AsyncTask
fun updateUserTitle(title: Title, callback: TitleDataSource.UpdateUserTitleCallback)
}
Finally the implementation of this interface does the actual API call and triggers the callbacks. Note, that this is done in an AsyncTask as network calls are not allowed to run on the UI thread.
class TitleDataSourceImpl : TitleDataSource {
override fun updateUserTitle(title: Title, callback: TitleDataSource.UpdateUserTitleCallback) {
// execute a new AsyncTask
val updateUserTitleTask = UpdateUserTitleTask(title, callback)
// Allow parallel execution of AsyncTasks in case other tasks are running
updateUserTitleTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
class UpdateUserTitleTask(var title: Title, var callback: TitleDataSource.UpdateUserTitleCallback)
: AsyncTask<Void, Void, Boolean>() {
var me : User? = null
override fun doInBackground(vararg params: Void?): Boolean{
return try {
// prepare the update call
me = V2APIUser.getUser()
me!!.title = title
val patchUser = PatchUser(
me!!.semester,
me!!.avatar.id,
title.id)
// do the update call with the patched user
V2APIUser.updateUser(patchUser)
// for onPostExecute: signalize that call was successful
true
} catch (e: Exception){
e.printStackTrace()
// for onPostExecute: signalize that call was not successful
false
}
}
override fun onPostExecute(success: Boolean?) {
// trigger the callbacks depending on the result of the asynchronous execution
if(success!!){
callback.onUpdateUserTitleSuccess(me)
} else {
callback.onUpdateUserTitleFailed()
}
}
}
}
Conclusion
So this is the the biggest part of the process for implementing calls to the Akamu game-server using the MVP pattern. It is important to note, that the actual network call actually hides behind the function V2APIUser.updateUser(patchUser)
. The wiki page AkamuAPI deals once again more deeply with the network API.