Building a Number Prediction and followup widget

πŸ“˜

Minimum SDK Version

This is a guide on building a custom Number Prediction and followup Widget. For an overview of the Custom Widget UI system see Custom Widget UI.

Number Prediction Widget Model

The Number Prediction Widget Model is responsible for providing prediction specific data and remote apis to submit prediction.

Number Prediction Data(object of LiveLikeWidget class)
The Number Prediction Data provides data about the Number Prediction Widget such as the question and the options consisting of imageUrl and description.

The model also provides metadata about the widget such as the Date that it was created or the timeout duration set by the Producer.

Note: Use options in LiveLikeWidget class for the Number Prediction.
Example show below:-

widgetData?.let { liveLikeWidget ->
            liveLikeWidget.options?.let { option ->
                if (option.size > 2) {
                    binding.rcylPredictionList.layoutManager =
                        GridLayoutManager(
                            context,
                            2
                        )
                }
                val adapter =
                    PredictionListAdapter(
                        context,
                        isImage,
                        ArrayList(option.map { item -> item!! })
                    )
                binding.rcylPredictionList.adapter = adapter
                  }
                }

lockInVote
For submitting the predictions you need to call lockInVote(options:List), with list of NumberPredictionVotes (consisting of the optionId and the number). It is mandatory to submit the prediction for all the options.

numberPredictionWidgetViewModel?.lockInVote(optionList)

Interaction History
To load the interaction history, you can call the loadInteractionHistory method
Example:-:

numberPredictionWidgetViewModel?.loadInteractionHistory(object :
            LiveLikeCallback<List<NumberPredictionWidgetUserInteraction>>() {
            override fun onResponse(
                result: List<NumberPredictionWidgetUserInteraction>?,
                error: String?
            ) {
                if (!result.isNullOrEmpty()) {
                   // interaction results
                }
            }
        })

Number Prediction FollowUp Widget Model

The follow-up model has the same functions as the above one except lockInVote.

getPredictionVotes()
This method on the model allows you to retrieve the predicted vote list, on which user voted for the prediction associated with this follow up

val votedList = followUpWidgetViewModel?.getPredictionVotes()

claimRewards()
claims the rewards on the number prediction follow up using a user’s prediction
returns nothing but notifies leaderboard clients

followUpWidgetViewModel?.claimRewards()

Full Number prediction sample with followup

