1
\$\begingroup\$

I am practicing Singleton pattern in kotlin.

It is a quiz app which has question tile with four options and one correctIndex.

McqOldActivity.classs

class McqOldActivity : AppCompatActivity() {

    var questions = mutableListOf<QuestionBO>()
    lateinit var mcqPagerAdapter: McqPagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mcq_old)

        val subjectName = "english"
        val levelNum = "0"
        val pathUrl = "rootNode/subRootNode/mcqRoot/$subjectName/$levelNum"
        val myRef = FirebaseDatabase.getInstance().getReference(pathUrl)

        mcqPagerAdapter = McqPagerAdapter(supportFragmentManager,questions)
        view_pager.adapter = mcqPagerAdapter

        val questionListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                dataSnapshot.children.mapNotNullTo(questions) {
                    it.getValue<QuestionBO>(
                        QuestionBO::class.java
                    )
                }
                SingletonModel.updateQuestion(questions)
                mcqPagerAdapter.notifyDataSetChanged()
                view_pager.offscreenPageLimit = questions.size
            }

            override fun onCancelled(databaseError: DatabaseError) {
                println("loadPost:onCancelled ${databaseError.toException()}")
            }
        }

        myRef.addValueEventListener(questionListener)
    }
}

McqPagerAdapter.class

class McqPagerAdapter(fragmentManager: FragmentManager, private val questionList: List<QuestionBO>) : FragmentStatePagerAdapter(fragmentManager){

    override fun getItem(position: Int): Fragment {
        return McqFragment.newInstance(questionList.get(position),position,null)
    }

    override fun getCount(): Int {
        return questionList.size
    }
}

QuestionBO

@Parcelize
data class QuestionBO(
    val CorrectIndex: Int = -1, val Option1: String = "option 1", val Option2: String = "option 2",
    val Option3: String = "option 3", val Option4: String = "option 4",
    val Question: String = "Question"
) : Parcelable

SingletonModel.kt

class SingletonModel {

    companion object {
        ///val instance = SingletonModel()
        var questions = mutableListOf<QuestionBO>()
        var results = mutableListOf<ResultBO>()

        fun updateQuestion(questionsFromServer:MutableList<QuestionBO>){
            questions = questionsFromServer;
            results.clear()
            for(question in questions){
                results.add(ResultBO(question,false,null))
            }
        }

        fun updateSelectedAnswer(indexOfQuestion:Int,userSelectedIndex:Int){
            results.get(indexOfQuestion).isCorrectAnswer = questions.get(indexOfQuestion).CorrectIndex == userSelectedIndex
            results.get(indexOfQuestion).userSelectedOptionIndex = userSelectedIndex
        }

        fun getResult() : MutableList<ResultBO>{
            return results;
        }

        fun clearAll(){
            results.clear()
            questions.clear()
        }
    }
}

I call SingletonModel.updateQuestion(questions) when questions are fetched. and SingletonModel.clearAll() on Destory() .. Am I using Singleton in the right way?

Edit After attempting quiz I have to show result to user ..

val intent = Intent(this@McqOldActivity,ResultActivity::class.java)
            intent.putParcelableArrayListExtra("arrayListKey", ArrayList(SingletonModel.getResult()))
            startActivity(intent)
            SingletonModel.clearAll()

and get it like

var results:MutableList<ResultBO> =intent.getParcelableArrayListExtra<ResultBO>("arrayListKey") as MutableList<ResultBO>

Edit 2

McqFragment.class

