Search code examples
androidandroid-roomandroid-viewmodelkotlin-coroutines

Android Coroutines block UI click listener


I'm using MVVM as architecture, also the repository pattern. I have a Web service, a room database also. Using coroutines block any button I click.

There's a list/detail implemented with a fragment and an activity respectively.

I can figure out what's wrong in the way I implemented the coroutines and Viewmodel.

class BuySharedViewModel(application: Application) : AndroidViewModel(application) {
private val repository: BuyRepository
var allBuys: LiveData<List<Buy>>


init {
    val buyDao = KunukRoomDatabase.getDatabase(application, viewModelScope).buyDao()
    val buyRemote = BuyRemote()
    repository = BuyRepository.getInstance(buyDao , buyRemote)
    //Use async because it return a result
    viewModelScope.launch { getAllBuys() }
    allBuys = buyDao.loadAllBuys()
}

private suspend fun getAllBuys() {
    repository.getBuys()

}
}

Here's is the Repository, it take data from web service and add it to the room database, while ViewModel get's data from room database.

class BuyRepository (private val buyDao: BuyDao, private val buyRemote: BuyRemote) {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)

companion object {
    //For singleton instantiation
    @Volatile private var instance: BuyRepository? = null

    fun getInstance(buyDao: BuyDao, buyRemote: BuyRemote) =
        instance ?: synchronized(this) {
            instance ?: BuyRepository(buyDao, buyRemote)
                .also { instance = it}

        }
}


suspend fun getBuys(){
    refresh()
}

private suspend fun refresh(){

    try {
        val list = scope.async { buyRemote.loadBuys() }
        list.await().forEach { buy -> insert(buy) }
    } catch (e: Throwable) {}
}


@WorkerThread
private fun insert(buy: Buy) {
    buyDao.insertBuy(buy)
}


}

The fragment work, data are displayed, when i click on an item from that fragment(recyclerView) it work, the activity display details data. But none of the click on that activity works, like it doesn't detect the clicks. I guess it got something to do with the coroutines because when I comment out the code viewmodelScope.launch { getAllBuys()} from the BuySharedViewModel it works, because it load data from the previous call from room database, and the clicks works.

Here's the code in the detail view:

class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lateinit var buy: Buy

    sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)

    val position = intent.getIntExtra("position", 0)
    sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
        buy = buys[position]
        val binding: com.example.drake.kunuk.databinding.ActivityBuyDetailBinding =
            DataBindingUtil.setContentView(this, com.example.drake.kunuk.R.layout.activity_buy_detail)
        binding.buy = buy


        val agentNumber = buy.agentNumber?:"+50937438713"
        bnvContactAgent.setOnNavigationItemSelectedListener { item ->

            when (item.itemId) {
                com.example.drake.kunuk.R.id.action_call -> {
                    val callNumberUri = Uri.parse("tel:$agentNumber")
                    val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
                    startActivity(callIntent)
                }
                com.example.drake.kunuk.R.id.action_sms -> {
                    val smsNumberUri = Uri.parse("sms:$agentNumber")
                    val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
                    startActivity(smsIntent)
                }
                com.example.drake.kunuk.R.id.action_email -> {
                    val uriText = "mailto:[email protected]" +
                            "?subject=" + Uri.encode("I'm interested in $agentNumber") +
                            "&body=" + Uri.encode("Hello, ")

                    val uri = Uri.parse(uriText)

                    val sendIntent = Intent(Intent.ACTION_SENDTO)
                    sendIntent.data = uri
                    startActivity(Intent.createChooser(sendIntent, "Send email"))
                }
            }
            false
        }

This is the code of my fragment:

class BuyFragment : Fragment() {
companion object {
    fun newInstance() = BuyFragment()
}

private lateinit var viewModel: BuySharedViewModel
private val buyList = ArrayList<Buy>()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Get a new or existing ViewModel from the ViewModelProvider.
    viewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)

    // Add an observer on the LiveData returned by loadAllBuys.
    // The onChanged() method fires when the observed data changes and the activity is
    // in the foreground.
    viewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
        // Update the cached copy of the words in the adapter.
        buys?.let { (rvBuy.adapter as BuyAdapter).setBuys(it) }
        progressBar.visibility = View.GONE
    })
}

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

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    rvBuy.layoutManager = LinearLayoutManager(context)
    rvBuy.adapter = BuyAdapter(activity!!.applicationContext,
        R.layout.buy_card, buyList)
    progressBar.visibility = View.VISIBLE


}
}