class CustomNumberPredictionWidget :
    ConstraintLayout {
    var numberPredictionWidgetViewModel: NumberPredictionWidgetModel? = null
    var followUpWidgetViewModel: NumberPredictionFollowUpWidgetModel? = null
    private lateinit var binding: CustomNumberPredictionWidgetBinding
    var isImage = false
    var isFollowUp = false

    constructor(context: Context) : super(context) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        init()
    }


    private fun init() {
        binding = CustomNumberPredictionWidgetBinding.inflate(
            LayoutInflater.from(context),
            [email protected],
            true
        )
    }


    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        var widgetData = numberPredictionWidgetViewModel?.widgetData
        if (isFollowUp) {
            widgetData = followUpWidgetViewModel?.widgetData
        }

        widgetData?.let { liveLikeWidget ->
            liveLikeWidget.options?.let { option ->
                if (option.size > 2) {
                    binding.rcylPredictionList.layoutManager =
                        GridLayoutManager(
                            context,
                            2
                        )
                }

                val adapter =
                    PredictionListAdapter(
                        context,
                        isImage,
                        ArrayList(option.map { item -> item!! })
                    )
                binding.rcylPredictionList.adapter = adapter
                binding.txt.text = liveLikeWidget.question

                getInteractedData(adapter) // get user interaction

                setOnClickListeners(adapter)
                if (isFollowUp) {
                    binding.btn1.visibility = View.GONE
                    claim_rewards.visibility = View.VISIBLE
                } else {
                    binding.btn1.visibility = View.VISIBLE
                    claim_rewards.visibility = View.GONE
                }

                if (isFollowUp) {
                    val votedList = followUpWidgetViewModel?.getPredictionVotes()
                    votedList?.forEach { op ->
                        adapter.predictionMap[op?.optionId!!] = op.number ?: 0
                    }
                    adapter.isFollowUp = true
                } else {
                    result_tv.visibility = GONE
                }
            }

        }
    }

    private fun setOnClickListeners(adapter: PredictionListAdapter) {
        // predict button click
        binding.btn1.setOnClickListener {
            if (!isFollowUp) {
                val optionList = submitVoteRequest(adapter)
                numberPredictionWidgetViewModel?.lockInVote(optionList)
            }
        }
        binding.imgClose.setOnClickListener {
            finish()
        }

        claim_rewards.setOnClickListener{
            followUpWidgetViewModel?.claimRewards()
        }
    }



    private fun submitVoteRequest(adapter: PredictionListAdapter):List<NumberPredictionVotes> {
        val optionList = mutableListOf<NumberPredictionVotes>()
        val maps = adapter.getPredictedScore()
        if(maps.isNullOrEmpty()){
            val options = numberPredictionWidgetViewModel?.widgetData?.options
            for(item in options!!){
                optionList.add(
                    NumberPredictionVotes(
                        optionId = item?.id!!,
                        number = 0
                    )
                )
            }
        }
        return optionList
    }

    //get user interacted data from load history api
    private fun getInteractedData(adapter: PredictionListAdapter) {
        val lists = numberPredictionWidgetViewModel?.getUserInteraction()
        lists?.votes?.let { scores ->
            adapter.setInteractedData(scores)
            adapter.notifyDataSetChanged()
        }
    }

    fun finish() {
        numberPredictionWidgetViewModel?.finish()
        followUpWidgetViewModel?.finish()
    }


    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // numberPredictionWidgetViewModel?.voteResults?.unsubscribe(this)
    }

      // adapter 
    class PredictionListAdapter(
        private val context: Context,
        private val isImage: Boolean,
        val list: ArrayList<OptionsItem>
    ) : RecyclerView.Adapter<PredictionListAdapter.PredictionListItemViewHolder>() {

        var predictionMap: HashMap<String, Int> = HashMap()
        var isFollowUp = false

        fun getPredictedScore(): HashMap<String, Int> {
            return predictionMap
        }

        class PredictionListItemViewHolder(view: View) : RecyclerView.ViewHolder(view)

        override fun onCreateViewHolder(
            parent: ViewGroup,
            viewType: Int
        ): PredictionListItemViewHolder {
            return PredictionListItemViewHolder(
                LayoutInflater.from(parent.context!!).inflate(
                    R.layout.custom_number_prediction_item,
                    parent, false
                )
            )
        }

        override fun onBindViewHolder(
            holder: PredictionListItemViewHolder,
            position: Int
        ) {
            val item = list[position]

            if (isImage) {
                Glide.with(context)
                    .load(item.imageUrl)
                    .into(
                        holder.itemView.img_1
                    )
                holder.itemView.text_1.visibility = View.GONE
                holder.itemView.img_1.visibility = View.VISIBLE

            } else {
                holder.itemView.text_1.text = item.description
                holder.itemView.text_1.visibility = View.VISIBLE
                holder.itemView.img_1.visibility = View.GONE
            }

            if (isFollowUp) {
                holder.itemView.option_view_1.text = "${predictionMap[item.id!!] ?: 0}"
                holder.itemView.correct_op.text =  item.correctNumber.toString()
                holder.itemView.correct_op.visibility = View.VISIBLE
            }else{
                holder.itemView.correct_op.visibility = View.GONE
            }

            if (!isFollowUp) {
                if (item.number != null) {
                    holder.itemView.option_view_1.text = item.number.toString()
                } else {
                    holder.itemView.option_view_1.text = "0"
                }
            }


            holder.itemView.plus.setOnClickListener {
                if (!isFollowUp) {
                    val updatedScore = holder.itemView.option_view_1.text.toString().toInt() + 1
                    holder.itemView.option_view_1.text = updatedScore.toString()
                    predictionMap[item.id!!] = updatedScore
                }

            }

            holder.itemView.minus.setOnClickListener {
                if (!isFollowUp) {
                    val updatedScore = holder.itemView.option_view_1.text.toString().toInt() - 1
                    holder.itemView.option_view_1.text = updatedScore.toString()
                    predictionMap[item.id!!] = updatedScore
                }
            }
        }


        override fun getItemCount(): Int = list.size

        fun setInteractedData(interactedList: List<NumberPredictionVotes>) {
            for (i in list.indices) {
                if (list[i].id == interactedList[i].optionId) list[i].number =
                    interactedList[i].number
            }
        }
    }
}