Android Example App: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(11 intermediate revisions by the same user not shown)
Line 5: Line 5:
*Room
*Room
*RxJava
*RxJava
=Implementing Views=
==Implement Use Cases==
==The Model==
===Model===
The model for the screens has data associated with each type of data to be displayed. With each construction pass the state and either the content type of data.
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
<br>
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.
<syntaxhighlight lang="kotlin">
<syntaxhighlight lang="kotlin">
class CovidResultListViewModel(
class CovidResultListViewModel(
Line 15: Line 24:
     var errorMessage: String? = null,
     var errorMessage: String? = null,
     val snackMessage: String? = null) {
     val snackMessage: String? = null) {
...
 
     companion object {
     companion object {


Line 23: Line 32:
         fun createLoading() =
         fun createLoading() =
             CovidResultListViewModel(loadingState = LoadingState.LOADING, contentState = ContentState.CONTENT)
             CovidResultListViewModel(loadingState = LoadingState.LOADING, contentState = ContentState.CONTENT)
        fun createRetryLoading() =
            CovidResultListViewModel(loadingState = LoadingState.RETRY, contentState = ContentState.ERROR)


         fun createError(error: String) =
         fun createError(error: String) =
             CovidResultListViewModel(contentState = ContentState.ERROR, errorMessage = error)
             CovidResultListViewModel(contentState = ContentState.ERROR, errorMessage = error)


...
        fun createSnack(snackMessage: String) =
            CovidResultListViewModel(contentState = ContentState.CONTENT, snackMessage = snackMessage)
    }
}
</syntaxhighlight>


===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
<br>
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.
<syntaxhighlight lang="kotlin">
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))
}
}
</syntaxhighlight>
===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.
<syntaxhighlight lang="kotlin">
    override fun onResume() {
        super.onResume()
        presenter.attach(this)
    }


    override fun onPause() {
        super.onPause()
        presenter.detach()
    }
</syntaxhighlight>
</syntaxhighlight>
====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.
<syntaxhighlight lang="kotlin">


    // Define functions to emit observable
    override fun intentLoadData(): Observable<Unit> =
        loadDataIntent


==The Presenter==
    override fun intentRefreshData(): Observable<Unit> =
The Presenter has a render method which is responsible for rendering the data.
        binding.swipeRefreshLayout.refreshes()
 
    override fun intentRetryData(): Observable<Unit> =
        binding.errorLayoutId.btnErrorRetry.clicks()
 
    override fun intentRemoveCountry(): Observable<CountryChoice> =
        covidResultAdapter.countryChoiceSwipeIntent
</syntaxhighlight>
To Manually force an action we omit an value to the observable.
<syntaxhighlight lang="kotlin">
    override fun loadData() = loadDataIntent.onNext(Unit)
</syntaxhighlight>
====Render====
Each time a Use Case is executed a Model is returned in the Resonse handled by the View.
<syntaxhighlight lang="kotlin">
<syntaxhighlight lang="kotlin">
     override fun render(viewModel: CovidResultListViewModel) {
     override fun render(viewModel: CovidResultListViewModel) {
         showLoading(viewModel.loadingState == LoadingState.LOADING)
         showLoading(viewModel.loadingState == LoadingState.LOADING)
         showRefreshingLoading(binding.swipeRefreshLayout, false)
         showRefreshingLoading(binding.swipeRefreshLayout, false)
Line 51: Line 228:
     }
     }
</syntaxhighlight>
</syntaxhighlight>
Each of the show methods set the flag appropriately for the data to be displayed.
====Render Show====
These function are responsible to setting the Content type, and the Loading State, so basically control what is seen by the user.
<syntaxhighlight lang="kotlin">
<syntaxhighlight lang="kotlin">
     protected fun showLoading(visible: Boolean) {
     private fun showLoading(visible: Boolean) {
         binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.GONE
         binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.GONE
     }
     }


     protected fun showRefreshingLoading(swipeRefreshLayout: SwipeRefreshLayout, visible: Boolean) {
     private fun showRefreshingLoading(swipeRefreshLayout: SwipeRefreshLayout, visible: Boolean) {
         swipeRefreshLayout.isRefreshing = visible
         swipeRefreshLayout.isRefreshing = visible
     }
     }


     protected fun showRetryLoading(visible: Boolean) {
     private fun showRetryLoading(visible: Boolean) {
         binding.errorLayoutId.btnErrorRetry.isClickable = !visible
         binding.errorLayoutId.btnErrorRetry.isClickable = !visible
         binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.INVISIBLE
         binding.progressLayoutId.progress.visibility = if (visible) View.VISIBLE else View.INVISIBLE
     }
     }
</syntaxhighlight>


