I am building a Custom View in a library module that every 30 seconds displays a question (obtained from a REST Api) and allows the user to select one of the possible answers.
Also, I need to make use of that library in an app, displaying a video underneath the question.
All the business and UI logic should be handled in the library.
I created an ErrorListener interface to be able to show the error messages from the library in the app's Activity.
Do I need to have a method in my Custom view to remove the listener if the activity is paused or stopped? Is there anything else I should have into account for when the activity is paused/stopped or destroyed? (like stopping the intervalsHandler, countDownTimer, etc.)
My Custom View
class BuffView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: LinearLayout(context, attrs) {
private val apiErrorHandler = ApiErrorHandler()
private val getBuffUseCase = GetBuffUseCase(apiErrorHandler)
private val intervalsHandler = Handler()
private val buffView: LinearLayout = inflate(context, R.layout.buff_view, this) as LinearLayout
private var errorListener: ErrorListener? = null
private var countDownTimer: CountDownTimer? = null
private var buffIdCount = 1
private var getBuffs = false
fun init() {
getBuffs = true
getBuff()
}
private fun getBuff() {
if (!getBuffs) return
getBuffUseCase.invoke(Params(buffIdCount.toLong()), object : UseCaseResponse<Buff> {
override fun onSuccess(result: Buff) {
if (isDataValid(result)) displayBuff(result) else hideBuff()
}
override fun onError(errorModel: ErrorModel?) {
errorListener?.onError(errorModel?.message ?: context.getString(R.string.generic_error_message))
hideBuff()
}
})
if (buffIdCount < TOTAL_BUFFS ) {
intervalsHandler.postDelayed({
buffIdCount++
getBuff()
stopCountDownTimer()
}, REQUEST_BUFF_INTERVAL_TIME)
}
}
private fun isDataValid(buff: Buff): Boolean {
if (buff.author.firstName.isEmpty() && buff.author.lastName.isEmpty()) {
showErrorInvalidData(context.getString(R.string.author_reason_error_message))
return false
}
if (buff.question.title.isEmpty()) {
showErrorInvalidData(context.getString(R.string.question_reason_error_message))
return false
}
if (buff.timeToShow == null || buff.timeToShow < 0) {
showErrorInvalidData(context.getString(R.string.timer_reason_error_message))
return false
}
if (buff.answers == null) {
showErrorInvalidData(context.getString(R.string.answers_reason_error_message))
return false
}
return true
}
private fun showErrorInvalidData(reason: String) {
errorListener?.onError(context.getString(R.string.data_incomplete_error_message, reason))
}
private fun displayBuff(buff: Buff) {
setQuestion(buff.question.title)
setAuthor(buff.author)
setAnswer(buff.answers!!)
setProgressBar(buff.timeToShow!!)
setCloseButton()
invalidate()
showBuff()
}
private fun setQuestion(questionText: String) {
question_text.text = questionText
}
private fun setAuthor(author: Buff.Author) {
val firstName = author.firstName
val lastName = author.lastName
sender_name.text = "$firstName $lastName"
Glide.with(context)
.load(author.image)
.into(sender_image)
}
private fun setAnswer(answers: List<Buff.Answer>) {
val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
answersContainer.removeAllViews()
for(answer in answers) {
val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
R.layout.buff_answer,
answersContainer,
false
)
answer.answerImage?.x0?.url?.let {
Glide.with(context)
.load(it)
.into(answerView.answer_image)
}
answerView.setOnClickListener {
answerView.background = ContextCompat.getDrawable(
context,
R.drawable.answer_selected_bg
)
answerView.answer_text.setTextColor(
ContextCompat.getColor(
context,
android.R.color.white
)
)
//freeze timer
stopCountDownTimer()
//hideView() after 2 seconds
it.postDelayed({
hideBuff()
}, HIDE_BUFF_AFTER_SELECTED_ANSWER_DURATION)
}
answerView.answer_text?.text = answer.title
answersContainer.addView(answerView)
}
}
private fun setProgressBar(timeToShow: Int) {
question_time_progress.max = timeToShow
countDownTimer = object : CountDownTimer(
timeToShow * ONE_SECOND_INTERVAL,
ONE_SECOND_INTERVAL
) {
override fun onTick(millisUntilFinished: Long) {
question_time.text = (millisUntilFinished / ONE_SECOND_INTERVAL).toString()
question_time_progress.progress = timeToShow - (millisUntilFinished / ONE_SECOND_INTERVAL).toInt()
}
override fun onFinish() {
hideBuff()
}
}.start()
}
private fun showBuff() {
buffView.visibility = VISIBLE
}
private fun hideBuff() {
buffView.visibility = GONE
}
private fun stopCountDownTimer() {
countDownTimer?.cancel()
}
private fun setCloseButton() {
buff_close.setOnClickListener {
hideBuff()
stopCountDownTimer()
}
}
fun addErrorListener(errorListener: ErrorListener) {
this.errorListener = errorListener
}
}
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
video.setVideoPath(url);
video.setOnPreparedListener {
video.start()
}
buff_view.addErrorListener(object : ErrorListener {
override fun onError(msg: String) {
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
}
})
buff_view.init()
}
}
activity_main.xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
tools:context=".ui.MainActivity">
<VideoView
android:id="@+id/video"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:keepScreenOn="true"
android:textSize="50sp"
android:textStyle="bold" />
<com.buffup.sdk.ui.BuffView
android:id="@+id/buff_view"
android:layout_gravity="bottom|start"
android:padding="16dp"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>