I'm making a screen similar to the image.
The data set in advance is taken from the Room DB and the data is set for each tab.
Each tab is a fragment and displays the data in a RecyclerView.
Each tab contains different data, so i set Tab to LiveData in ViewModel and observe it.
Therefore, whenever tabs change, the goal is to get the data for each tab from the database and set it in the RecyclerView.
However, even if I import the data, it is not set in RecyclerView.
I think the data comes in well even when I debug it.
This is not an adapter issue.
What am I missing?
WorkoutList
#Entity
data class WorkoutList(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val chest: List<String>,
val back: List<String>,
val leg: List<String>,
val shoulder: List<String>,
val biceps: List<String>,
val triceps: List<String>,
val abs: List<String>
)
ViewModel
class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
private var _part :MutableLiveData<BodyPart> = MutableLiveData()
private var result : List<String> = listOf()
private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
private val workoutListRepo = WorkoutListRepository(workoutDao)
val part = _part
fun setList(part : BodyPart) : List<String> {
_part.value = part
viewModelScope.launch(Dispatchers.IO){
result = workoutListRepo.getWorkoutList(part)
}
return result
}
}
Repository
class WorkoutListRepository(private val workoutListDao: WorkoutListDao) {
suspend fun getWorkoutList(part: BodyPart) : List<String> {
val partList = workoutListDao.getWorkoutList()
return when(part) {
is BodyPart.Chest -> partList.chest
is BodyPart.Back -> partList.back
is BodyPart.Leg -> partList.leg
is BodyPart.Shoulder -> partList.shoulder
is BodyPart.Biceps -> partList.biceps
is BodyPart.Triceps -> partList.triceps
is BodyPart.Abs -> partList.abs
}
}
}
Fragment
class WorkoutListTabPageFragment : Fragment() {
private var _binding : FragmentWorkoutListTabPageBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: WorkoutListAdapter
private lateinit var part: BodyPart
private val viewModel: WorkoutListViewModel by viewModels()
companion object {
#JvmStatic
fun newInstance(part: BodyPart) =
WorkoutListTabPageFragment().apply {
arguments = Bundle().apply {
putParcelable("part", part)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { bundle ->
part = bundle.getParcelable("part") ?: throw NullPointerException("No BodyPart Object")
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWorkoutListTabPageBinding.inflate(inflater, container, false)
binding.apply {
adapter = WorkoutListAdapter()
rv.adapter = adapter
}
val result = viewModel.setList(part)
// Set data whenever tab changes
viewModel.part.observe(viewLifecycleOwner) { _ ->
// val result = viewModel.setList(part)
adapter.addItems(result)
}
return binding.root
}
} viewModel.part.observe(viewLifecycleOwner) { _ ->
adapter.addItems(result)
}
return binding.root
}
}
The problem you are seeing is that in setList you start an asynchronous coroutine on the IO thread to get the list, but then you don't actually wait for that coroutine to run but just return the empty list immediately.
One way to fix that would be to observe a LiveData object containing the list, instead of observing the part. Then, when the asynchronous task is complete
you can post the retrieved data to that LiveData. That would look like this in the view model
class WorkoutListViewModel(application: Application) : AndroidViewModel(application) {
private val _list = MutableLiveData<List<String>>()
val list: LiveData<List<String>>
get() = _list
// "part" does not need to be a member of the view model
// based on the code you shared, but if you wanted it
// to be you could do it like this, then
// call "viewModel.part = part" in "onCreateView". It does not need
// to be LiveData if it's only ever set from the Fragment directly.
//var part: BodyPart = BodyPart.Chest
// calling getList STARTS the async process, but the function
// does not return anything
fun getList(part: BodyPart) {
viewModelScope.launch(Dispatchers.IO){
val result = workoutListRepo.getWorkoutList(part)
_list.postValue(result)
}
}
}
Then in the fragment onCreateView you observe the list, and when the values change you add them to the adapter. If the values may change several times you may need to clear the adapter before adding the items inside the observer.
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
// Set data whenever new data is posted
viewModel.list.observe(viewLifecycleOwner) { result ->
adapter.addItems(result)
}
// Start the async process of retrieving the list, when retrieved
// it will be posted to the live data and trigger the observer
viewModel.getList(part)
return binding.root
}
Note: The documentation currently recommends only inflating views in onCreateView and doing all other setup and initialization in onViewCreated - I kept it how you had it in your question for consistency.
Related
Using Kotlin, Retrofit and Coroutines, I have defined an interface to get data from a remote server and most importantly pass the id of a selected RecyclerView item back to the server.
interface CourseService {
#GET("/mobile/feed/course_data.php")
suspend fun getCourseData(#Query("pathName") pathName: String): Response<List<Course>>
}
Here, i get the id of the selected item from a RecyclerView from my MainFragment and store it in "selectedItem" variable.
override fun onPathItemClick(path: Path) {
viewModel.selectedItem.value = path
selectedItem= viewModel.selectedItem.value!!.path_id
navController.navigate(R.id.action_mainFragment_to_courseFragment)
}
I pass the value of selected item to the getCourseData() function
class CourseRepository(val app: Application) {
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
But i get no results and it seems as though the value passed to getCourseData() function is null, but when checking the log is does have a value.
so if i give it a predefined value anywhere in my code like below, everything works completely fine
selectedItem= "MOB001"
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
However, i cannot give it a fixed value prior to runtime because the value is retrieved when the user selects an item from a RecyclerView.
These are my multiple logs:
2020-05-01 13:56:30.431 23843-23843/ I/mylog: Main Fragment before item click: selectedItem =
2020-05-01 13:56:37.757 23843-23843/ I/mylog: Main Fragment after item click: selectedItem = WEB001
2020-05-01 13:56:37.763 23843-23843/ I/mylog: Course Fragment onCreateView(): selectedItem = WEB001
2020-05-01 13:56:37.772 23843-23901/ I/mylog: Course Fragment CourseRepository: selectedItem = WEB001
How can i overcome this issue?
You should call your CourseRepository's suspend function callWebService inside your ViewModel. Here is your repository:
class CourseRepository(val app: Application) {
suspend fun callWebService(path: Path): List<Course> {
return withContext(Dispatchers.IO) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
service.getCourseData(path.path_id).body() ?: emptyList()
}
}
}
Then you should call your repository function in your ViewModel as follows:
fun getCourseData(path: Path): LiveData<List<Course>> {
val response = MutableLiveData<List<Course>>()
viewModelScope.launch {
response.postValue(repository.callWebService(path))
}
return response
}
Then call viewModel. getCourseData(path) from your Activity or Fragment or anywhere when you get valid Path value.
Don't forget to include implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file.
Your code seems to be correct, however, it is highly possible that your RecyclerView is being populated the first time and and evertime you go back and choose another path it is being populated with the same data and view.
Therefore, your attentions should be focused on why the data is not being fetched again, which is the cause of the RecyclerView and Fragment holding on to the same first view.
After days of thinking my code was wrong, it turned out that my RecyclerView adapter was loading the same view everytime i wen back to select a different path becuase my RecyclerView was being inflated in the onCreateView() function which is only called once only, when a fragment is inflated the first time.
class CourseFragment : Fragment(),
CourseRecyclerAdapter.CourseItemListener {
private lateinit var viewModel: CourseViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var navController: NavController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_course, container, false)
recyclerView = view.findViewById(R.id.courseRecyclerView)
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(CourseViewModel::class.java)
viewModel.courseData.observe(viewLifecycleOwner, Observer {
val adapter =
CourseRecyclerAdapter(
requireContext(),
it,
this
)
recyclerView.adapter = adapter
} )
return view
}
override fun onCourseItemClick(course: Course) {
viewModel.selectedCourse.value = course
navController.navigate(R.id.action_courseFragment_to_detailFragment)
}
}
I'm having trouble with deleting data from my Firestore collection when the user clicks a delete button in a recyclerview. I can delete it from the recyclerview without any problems, but I'm having trouble to make the connection between the adapter, the viewmodel and the repository that handles Firestore operations.
In my adapter, I remove the item the user clicked on from the recyclerview:
class ArticleAdapter : RecyclerView.Adapter<ArticleAdapter.ViewHolder>() {
var data = mutableListOf<Product>()
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
holder.deleteButton.setOnClickListener {
data.removeAt(position)
notifyDataSetChanged()
}
} ...
The recyclerview is populated after a query to the Firestore collection in my viewmodel:
class ArticleViewModel(private val repository: ProductRepository) : ViewModel() {
var savedProducts: MutableLiveData<MutableList<Product>> = MutableLiveData<MutableList<Product>>()
init {
savedProducts = getProducts()
}
fun getProducts(): MutableLiveData<MutableList<Product>> {
repository.getProducts().addSnapshotListener(EventListener<QuerySnapshot> { value, e ->
if (e != null) {
savedProducts.value = null
return#EventListener
}
val savedProductsList: MutableList<Product> = mutableListOf()
for (doc in value!!) {
val item = doc.toObject(Product::class.java)
item.id = doc.id
savedProductsList.add(item)
}
savedProductsList.sortBy { i -> i.productName }
savedProducts.value = savedProductsList
})
return savedProducts
} }
In my Fragment, I'm then observing any changes that might happen to savedProducts:
class ArticleOverviewFragment : Fragment(), KodeinAware {
override val kodein: Kodein by kodein()
private val factory: ArticleViewModelFactory by instance()
private lateinit var viewModel: ArticleViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentArticleOverviewBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_article_overview, container, false)
viewModel = ViewModelProviders.of(this, factory).get(ArticleViewModel::class.java)
binding.viewModel = viewModel
val adapter = ArticleAdapter()
binding.recyclerViewGoods.adapter = adapter
viewModel.savedProducts.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
}
})
...
} }
Is there a way that I can observe/save the ID of the deleted item in my adapter and "transfer" that ID from the adapter to the UI where I call a function declared in the viewmodel whenever that field holding the ID is populated? Or should I directly access the viewmodel from the adapter? Somehow, that feels kinda wrong...
Declare one local variable
var removedPosition : Int ? = null
then update this variable into onClick event of deleteButton
holder.deleteButton.setOnClickListener {
data.removeAt(position)
removedPosition = position
notifyDataSetChanged()
}
Please make one method in Adapter (ArticleAdapter)
fun getRemoveItemPosition() : Int {
var position = removedPosition
return position;
}
which return the position of removed Item and call that method in UI(ArticleOverviewFragment) where you will require to get position of removed item from recyclerview
var removedItemPosition = adapter.getRemoveItemPosition()
Now you will get value of remove item Position using variable called removedItemPosition
So You can get Position of removed Item in UI where you can call a function declared in the viewmodel (ArticleViewModel) to delete particular item in firestore collection.
I am trying out flows and trying to see how they can be converted to mvvm with android view models. Here is what I tried first to test it out :
class HomeViewModel : ViewModel() {
private lateinit var glucoseFlow: LiveData<Int>
var _glucoseFlow = MutableLiveData<Int>()
fun getGlucoseFlow() {
glucoseFlow = flowOf(1,2).asLiveData()
_glucoseFlow.value = glucoseFlow.value
}
}
class HomeFragment : Fragment() {
private lateinit var viewModel: HomeViewModel
override fun onCreateView (
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.home_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)
viewModel._glucoseFlow.observe(this, Observer {
handleUpdate(it)
})
viewModel.getGlucoseFlow()
}
private fun handleUpdate(reading : Int) {
glucose_reading.text = reading.toString()
}
}
I get a null for the reading number however any ideas ?
This happens because you are trying to assign glucoseFlow.value to _glucoseFlow.value directly, I guess you should use a MediatorLiveData<Int>, however this is not my final suggestion.
You can solve it if you collect flow items and then assign them to your private variable.
// For private variables, prefer use underscore prefix, as well MutableLiveData for assignable values.
private val _glucoseFlow = MutableLiveData<Int>()
// For public variables, prefer use LiveData just to read values.
val glucoseFlow: LiveData<Int> get() = _glucoseFlow
fun getGlucoseFlow() {
viewModelScope.launch {
flowOf(1, 2)
.collect {
_glucoseFlow.value = it
}
}
}
Having the before implementation over the HomeViewModel, start to observe your public glucoseFlow from HomeFragment and you will be able to receive non-null sequence values (1 and then 2).
If you are using databinding, do not forget specify the fragment view as the lifecycle owner of the binding so that the binding can observe LiveData updates.
class HomeFragment : Fragment() {
...
binding.lifecycleOwner = viewLifecycleOwner
}
I am going through Guide to app architecture and trying to implement MVVM and LiveData in one of my apps. I am using realm and I am using this to create a RealmLiveData as shown below
class RealmLiveData<T : RealmModel>(private val results: RealmResults<T>) : MutableLiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results -> value = results }
override fun onActive() {
results.addChangeListener(listener)
}
override fun onInactive() {
results.removeChangeListener(listener)
}
}
This how I am updating the list to recyclerview
var mList:ArrayList<Notes> = ArrayList()
lateinit var historyViewModel: HistoryViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_history, container, false)
mRCview = view.findViewById(R.id.list)
historyViewModel = ViewModelProviders.of(activity!!).get(HistoryViewModel::class.java)
// this is how I observe
historyViewModel.getList().observe(this, Observer{
(mRCview.adapter as MyHistoryRecyclerViewAdapter).setData(it)
})
with(mRCview) {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(mContext)
mList = ArrayList()
adapter = MyHistoryRecyclerViewAdapter(
mContext as OnListFragmentInteractionListener
)
}
return view
}
This is how I get the data in my repository class
class HistoryRepository {
fun getHistory(): RealmLiveData<Notes> {
val realmInstance = Realm.getDefaultInstance()
val realmResults = realmInstance
.where(Notes::class.java)
.findAll()
.sort("lastUpdatedTimeStamp", Sort.DESCENDING)
return realmResults.asLiveData()
}
fun <T:RealmModel> RealmResults<T>.asLiveData() = RealmLiveData(this)
}
EDIT
Here is the ViewModel
class HistoryViewModel: ViewModel() {
val repository = HistoryRepository()
fun getList(): RealmLiveData<Notes> {
return repository.getHistory()
}
}
The issue is that the observer is not getting triggered for the first time. If I update the realmresult, the live data update gets invoked and updates my list. Please let me know how I can fix the issue.
We need to notify the Observer of the existing data. When the first Observer registers to historyViewModel.getList() you are registering the realm callback. At this point we need to trigger a change just to notify this Observer of the existing data.
Something like
class RealmLiveData<T : RealmModel>(private val results: RealmResults<T>) : MutableLiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results -> value = results }
override fun onActive() {
results.addChangeListener(listener)
listener.onChange(results) // notify the added Observer of the existing data.
}
override fun onInactive() {
results.removeChangeListener(listener)
}
}
I'm new to Room and Kotlin, and I'm trying to build a simple Room's database and present it in a RecyclerView that sits in a ViewPager.
The Activity contains a ViewPager object, that ViewPager contains a Fragment, and that Fragment contains a RecyclerView:
Activity --> ViewPager --> Fragment --> RecyclerView
The problems I have is that I get null when I'm trying to receive the database (after an insertion).
code:
#Entity(tableName = "Guests_table")
data class Guest(#NonNull #ColumnInfo (name = "Name") var name: String,
#ColumnInfo (name = "Phone number") var phoneNumber: String,
#ColumnInfo (name = "Coming") var coming: Boolean = false,
#ColumnInfo (name = "Participants") var participants: Int)
{
#PrimaryKey (autoGenerate = true)
var id: Long = 0
init
{
phoneNumber = PhoneNumberUtils.formatNumber(phoneNumber, Locale.getDefault().country)
}
}
dao:
#Dao
interface GuestsDAO
{
#Insert
fun insert(guest: Guest)
#Delete
fun delete(guest: Guest)
#Query ("DELETE FROM Guests_table")
fun deleteAll()
#Query ("SELECT * FROM Guests_table ORDER BY name ASC")
fun getAllGuests(): List<Guest>
}
database:
#Database(entities = [Guest::class], version = 1)
abstract class InviterRoomDatabase: RoomDatabase()
{
abstract fun guestsDao(): GuestsDAO
companion object
{
private var INSTANCE: InviterRoomDatabase ?= null
fun getDatabase(context: Context): InviterRoomDatabase?
{
if (INSTANCE == null)
{
synchronized(InviterRoomDatabase::class)
{
INSTANCE = Room.databaseBuilder(context.applicationContext,InviterRoomDatabase::class.java,"Guests.db").build()
}
}
return INSTANCE
}
fun destroyInstance()
{
INSTANCE = null
}
}
}
activity:
class EventActivity : AppCompatActivity()
{
private lateinit var viewPager: ViewPager
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_event)
viewPager = findViewById(R.id.guests_view_pager)
val pagerAdapter = EventPagerAdapter(supportFragmentManager)
viewPager.adapter = pagerAdapter
}
}
adapter:
class EventPagerAdapter(fragmentManager: FragmentManager): FragmentPagerAdapter(fragmentManager)
{
override fun getCount(): Int
{
return 1
}
override fun getItem(position: Int): Fragment
{
return GuestListFragment.newInstance()
}
}
fragment:
class GuestListFragment : Fragment()
{
private var guestsDataBase: InviterRoomDatabase? = null
private lateinit var dbWorkerThread: DBWorkerThread
companion object
{
fun newInstance(): GuestListFragment
{
val fragment = GuestListFragment()
return fragment
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
dbWorkerThread = DBWorkerThread("workerThread")
dbWorkerThread.start()
guestsDataBase = InviterRoomDatabase.getDatabase(this.context!!)
insertGuestToDB(Guest("Joe","052352332",false,0))
var rootView = inflater.inflate(R.layout.fragment_guest_list, container, false)
var rv: RecyclerView = rootView.findViewById(R.id.guests_list_recycler_view)
rv.layoutManager = LinearLayoutManager(context)
var d = getAllDataFromDB()
rv.adapter = GuestsRecycleViewAdapter.newInstance(d)
return rootView
}
private fun insertGuestToDB(guest: Guest)
{
val task = Runnable {guestsDataBase?.guestsDao()?.insert(guest)}
dbWorkerThread.postTask(task)
}
private fun getAllDataFromDB(): List<Guest>
{
var data: List<Guest> = emptyList()
val task = Runnable { data = guestsDataBase?.guestsDao()?.getAllGuests()!!}
dbWorkerThread.postTask(task)
return data
}
}
You need to understand the concept of multi-threading to understand why your code does not work. In brief, you are getting an empty list because you are trying to get the list in main thread while your insertion and query running on the worker thread have not completed yet.
I put the comment in the code to explain the order of execution. The number in the comment is the order of completion:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
// [1] onCreateView starts on main thread.
dbWorkerThread = DBWorkerThread("workerThread")
dbWorkerThread.start()
guestsDataBase = InviterRoomDatabase.getDatabase(this.context!!)
// [2] calls insertGuestToDB on main thread.
insertGuestToDB(Guest("Joe","052352332",false,0))
var rootView = inflater.inflate(R.layout.fragment_guest_list, container, false)
var rv: RecyclerView = rootView.findViewById(R.id.guests_list_recycler_view)
rv.layoutManager = LinearLayoutManager(context)
// [4] calls getAllDataFromDB on main thread.
var d = getAllDataFromDB()
// [6] get "d" on main thread. "d" is an empty list.
rv.adapter = GuestsRecycleViewAdapter.newInstance(d)
return rootView
}
private fun insertGuestToDB(guest: Guest)
{
// [2] insertGuestToDB executed on main thread.
val task = Runnable {
guestsDataBase?.guestsDao()?.insert(guest)
// [7] insertion finished on worker thread.
}
dbWorkerThread.postTask(task)
// [3] insertGuestToDB finishes on main thread.
}
private fun getAllDataFromDB(): List<Guest>
{
// [4] getAllDataFromDB on main thread. "data" points to an empty list
var data: List<Guest> = emptyList()
val task = Runnable {
data = guestsDataBase?.guestsDao()?.getAllGuests()!!
// [8] query finishes on worker thread, but "data" is now lost.
}
dbWorkerThread.postTask(task)
// [5] returns on main thread. "data" still points to an empty list
return data
}
What you need to do is to wait for dbWorkerThread to complete its job before assigning data list to the adapter. Take a look at one of the simplest solution:
var rv: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
dbWorkerThread = DBWorkerThread("workerThread")
dbWorkerThread.start()
var rootView = inflater.inflate(R.layout.fragment_guest_list, container, false)
rv= rootView.findViewById(R.id.guests_list_recycler_view)
rv.layoutManager = LinearLayoutManager(context)
guestsDataBase = InviterRoomDatabase.getDatabase(this.context!!)
insertGuestToDbAndUpdateRv(Guest("Joe","052352332",false,0))
return rootView
}
private fun insertGuestToDbAndUpdateRv(guest: Guest)
{
val task = Runnable {
// 1. Insert on a worker thread.
guestsDataBase?.guestsDao()?.insert(guest)
// 2. Query on a worker thread.
var data = guestsDataBase?.guestsDao()?.getAllGuests()!!
this#GuestListFragment.getActivity().runOnUiThread {
// 3. Once they are done, update ui on main thread.
rv.adapter = GuestsRecycleViewAdapter.newInstance(data)
}
}
dbWorkerThread.postTask(task)
}
Be aware that this is definitely not the best way to tackle this problem. This is a very common problem and there are tons of different solutions that are much more readable, flexible and maintainable. It is just that other solutions require deeper understanding and multi-threading is such a broad topic that I cannot explain all of them in a single SO answer. I hope you get the general idea though and encourage you to explore other solutions to this.