Rewritten MessageListActivity to be compliant with IOSP

Full compliance was not possible at every step but largely the
operations are now separated and can be unit-tested in isolation.

As part of this rewrite, the onClick event handler is now set
programmatically and no longer in the layout file.
This commit is contained in:
Jim Martens 2019-07-18 21:35:24 +02:00
parent 36b5f1cda8
commit 147e5ce56a
2 changed files with 188 additions and 83 deletions

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.udevel.widgetlab.TypingIndicatorView
import kotlinx.android.synthetic.main.activity_message_list.view.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.collections.ArrayList
@ -26,11 +27,21 @@ class MessageListActivity : AppCompatActivity() {
private lateinit var adapter: ArrayAdapter<CharSequence>
private var showSpinner = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_message_list)
initVariables()
adapter = initSpinner(model, spinner)
bindMessageObserver(viewAdapter, model)
initButton(horizontalLayout, ::step, spinner)
val messages = initFirstChoice(viewAdapter, model, horizontalLayout)
receiveMessages(messages, model, typingIndicator, recyclerView, horizontalLayout)
}
private fun initVariables() {
val viewManager = LinearLayoutManager(this)
viewManager.stackFromEnd = true
viewManager.reverseLayout = false
@ -54,42 +65,88 @@ class MessageListActivity : AppCompatActivity() {
// specify an viewAdapter (see also next example)
adapter = viewAdapter
}
}
private fun initSpinner(model: MessageListViewModel,
spinner: Spinner): ArrayAdapter<CharSequence> {
val spinnerOptions: ArrayList<CharSequence>? = model.spinnerOptions.value
if (spinnerOptions != null) {
adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, spinnerOptions)
val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, spinnerOptions)
// Specify the layout to use when the list of choices appears
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
arrayAdapter.setNotifyOnChange(true)
// Apply the adapter to the spinner
spinner.adapter = arrayAdapter
return arrayAdapter
}
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
adapter.setNotifyOnChange(true)
// Apply the adapter to the spinner
spinner.adapter = adapter
return ArrayAdapter(this, android.R.layout.simple_spinner_item, ArrayList<CharSequence>())
}
private fun bindMessageObserver(viewAdapter: MessageListAdapter, model: MessageListViewModel) {
val messageObserver = Observer<ArrayList<Message>> { messages ->
viewAdapter.replaceMessages(messages)
}
model.getMessages().observe(this, messageObserver)
initFirstChoice()
}
fun sendMessage(view: View) {
val text: String = spinner.selectedItem.toString()
private fun initButton(horizontalLayout: LinearLayout, step: (String) -> Unit, spinner: Spinner) {
horizontalLayout.button_send.setOnClickListener {
val text: String = spinner.selectedItem.toString()
step(text)
}
}
private fun initFirstChoice(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
model: MessageListViewModel,
horizontalLayout: LinearLayout): List<Message> {
val isInitialized = model.initialized.value
return if (isInitialized != null && !isInitialized) {
model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.components))
adapter.notifyDataSetChanged()
model.initialized.value = true
listOf(Message(getString(R.string.greeting), false, System.currentTimeMillis()))
} else {
val spinnerVisible = model.spinnerVisible.value
if (spinnerVisible != null && spinnerVisible) horizontalLayout.visibility = View.VISIBLE
listOf()
}
}
private fun step(selection: String) {
sendMessage(selection, model, recyclerView, horizontalLayout)
val messages = branch(
selection,
model, adapter, ::getSolutionArray,
::componentSelected,
::problemSelected,
::yesNoSelected,
::triedAnswer,
::doesItWorkAnswer,
::didItWorkQuestion,
::pleaseTry,
::problemSolved,
::nextSolutionQuestion,
::doNotShowSpinner)
receiveMessages(messages, model, typingIndicator, recyclerView, horizontalLayout, showSpinner)
}
private fun sendMessage(text: String, model: MessageListViewModel,
recyclerView: RecyclerView, horizontalLayout: LinearLayout) {
val message = Message(text, true, System.currentTimeMillis())
model.add(message)
recyclerView.scrollToPosition(model.size() - 1)
horizontalLayout.visibility = View.GONE
model.spinnerVisible.value = false
nextStage(text)
}
private fun receiveMessage(message: Message, showSpinner: Boolean = true) {
receiveMessages(listOf(message), showSpinner)
}
private fun receiveMessages(messages: List<Message>, showSpinner: Boolean = true) {
private fun receiveMessages(messages: List<Message>,
model: MessageListViewModel,
typingIndicator: View,
recyclerView: RecyclerView,
horizontalLayout: LinearLayout,
showSpinner: Boolean = true) {
model.uiScope.launch {
for (message in messages) {
typingIndicator.visibility = View.VISIBLE
@ -98,70 +155,88 @@ class MessageListActivity : AppCompatActivity() {
model.add(message)
recyclerView.scrollToPosition(model.size() - 1)
}
if (showSpinner) {
if (messages.isNotEmpty() && showSpinner) {
horizontalLayout.visibility = View.VISIBLE
model.spinnerVisible.value = true
}
}
}
private fun nextStage(selection: String) {
when (selection) {
private fun doNotShowSpinner() {
showSpinner = false
}
private fun branch(selection: String,
model: MessageListViewModel,
adapter: ArrayAdapter<CharSequence>,
getSolutionArray: (String) -> Array<String>,
componentSelected: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
problemSelected: (MessageListViewModel, ArrayAdapter<CharSequence>,
(String) -> Array<String>) -> List<Message>,
yesNoSelected: (String,
MessageListViewModel,
ArrayAdapter<CharSequence>,
triedAnswer: (String, MessageListViewModel,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>) -> List<Message>,
doesItWorkAnswer: (String, MessageListViewModel, ArrayAdapter<CharSequence>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>) -> List<Message>,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>) -> List<Message>,
triedAnswer: (String, MessageListViewModel,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>) -> List<Message>,
doesItWorkAnswer: (String, MessageListViewModel, ArrayAdapter<CharSequence>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>) -> List<Message>,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
doNotShowSpinner: () -> Unit): List<Message> {
return when (selection) {
in resources.getStringArray(R.array.components) -> {
// first selection
model.component.value = Component.valueOf(selection.toUpperCase())
secondChoice()
componentSelected(model, adapter)
}
in resources.getStringArray(R.array.monitor_problems),
in resources.getStringArray(R.array.pc_problems),
in resources.getStringArray(R.array.printer_problems),
in resources.getStringArray(R.array.tv_problems),
in resources.getStringArray(R.array.router_problems) -> {
// second selection
model.problem.value = Problem.valueOf(selection.toUpperCase().replace(' ', '_'))
solutionQuestion()
problemSelected(model, adapter, getSolutionArray)
}
in resources.getStringArray(R.array.binary) -> {
when (model.currentSolutionState.value) {
State.HAVE_YOU_TRIED -> triedAnswer(selection)
State.DID_IT_WORK -> doesItWorkAnswer(selection)
}
yesNoSelected(selection, model, adapter,
triedAnswer, doesItWorkAnswer,
didItWorkQuestion, pleaseTry,
problemSolved, nextSolutionQuestion)
}
getString(R.string.who_are_you) -> {
receiveMessage(Message(
doNotShowSpinner()
listOf(Message(
getString(R.string.gardner_second),
false,
System.currentTimeMillis()
), false)
))
}
getString(R.string.can_you_say_sth_different) -> {
receiveMessages(listOf(
doNotShowSpinner()
listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.im_gardner), false, System.currentTimeMillis()),
Message(getString(R.string.okay_saves_time), false, System.currentTimeMillis()),
Message(getString(R.string.its_funny), false, System.currentTimeMillis())),
false)
Message(getString(R.string.its_funny), false, System.currentTimeMillis()))
}
// defacto this case should not happen
else -> listOf()
}
}
private fun initFirstChoice() {
val isInitialized = model.initialized.value
if (isInitialized != null && !isInitialized) {
model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.components))
adapter.notifyDataSetChanged()
model.initialized.value = true
receiveMessage(Message(getString(R.string.greeting), false, System.currentTimeMillis()))
}
else {
val spinnerVisible = model.spinnerVisible.value
if (spinnerVisible != null && spinnerVisible) horizontalLayout.visibility = View.VISIBLE
return
}
}
private fun secondChoice() {
private fun componentSelected(model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>): List<Message> {
model.spinnerOptions.value?.clear()
when (model.component.value) {
Component.PRINTER -> model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.printer_problems))
@ -171,16 +246,18 @@ class MessageListActivity : AppCompatActivity() {
Component.ROUTER -> model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.router_problems))
}
adapter.notifyDataSetChanged()
receiveMessages(listOf(
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.select_problem), false, System.currentTimeMillis())
))
)
}
private fun solutionQuestion() {
private fun problemSelected(model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>,
getSolutionArray: (String) -> Array<String>): List<Message> {
val format = "component_problem"
val stringName = format.replace("component", model.component.value.toString().toLowerCase()).
replace("problem", model.problem.value.toString().toLowerCase())
val stringName = format.replace("component", model.component.value.toString()).
replace("problem", model.problem.value.toString())
model.solutions.value?.addAll(getSolutionArray(stringName))
val firstSolutionQuestion = getString(R.string.have_you_tried, model.solutions.value?. let { it[0] })
@ -189,56 +266,85 @@ class MessageListActivity : AppCompatActivity() {
model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.binary))
adapter.notifyDataSetChanged()
receiveMessages(listOf(
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(firstSolutionQuestion, false, System.currentTimeMillis())
))
)
}
private fun triedAnswer(selection: String) {
when (selection) {
"yes" -> didItWorkQuestion()
private fun yesNoSelected(selection: String, model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>,
triedAnswer: (String,
MessageListViewModel,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>) -> List<Message>,
doesItWorkAnswer: (String,
MessageListViewModel,
ArrayAdapter<CharSequence>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>) -> List<Message>,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>): List<Message> {
val state: State? = model.currentSolutionState.value
if (state != null) {
return when (state) {
State.HAVE_YOU_TRIED -> triedAnswer(selection, model, didItWorkQuestion, pleaseTry)
State.DID_IT_WORK -> doesItWorkAnswer(selection, model, adapter, problemSolved, nextSolutionQuestion)
}
}
// defacto this case should not happen
return listOf()
}
private fun triedAnswer(selection: String, model: MessageListViewModel,
didItWorkQuestion: (MessageListViewModel) -> List<Message>,
pleaseTry: () -> List<Message>): List<Message> {
return when (selection) {
"yes" -> didItWorkQuestion(model)
else -> pleaseTry()
}
}
private fun doesItWorkAnswer(selection: String) {
when (selection) {
"yes" -> problemSolved()
else -> nextSolutionQuestion()
private fun doesItWorkAnswer(selection: String, model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>,
problemSolved: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>,
nextSolutionQuestion: (MessageListViewModel, ArrayAdapter<CharSequence>) -> List<Message>): List<Message> {
return when (selection) {
"yes" -> problemSolved(model, adapter)
else -> nextSolutionQuestion(model, adapter)
}
}
private fun didItWorkQuestion() {
receiveMessages(listOf(
private fun didItWorkQuestion(model: MessageListViewModel): List<Message> {
model.currentSolutionState.value = State.DID_IT_WORK
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.did_it_work), false, System.currentTimeMillis())
))
model.currentSolutionState.value = State.DID_IT_WORK
)
}
private fun pleaseTry() {
receiveMessages(listOf(
private fun pleaseTry(): List<Message> {
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.please_try), false, System.currentTimeMillis()),
Message(getString(R.string.i_wait), false, System.currentTimeMillis()),
Message(getString(R.string.have_you_tried, model.solutions.value?.let { it[(model.nextSolutionIndex.value ?: 1) - 1]}), false, System.currentTimeMillis())
))
)
}
private fun problemSolved() {
private fun problemSolved(model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>): List<Message> {
model.spinnerOptions.value?.clear()
model.spinnerOptions.value?.add(getString(R.string.can_you_say_sth_different))
adapter.notifyDataSetChanged()
receiveMessage(Message(getString(R.string.okay), false, System.currentTimeMillis()))
return listOf(Message(getString(R.string.okay), false, System.currentTimeMillis()))
}
private fun nextSolutionQuestion() {
private fun nextSolutionQuestion(model: MessageListViewModel, adapter: ArrayAdapter<CharSequence>): List<Message> {
if (model.nextSolutionIndex.value == model.solutions.value?.size) {
// we exhausted all options
var summary = """
Component: ${model.component.value.toString().toLowerCase().capitalize()}
Problem: ${model.problem.value.toString().replace('_', ' ').toLowerCase().capitalizeWords()}
Component: ${model.component.value.toString()}
Problem: ${model.problem.value.toString()}
Tried Solutions:
""".trimIndent()
val solutions: ArrayList<CharSequence>? = model.solutions.value
@ -253,7 +359,7 @@ class MessageListActivity : AppCompatActivity() {
model.spinnerOptions.value?.add(getString(R.string.who_are_you))
adapter.notifyDataSetChanged()
receiveMessages(listOf(
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.sorry), false, System.currentTimeMillis()),
Message(getString(R.string.cant_help), false, System.currentTimeMillis()),
@ -261,7 +367,7 @@ class MessageListActivity : AppCompatActivity() {
Message(getString(R.string.okay_wait), false, System.currentTimeMillis()),
Message(getString(R.string.tell_support), false, System.currentTimeMillis()),
Message(summary, false, System.currentTimeMillis())
))
)
}
else {
val solutionQuestion = getString(R.string.have_you_tried, model.solutions.value?.let {it[model.nextSolutionIndex.value ?: 0]})
@ -271,11 +377,11 @@ class MessageListActivity : AppCompatActivity() {
model.spinnerOptions.value?.addAll(resources.getStringArray(R.array.binary))
adapter.notifyDataSetChanged()
receiveMessages(listOf(
return listOf(
Message(getString(R.string.okay), false, System.currentTimeMillis()),
Message(getString(R.string.sorry), false, System.currentTimeMillis()),
Message(solutionQuestion, false, System.currentTimeMillis())
))
)
}
}

View File

@ -72,7 +72,6 @@
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:background="?attr/selectableItemBackground"
android:onClick="sendMessage"
android:text="@string/button_send"
android:gravity="center"
android:layout_gravity="bottom" />