Android Example App: Difference between revisions
(One intermediate revision by the same user not shown) | |||
Line 279: | Line 279: | ||
} | } | ||
} | } | ||
} | |||
</syntaxhighlight> | |||
==Swipe And Delete== | |||
I could not find a great way to implement the touch listener with the same feel as this implementation. In the View I created a ItemTouchHelper(itemTouchHelper. This defined the Swipe left and Right better than I could using gesture. Basically the deleteItem() is called on the adapter. I would have liked to have this could in the Adapter but a job for next time I think. | |||
<syntaxhighlight lang="kotlin"> | |||
fun initTouch() { | |||
val itemTouchHelperCallback = | |||
object : | |||
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { | |||
override fun onMove( | |||
recyclerView: RecyclerView, | |||
viewHolder: RecyclerView.ViewHolder, | |||
target: RecyclerView.ViewHolder | |||
): Boolean { | |||
return false | |||
} | |||
override fun onSwiped( | |||
viewHolder: RecyclerView.ViewHolder, | |||
direction: Int) { | |||
covidResultAdapter.deleteItem(viewHolder.adapterPosition) | |||
} | |||
} | |||
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) | |||
itemTouchHelper.attachToRecyclerView(binding.covidResultRecyclerView) | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 10:08, 21 March 2021
Introduction
I decided to base my application on the CLEAN architecture and set about looking for great examples.
I found Lopez at https://github.com/lopspower/CleanRxArchitecture which had all of the features I looking for
- Retrofit2
- Room
- RxJava
Implement Use Cases
Model
In the model we need to
- Define data to be displayed
- Data
- Error Message
- Snack Message
- Define Content Type e.g. Error or Data
- Define State e.g. Loading or Retry
- Define constructors for each type of Data
The Data is the actual model data to be displayed. The different Data types are all independent. E.g. we need display the snack message in the same model as the error message.
class CovidResultListViewModel(
val loadingState: LoadingState = LoadingState.NONE,
val contentState: ContentState = ContentState.NONE,
val covidResults: List<CovidResult>? = null,
var errorMessage: String? = null,
val snackMessage: String? = null) {
companion object {
fun createData(data: List<CovidResult>?) =
CovidResultListViewModel(contentState = ContentState.CONTENT, covidResults = data)
fun createLoading() =
CovidResultListViewModel(loadingState = LoadingState.LOADING, contentState = ContentState.CONTENT)
fun createRetryLoading() =
CovidResultListViewModel(loadingState = LoadingState.RETRY, contentState = ContentState.ERROR)
fun createError(error: String) =
CovidResultListViewModel(contentState = ContentState.ERROR, errorMessage = error)
fun createSnack(snackMessage: String) =
CovidResultListViewModel(contentState = ContentState.CONTENT, snackMessage = snackMessage)
}
}
Presenter
In the presenter we need to
- Pass the Use Cases we which to execute
- Attach Connect the Presenter to the View
- Create Delegates for each function we want to execute
- Pass the Delegates to the Model and Subscribe
- Define functions to Execute the Use and Create a Model
Most of the functionality is very straight forward. It is the presenter functions which were the most complex. I have added comments to make this clearer.
class CovidResultListPresenter
@Inject constructor(
private val getListCovidResultUseCase: GetListCovidResultUseCase,
private val getListCountryPreferencesUseCase: GetListCountryPreferencesUseCase,
private val deleteCountryPreferenceUseCase: DeleteCountryPreferenceUseCase,
private val scheduler: Scheduler,
errorMessageFactory: ErrorMessageFactory
) : BasePresenter<CovidResultListView, CovidResultListViewModel>(errorMessageFactory) {
private val TAG = CovidResultListPresenter::class.java.simpleName
override fun attach(view: CovidResultListView) {
val loadDataDelegate =
view.intentLoadData().flatMap {
loadDataPresenter()
}
val refreshDataDelegate =
view.intentRefreshData().flatMap {
refreshDataPresenter()
}
val retryDataDelegate =
view.intentRetryData().flatMap {
retryDataPresenter()
}
val removeCountryDelegate =
view.intentRemoveCountry().flatMap {
removeCountryPresenter(it)
}
subscribeViewModel(
view,
loadDataDelegate,
retryDataDelegate,
refreshDataDelegate,
removeCountryDelegate,
)
view.loadData()
}
private fun loadDataPresenter(): Observable<CovidResultListViewModel> =
// Get the List of Countries from Room
getListCountryPreferencesUseCase.execute().toObservable()
.flatMap {
// Get the Covid Results for List of Countries from Rest API
getListCovidResultUseCase.execute(
it.map { it.alpha2Code }
).toObservable()
// Create Data to display
.map { CovidResultListViewModel.createData(it) }
// Force Loading to be the first View the user sees
.startWithSingle(CovidResultListViewModel.createLoading())
// Handle Error
.onErrorReturn { onError(it) }
}
private fun refreshDataPresenter(): Observable<CovidResultListViewModel> =
// Get the List of Countries from Room
getListCountryPreferencesUseCase.execute().toObservable()
.flatMap {
// Get the Covid Results for List of Countries from Rest API
getListCovidResultUseCase.execute(
it.map { it.alpha2Code }
).toObservable()
// Create Data to display
.map { CovidResultListViewModel.createData(it) }
// Handle Error Create A Snack Message
.onErrorReturn { CovidResultListViewModel.createSnack(getErrorMessage(it)) }
}
private fun retryDataPresenter(): Observable<CovidResultListViewModel> =
// Get the List of Countries from Room
getListCountryPreferencesUseCase.execute().toObservable()
.flatMap {
// Get the Covid Results for List of Countries from Rest API
getListCovidResultUseCase.execute(
it.map { it.alpha2Code }
).toObservable()
// Create Data to display
.map { CovidResultListViewModel.createData(it) }
// The Starts With ensures this is the first Emitted value
// Sets the State to Retry and the Content to Error
.startWithSingle(CovidResultListViewModel.createRetryLoading())
// The DelayFunction is executed if an Error occurs
.onErrorResumeNext(DelayFunction<CovidResultListViewModel>(scheduler))
// On Error Create A Snack Message
.onErrorReturn { onError(it) }
}
private fun removeCountryPresenter(countryChoice: CountryChoice): Observable<CovidResultListViewModel> =
// Delete the Country from Room
deleteCountryPreferenceUseCase.execute(countryChoice).toSingleDefault(Unit).toObservable()
.flatMap {
// Get the List of Countries from Room
getListCountryPreferencesUseCase.execute().toObservable()
.flatMap {
// Get the Covid Results for List of Countries from Rest API
getListCovidResultUseCase.execute(
it.map { it.alpha2Code }
).toObservable()
// Create Data to display
.map { CovidResultListViewModel.createData(it) }
// Force Loading to be the first View the user sees
.startWithSingle(CovidResultListViewModel.createLoading())
// Handle Error
.onErrorReturn { onError(it) }
}
}
private fun onError(error: Throwable): CovidResultListViewModel =
CovidResultListViewModel.createError(getErrorMessage(error))
}
View
In the View we need to
- Connect/Disconnect the View to the Presenter
- Provide Function which Emits Observable
- Render the Model returned from the Presenter
Connect/Disconnect the View to the Presenter
When we rotate the device the Fragment is destroyed. We need to reconnect to the Presenter and Disconnect.
override fun onResume() {
super.onResume()
presenter.attach(this)
}
override fun onPause() {
super.onPause()
presenter.detach()
}
Provide Function which Emits Observable
When the user does something to the UI we need to trigger the appropriate action to fire in the Presenter.
// Define functions to emit observable
override fun intentLoadData(): Observable<Unit> =
loadDataIntent
override fun intentRefreshData(): Observable<Unit> =
binding.swipeRefreshLayout.refreshes()
override fun intentRetryData(): Observable<Unit> =
binding.errorLayoutId.btnErrorRetry.clicks()
override fun intentRemoveCountry(): Observable<CountryChoice> =
covidResultAdapter.countryChoiceSwipeIntent
To Manually force an action we omit an value to the observable.
override fun loadData() = loadDataIntent.onNext(Unit)
Render
Each time a Use Case is executed a Model is returned in the Resonse handled by the View.
override fun render(viewModel: CovidResultListViewModel) {
showLoading(viewModel.loadingState == LoadingState.LOADING)
showRefreshingLoading(binding.swipeRefreshLayout, false)
showRetryLoading(viewModel.loadingState == LoadingState.RETRY)
showContent(binding.content, viewModel.contentState == ContentState.CONTENT)
showError(viewModel.contentState == ContentState.ERROR)
renderData(viewModel.covidResults)
renderError(viewModel.errorMessage)
renderSnack(viewModel.snackMessage)
}
Render Show
These function are responsible to setting the Content type, and the Loading State, so basically control what is seen by the user.
private fun showLoading(visible: Boolean) {
binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun showRefreshingLoading(swipeRefreshLayout: SwipeRefreshLayout, visible: Boolean) {
swipeRefreshLayout.isRefreshing = visible
}
private fun showRetryLoading(visible: Boolean) {
binding.errorLayoutId.btnErrorRetry.isClickable = !visible
binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
private fun showContent(content: View, visible: Boolean) {
content.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun showError(visible: Boolean) {
binding.errorLayoutId.viewError.visibility = if (visible) View.VISIBLE else View.GONE
}
Render Render
Sounds cool. These functions render either the Data, Error or Snack
private fun renderData(covidResultList: List<CovidResult>?) {
covidResultList?.also {
val data = it.toMutableList()
if((activity as CovidResultListActivity).sorted) {
data.sort()
}
covidResultAdapter.covidResultList = data
binding.covidResultRecyclerView.scrollToPosition(0)
}
}
private fun renderError(messageError: String?) {
messageError?.also { binding.errorLayoutId.textErrorDescription.text = it }
}
private fun renderSnack(message: String?) {
message?.also {
activity?.also { activity ->
Snackbar.make(
binding.content,
it, Snackbar.LENGTH_LONG
).show()
}
}
}
Swipe And Delete
I could not find a great way to implement the touch listener with the same feel as this implementation. In the View I created a ItemTouchHelper(itemTouchHelper. This defined the Swipe left and Right better than I could using gesture. Basically the deleteItem() is called on the adapter. I would have liked to have this could in the Adapter but a job for next time I think.
fun initTouch() {
val itemTouchHelperCallback =
object :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int) {
covidResultAdapter.deleteItem(viewHolder.adapterPosition)
}
}
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(binding.covidResultRecyclerView)
}
Refreshing
Android provides the SwipeRefreshLayout which is demonstrated below
Quite liked the approach of on by default. To implement this we
- Wrap the RecyclerView in it
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/covid_result_recycler_view"
android:layout_width="match_parent"
...
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>