==Use Case Example Refresh==
    private fun showContent(content: View, visible: Boolean) {
We want to allow the user to refresh the data. In our example it doesn't make much sense as the Covid Results only change daily but hopefully you get the idea.
        content.visibility = if (visible) View.VISIBLE else View.GONE
[[File:Android Example App Refresh.png|200px]]<br>
    }
When the user pulls down on the list of countries we want the screen to
 
*start showing the SwipeRefreshLayout
     private fun showError(visible: Boolean) {
*load new data
         binding.errorLayoutId.viewError.visibility = if (visible) View.VISIBLE else View.GONE
*stop showing the SwipeRefreshLayout
    }
To achieve this we need to
*Create Hook in view
*Create Delegate in Presenter
===Create Hook in the View (Fragment)===
In the View (Fragment) hook up the pulling of the recylerview to the view (fragment)
<syntaxhighlight lang="kotlin">
     override fun intentRefreshData(): Observable<Boolean> =
         binding.swipeRefreshLayout.refreshes().map { true }
</syntaxhighlight>
</syntaxhighlight>
===Attach and Create Function to Call (Presenter)===
====Render Render====
 
Sounds cool. These functions render either the Data, Error or Snack
====Attach====
In the attach function we need to create our delegate functions and add the subscriptions to the model.
<syntaxhighlight lang="kotlin">
<syntaxhighlight lang="kotlin">
     override fun attach(view: CovidResultListView) {
     private fun renderData(covidResultList: List<CovidResult>?) {
...
        covidResultList?.also {
        val refreshDataDelegate =
            val data = it.toMutableList()
             view.intentRefreshData().flatMap {
             if((activity as CovidResultListActivity).sorted) {
                 refreshDataPresenter()
                 data.sort()
             }
             }
            covidResultAdapter.covidResultList = data
            binding.covidResultRecyclerView.scrollToPosition(0)
        }
    }


        subscribeViewModel(
    private fun renderError(messageError: String?) {
            view,
         messageError?.also { binding.errorLayoutId.textErrorDescription.text = it }
            refreshDataDelegate,
            refreshCovidResultListDelegate,
            loadCountryPreferencesDelegate,
            removeFavoriteCountryChoiceDelegate,
         )
     }
     }
</syntaxhighlight>


====Create function====
     private fun renderSnack(message: String?) {
This function executes the void/Unit use case GetSelectedCountryChoiceListUseCase which returns a list of country codes. E.g. NZ, GB, AU. This is passed to the second use case GetListCovidResultUseCase. Probably arguable where these should be in the repository or the usecase.
         message?.also {
<syntaxhighlight lang="kotlin">
             activity?.also { activity ->
     private fun refreshDataPresenter(): Observable<CovidResultListViewModel> =
                 Snackbar.make(
         getSelectedCountryChoiceListUseCase.execute().toObservable()
                     binding.content,
             .flatMap {
                     it, Snackbar.LENGTH_LONG
                 getListCovidResultUseCase.execute(it.map { it.alpha2Code }).toObservable()
                ).show()
                     .map { CovidResultListViewModel.createData(it) }
                     .onErrorReturn {
                        CovidResultListViewModel.createSnack(getErrorMessage(it))
                    }
             }
             }
        }
    }
</syntaxhighlight>
</syntaxhighlight>


 
==Swipe And Delete==
==Implement Use Case==
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.
===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
<br>
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.
<syntaxhighlight lang="kotlin">
<syntaxhighlight lang="kotlin">
class CovidResultListViewModel(
    fun initTouch() {
    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 {
        val itemTouchHelperCallback =
            object :
                ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {


        fun createData(data: List<CovidResult>?) =
                override fun onMove(
            CovidResultListViewModel(contentState = ContentState.CONTENT, covidResults = data)
                    recyclerView: RecyclerView,
                    viewHolder: RecyclerView.ViewHolder,
                    target: RecyclerView.ViewHolder
                ): Boolean {
                    return false
                }


        fun createLoading() =
                override fun onSwiped(
            CovidResultListViewModel(loadingState = LoadingState.LOADING, contentState = ContentState.CONTENT)
                    viewHolder: RecyclerView.ViewHolder,
 
                    direction: Int) {
        fun createRetryLoading() =
                    covidResultAdapter.deleteItem(viewHolder.adapterPosition)
            CovidResultListViewModel(loadingState = LoadingState.RETRY, contentState = ContentState.ERROR)
                }
 
             }
        fun createError(error: String) =
             CovidResultListViewModel(contentState = ContentState.ERROR, errorMessage = error)


         fun createSnack(snackMessage: String) =
         val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
            CovidResultListViewModel(contentState = ContentState.CONTENT, snackMessage = snackMessage)
        itemTouchHelper.attachToRecyclerView(binding.covidResultRecyclerView)
     }
     }
}
</syntaxhighlight>
</syntaxhighlight>
===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
===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
We want to allow the use to


==Refreshing==
==Refreshing==

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>