class McqFragment: Fragment(),View.OnClickListener {
    var indexOfQuestion:Int? = -1
    var questionBO: QuestionBO? = null
    var onOptionSelectedListener:OnOptionSelectedListener? = null
    companion object {
        private const val KEY_QUESTION_BO = "questionKey"
        private const val KEY_POSITION = "positionKey"

        fun newInstance(questionBO: QuestionBO, position: Int,listener:OnOptionSelectedListener?) = McqFragment().apply {
            arguments = Bundle(2).apply {
                putParcelable(KEY_QUESTION_BO,questionBO)
                putInt(KEY_POSITION, position)
                onOptionSelectedListener = listener
            }
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return LayoutInflater.from(container?.context).inflate(R.layout.mcq_fragment,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        indexOfQuestion = arguments?.getInt(KEY_POSITION,-1)
        questionBO = arguments?.getParcelable<QuestionBO>(KEY_QUESTION_BO)
        questionTv.text = questionBO?.Question
        option1.text = questionBO?.Option1
        option2.text = questionBO?.Option2
        option3.text = questionBO?.Option3
        option4.text = questionBO?.Option4

        option1.setOnClickListener(this)
        option2.setOnClickListener(this)
        option3.setOnClickListener(this)
        option4.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v?.id){
            R.id.option1 -> {
                SingletonModel.updateSelectedAnswer(indexOfQuestion!!,1);
                option1.background = resources.getDrawable(R.drawable.round_corner_view_filled,null)
                option2.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option3.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option4.background = resources.getDrawable(R.drawable.round_corner_view,null)
            }
            R.id.option2 -> {
                SingletonModel.updateSelectedAnswer(indexOfQuestion!!,2);
                option1.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option2.background = resources.getDrawable(R.drawable.round_corner_view_filled,null)
                option3.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option4.background = resources.getDrawable(R.drawable.round_corner_view,null)
            }
            R.id.option3 -> {
                SingletonModel.updateSelectedAnswer(indexOfQuestion!!,3);
                option1.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option2.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option3.background = resources.getDrawable(R.drawable.round_corner_view_filled,null)
                option4.background = resources.getDrawable(R.drawable.round_corner_view,null)
            }
            R.id.option4 -> {
                SingletonModel.updateSelectedAnswer(indexOfQuestion!!,4);
                option1.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option2.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option3.background = resources.getDrawable(R.drawable.round_corner_view,null)
                option4.background = resources.getDrawable(R.drawable.round_corner_view_filled,null)
            }
        }
    }
}

McqOldActivity fetch questions from firebase server.. McqPagerAdapter pass each question to McqFragment and when user select an option I update that answer using SingletonModel

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

If you want a thread safe singleton in Kotlin, you should re-write your singleton like so:

object Singleton {
    private val questions = mutableListOf<QuestionBO>()
    fun getQuestions() = questions.toList()

    private val results = mutableListOf<ResultBO>()
    fun getResults() = results.toList()

    fun updateQuestion(questionsFromServer:MutableList<QuestionBO>) {
        questions.apply {
            this.clear()
            this.addAll(questionsFromServer)
        }

        results.apply {
            this.clear()
            this.addAll(questions.map { question ->
                add(ResultBO(question, false, null))
            })
        }
    }

    fun updateSelectedAnswer(indexOfQuestion:Int,userSelectedIndex:Int){
        results.get(indexOfQuestion).isCorrectAnswer = questions.get(indexOfQuestion).CorrectIndex == userSelectedIndex
        results.get(indexOfQuestion).userSelectedOptionIndex = userSelectedIndex
    }

    fun clearAll(){
        results.clear()
        questions.clear()
    }
}

Note, in addition to using object as a top level declaration (which gives you thread safe singleton for free), I have also changed the code in the Singleton to fix some causes of bugs.

You had defined both questions and results as public mutable properties of the Singleton. This is bad because it means you can re-write the values... instead, make them private immutable properties of the Singleton, and expose methods for getting them, which call toList on the mutable collections to make sure an immutable copy is returned from the get functions.

Next up, in updateQuestion I have removed the re-assignment of questions and instead just cleared them and added the new ones from the server.

Instead of using a forEach over questions when re-populating the results collection, I have instead opted for map, which iterates the source collection, applying a mapping function to create a new value putting that new value into a new collection that is returned from map.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.