This is the code for the BuyDao:

@Dao

interface BuyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertBuy(vararg buys: Buy)

@Update
fun updateBuy(vararg buys: Buy)

@Delete
 fun deleteBuys(vararg buys: Buy)

@Query("SELECT * FROM buys")
fun loadAllBuys(): LiveData<List<Buy>>

@Query("DELETE FROM buys")
suspend fun deleteAll()

}

Solution

  • viewModelScope by default uses Dispatchers.Main and it is blocking your UI.

    Try this:

    viewmodelScope.launch(Dispatchers.IO) { getAllBuys()}
    

    Edit:

    The problem is your setting listner on BottomNavigation when your livedata is updated which is causing this weird issue.

    Replace your BuyDetailActivity code with this:

    class BuyDetailActivity : AppCompatActivity() {
        private lateinit var sharedViewModel: BuySharedViewModel
        private var agentNumber = ""
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val binding: ActivityBuyDetailBinding =
                DataBindingUtil.setContentView(this, R.layout.activity_buy_detail)
            binding.buy = Buy()
            lateinit var buy: Buy
    
            sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
    
            val position = intent.getIntExtra("position", 0)
            sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
                buy = buys[position]
                binding.buy = buy
                binding.executePendingBindings()
    
                agentNumber = buy.agentNumber
    
                // set animation duration via code, but preferable in your layout files by using the animation_duration attribute
                expandableTextView.setAnimationDuration(750L)
    
                // set interpolators for both expanding and collapsing animations
                expandableTextView.setInterpolator(OvershootInterpolator())
    
                // or set them separately.
                expandableTextView.expandInterpolator = OvershootInterpolator()
                expandableTextView.collapseInterpolator = OvershootInterpolator()
    
                // toggle the ExpandableTextView
                buttonToggle.setOnClickListener {
                    buttonToggle.setText(if (expandableTextView.isExpanded) com.example.drake.kunuk.R.string.more else com.example.drake.kunuk.R.string.less)
                    expandableTextView.toggle()
                }
    
                // but, you can also do the checks yourself
                buttonToggle.setOnClickListener {
                    if (expandableTextView.isExpanded) {
                        expandableTextView.collapse()
                        buttonToggle.setText(com.example.drake.kunuk.R.string.more)
                    } else {
                        expandableTextView.expand()
                        buttonToggle.setText(com.example.drake.kunuk.R.string.less)
                    }
                }
    
                //Open photoView activity when clicked
                ivHouseDetail.setOnClickListener {
                    applicationContext
                        .startActivity(
                            Intent(
                                applicationContext,
                                ViewPagerActivity::class.java
                            )
                                .putExtra("imageList", buy.propertyImage)
                                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        )
                }
            })
    
            findViewById<BottomNavigationView>(R.id.bnvContactAgent)?.setOnNavigationItemSelectedListener { item ->
    
                when (item.itemId) {
                    R.id.action_call -> {
                        Log.e("BIRJU", "Action call")
                        val callNumberUri = Uri.parse("tel:$agentNumber")
                        val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
                        startActivity(callIntent)
                    }
                    R.id.action_sms -> {
                        Log.e("BIRJU", "Action SMS")
                        val smsNumberUri = Uri.parse("sms:$agentNumber")
                        val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
                        startActivity(smsIntent)
                    }
                    R.id.action_email -> {
                        Log.e("BIRJU", "Action Email")
                        val uriText = "mailto:[email protected]" +
                                "?subject=" + Uri.encode("I'm interested in $agentNumber") +
                                "&body=" + Uri.encode("Hello, ")
                        val uri = Uri.parse(uriText)
                        val sendIntent = Intent(Intent.ACTION_SENDTO)
                        sendIntent.data = uri
                        startActivity(Intent.createChooser(sendIntent, "Send email"))
                    }
                }
                false
            }
        }
    }