Skip to content

Commit c75aa41

Browse files
fix(dashboard): Resolve ANR using Flow-based listeners
Refactored `DashboardActivity` to address a critical ANR risk caused by synchronous Realm queries on the main thread. - Replaced the manual `RealmChangeListener` implementation with a modern, asynchronous approach using Kotlin Coroutines and Flow. - Added the `io.realm:realm-android-kotlin-extensions` dependency to enable Flow support for the legacy Realm SDK. - Used `asFlow()` to convert Realm results to a Flow, `flowOn(Dispatchers.IO)` to move database work to a background thread, and `debounce()` to throttle notifications. - The `observeQuery` function is now managed by `lifecycleScope`, which automatically handles cancellation and prevents memory leaks.
1 parent d83a673 commit c75aa41

File tree

4 files changed

+144
-201
lines changed

4 files changed

+144
-201
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ dependencies {
242242
implementation "io.noties.markwon:html:$markwon_version"
243243
implementation "io.noties.markwon:ext-tables:$markwon_version"
244244
implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.2.10"))
245+
implementation "io.realm:realm-android-kotlin-extensions:10.19.0"
245246

246247
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
247248
}

app/src/main/java/org/ole/planet/myplanet/ui/dashboard/BaseDashboardFragment.kt

Lines changed: 115 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.ole.planet.myplanet.ui.dashboard
33
import android.app.DatePickerDialog
44
import android.content.Intent
55
import android.graphics.Typeface
6-
import android.os.Bundle
76
import android.text.TextUtils
87
import android.view.LayoutInflater
98
import android.view.View
@@ -45,13 +44,11 @@ import org.ole.planet.myplanet.model.RealmTeamTask
4544
import org.ole.planet.myplanet.model.RealmUserModel
4645
import org.ole.planet.myplanet.service.TransactionSyncManager
4746
import org.ole.planet.myplanet.service.UserProfileDbHandler.Companion.KEY_LOGIN
48-
import org.ole.planet.myplanet.ui.courses.TakeCourseFragment
4947
import org.ole.planet.myplanet.ui.exam.UserInformationFragment
5048
import org.ole.planet.myplanet.ui.myhealth.UserListArrayAdapter
5149
import org.ole.planet.myplanet.ui.team.TeamDetailFragment
5250
import org.ole.planet.myplanet.ui.userprofile.BecomeMemberActivity
5351
import org.ole.planet.myplanet.ui.userprofile.UserProfileFragment
54-
import org.ole.planet.myplanet.ui.library.ResourceDetailFragment
5552
import org.ole.planet.myplanet.utilities.Constants
5653
import org.ole.planet.myplanet.utilities.DialogUtils
5754
import org.ole.planet.myplanet.utilities.DownloadUtils
@@ -60,24 +57,6 @@ import org.ole.planet.myplanet.utilities.Utilities
6057

6158
open class BaseDashboardFragment : BaseDashboardFragmentPlugin(), NotificationCallback,
6259
SyncListener {
63-
private data class DashboardData(
64-
val myLibrary: List<MyLibraryData>,
65-
val myCourses: List<CourseData>,
66-
val myTeams: List<TeamData>,
67-
val myLife: List<MyLifeData>
68-
)
69-
70-
private data class MyLibraryData(val _id: String, val title: String)
71-
private data class CourseData(val id: String, val title: String)
72-
73-
private data class TeamData(
74-
val id: String,
75-
val name: String,
76-
val teamType: String,
77-
val showChatNotification: Boolean,
78-
val showTaskNotification: Boolean
79-
)
80-
8160
private var fullName: String? = null
8261
private var params = LinearLayout.LayoutParams(250, 100)
8362
private var di: DialogUtils.CustomProgressDialog? = null
@@ -160,16 +139,21 @@ open class BaseDashboardFragment : BaseDashboardFragmentPlugin(), NotificationCa
160139
}
161140
}
162141

163-
private fun myLibraryDiv(myLibrary: List<MyLibraryData>, view: View) {
164-
val flexboxLayout = view.findViewById<FlexboxLayout>(R.id.flexboxLayout)
165-
flexboxLayout.removeAllViews()
166-
flexboxLayout.flexDirection = FlexDirection.ROW
167-
if (myLibrary.isEmpty()) {
142+
private suspend fun myLibraryDiv(view: View) {
143+
val dbMylibrary = withContext(Dispatchers.IO) {
144+
Realm.getDefaultInstance().use { realm ->
145+
val results = RealmMyLibrary.getMyLibraryByUserId(realm, settings)
146+
realm.copyFromRealm(results)
147+
}
148+
}
149+
150+
view.findViewById<FlexboxLayout>(R.id.flexboxLayout).flexDirection = FlexDirection.ROW
151+
if (dbMylibrary.isEmpty()) {
168152
view.findViewById<TextView>(R.id.count_library).visibility = View.GONE
169153
} else {
170-
view.findViewById<TextView>(R.id.count_library).text = getString(R.string.number_placeholder, myLibrary.size)
154+
view.findViewById<TextView>(R.id.count_library).text = getString(R.string.number_placeholder, dbMylibrary.size)
171155
}
172-
for ((itemCnt, item) in myLibrary.withIndex()) {
156+
for ((itemCnt, items) in dbMylibrary.withIndex()) {
173157
val itemLibraryHomeBinding = ItemLibraryHomeBinding.inflate(LayoutInflater.from(activity))
174158
val v = itemLibraryHomeBinding.root
175159
setTextColor(itemLibraryHomeBinding.title, itemCnt)
@@ -179,144 +163,129 @@ open class BaseDashboardFragment : BaseDashboardFragmentPlugin(), NotificationCa
179163
v.setBackgroundColor(color)
180164
}
181165

182-
itemLibraryHomeBinding.title.text = item.title
166+
itemLibraryHomeBinding.title.text = items.title
183167
itemLibraryHomeBinding.detail.setOnClickListener {
184168
if (homeItemClickListener != null) {
185-
val b = Bundle()
186-
b.putString("libraryId", item._id)
187-
val f = ResourceDetailFragment()
188-
f.arguments = b
189-
homeItemClickListener?.openCallFragment(f)
169+
homeItemClickListener?.openLibraryDetailFragment(items)
190170
}
191171
}
192172

193-
myLibraryItemClickAction(itemLibraryHomeBinding.title, item)
194-
flexboxLayout.addView(v, params)
173+
myLibraryItemClickAction(itemLibraryHomeBinding.title, items)
174+
view.findViewById<FlexboxLayout>(R.id.flexboxLayout).addView(v, params)
195175
}
196176
}
197177

198-
199-
private fun setUpCourses(courses: List<CourseData>, view: View) {
200-
val flexboxLayout: FlexboxLayout = view.findViewById(R.id.flexboxLayoutCourse)
201-
flexboxLayout.removeAllViews()
178+
private fun initializeFlexBoxView(v: View, id: Int, c: Class<out RealmObject>) {
179+
val flexboxLayout: FlexboxLayout = v.findViewById(id)
202180
flexboxLayout.flexDirection = FlexDirection.ROW
203-
setCountText(courses.size, RealmMyCourse::class.java, view)
204-
val myCoursesTextViewArray = arrayOfNulls<TextView>(courses.size)
205-
for ((itemCnt, course) in courses.withIndex()) {
206-
setTextViewProperties(myCoursesTextViewArray, itemCnt, course)
207-
myCoursesTextViewArray[itemCnt]?.let { setTextColor(it, itemCnt) }
208-
flexboxLayout.addView(myCoursesTextViewArray[itemCnt], params)
209-
}
181+
setUpMyList(c, flexboxLayout, v)
210182
}
211183

212-
private fun setTextViewProperties(
213-
textViewArray: Array<TextView?>,
214-
itemCnt: Int,
215-
course: CourseData
216-
) {
217-
textViewArray[itemCnt] = TextView(context).apply {
218-
setPadding(20, 10, 20, 10)
219-
textAlignment = View.TEXT_ALIGNMENT_CENTER
220-
gravity = Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL
221-
handleClick(course.id, course.title, TakeCourseFragment(), this)
184+
private fun setUpMyList(c: Class<out RealmObject>, flexboxLayout: FlexboxLayout, view: View) {
185+
val dbMycourses: List<RealmObject>
186+
val userId = settings?.getString("userId", "--")
187+
setUpMyLife(userId)
188+
dbMycourses = when (c) {
189+
RealmMyCourse::class.java -> {
190+
RealmMyCourse.getMyByUserId(mRealm, settings).filter { !it.courseTitle.isNullOrBlank() }
191+
}
192+
RealmMyTeam::class.java -> {
193+
val i = myTeamInit(flexboxLayout)
194+
setCountText(i, RealmMyTeam::class.java, view)
195+
return
196+
}
197+
RealmMyLife::class.java -> {
198+
myLifeListInit(flexboxLayout)
199+
return
200+
}
201+
else -> {
202+
userId?.let {
203+
mRealm.where(c).contains("userId", it, Case.INSENSITIVE).findAll()
204+
} ?: listOf()
205+
}
206+
}
207+
setCountText(dbMycourses.size, c, view)
208+
val myCoursesTextViewArray = arrayOfNulls<TextView>(dbMycourses.size)
209+
for ((itemCnt, items) in dbMycourses.withIndex()) {
210+
val course = items as RealmMyCourse
211+
setTextViewProperties(myCoursesTextViewArray, itemCnt, items)
212+
myCoursesTextViewArray[itemCnt]?.let { setTextColor(it, itemCnt) }
213+
flexboxLayout.addView(myCoursesTextViewArray[itemCnt], params)
222214
}
223215
}
224216

225-
private fun setUpTeams(teams: List<TeamData>, view: View) {
226-
val flexboxLayout: FlexboxLayout = view.findViewById(R.id.flexboxLayoutTeams)
227-
flexboxLayout.removeAllViews()
228-
flexboxLayout.flexDirection = FlexDirection.ROW
229-
setCountText(teams.size, RealmMyTeam::class.java, view)
230-
for ((count, team) in teams.withIndex()) {
217+
private fun myTeamInit(flexboxLayout: FlexboxLayout): Int {
218+
val dbMyTeam = RealmMyTeam.getMyTeamsByUserId(mRealm, settings)
219+
val userId = profileDbHandler.userModel?.id
220+
for ((count, ob) in dbMyTeam.withIndex()) {
231221
val v = LayoutInflater.from(activity).inflate(R.layout.item_home_my_team, flexboxLayout, false)
232222
val name = v.findViewById<TextView>(R.id.tv_name)
233223
setBackgroundColor(v, count)
234-
if (team.teamType == "sync") {
224+
if ((ob as RealmMyTeam).teamType == "sync") {
235225
name.setTypeface(null, Typeface.BOLD)
236226
}
237-
handleClick(team.id, team.name, TeamDetailFragment(), name)
238-
v.findViewById<ImageView>(R.id.img_chat).visibility = if (team.showChatNotification) View.VISIBLE else View.GONE
239-
v.findViewById<ImageView>(R.id.img_task).visibility = if (team.showTaskNotification) View.VISIBLE else View.GONE
240-
name.text = team.name
227+
handleClick(ob._id, ob.name, TeamDetailFragment(), name)
228+
showNotificationIcons(ob, v, userId)
229+
name.text = ob.name
241230
flexboxLayout.addView(v, params)
242231
}
232+
return dbMyTeam.size
243233
}
244234

245-
private fun setUpMyLife(myLifeList: List<MyLifeData>, view: View) {
246-
val flexboxLayout: FlexboxLayout = view.findViewById(R.id.flexboxLayoutMyLife)
247-
flexboxLayout.removeAllViews()
248-
flexboxLayout.flexDirection = FlexDirection.ROW
249-
for ((itemCnt, item) in myLifeList.withIndex()) {
250-
flexboxLayout.addView(getLayout(itemCnt, item), params)
235+
private fun showNotificationIcons(ob: RealmObject, v: View, userId: String?) {
236+
val current = Calendar.getInstance().timeInMillis
237+
val tomorrow = Calendar.getInstance()
238+
tomorrow.add(Calendar.DAY_OF_YEAR, 1)
239+
val imgTask = v.findViewById<ImageView>(R.id.img_task)
240+
val imgChat = v.findViewById<ImageView>(R.id.img_chat)
241+
val notification: RealmTeamNotification? = mRealm.where(RealmTeamNotification::class.java)
242+
.equalTo("parentId", (ob as RealmMyTeam)._id).equalTo("type", "chat").findFirst()
243+
val chatCount: Long = mRealm.where(RealmNews::class.java).equalTo("viewableBy", "teams")
244+
.equalTo("viewableId", ob._id).count()
245+
if (notification != null) {
246+
imgChat.visibility = if (notification.lastCount < chatCount) View.VISIBLE else View.GONE
251247
}
248+
val tasks = mRealm.where(RealmTeamTask::class.java).equalTo("assignee", userId)
249+
.between("deadline", current, tomorrow.timeInMillis).findAll()
250+
imgTask.visibility = if (tasks.isNotEmpty()) View.VISIBLE else View.GONE
252251
}
253252

253+
private fun myLifeListInit(flexboxLayout: FlexboxLayout) {
254+
val dbMylife: MutableList<RealmMyLife> = ArrayList()
255+
val rawMylife: List<RealmMyLife> = RealmMyLife.getMyLifeByUserId(mRealm, settings)
256+
for (item in rawMylife) if (item.isVisible) dbMylife.add(item)
257+
for ((itemCnt, items) in dbMylife.withIndex()) {
258+
flexboxLayout.addView(getLayout(itemCnt, items), params)
259+
}
260+
}
254261

255-
private suspend fun loadDashboardData(): DashboardData = withContext(Dispatchers.IO) {
256-
Realm.getDefaultInstance().use { realm ->
257-
val userId = settings?.getString("userId", "--")
258-
val user = profileDbHandler.userModel
259-
260-
val myLibrary = RealmMyLibrary.getMyLibraryByUserId(realm, settings)
261-
.map { MyLibraryData(it._id ?: "", it.title ?: "") }
262-
263-
val myCourses = RealmMyCourse.getMyByUserId(realm, settings)
264-
.filter { !it.courseTitle.isNullOrBlank() }
265-
.map { CourseData(it.courseId ?: "", it.courseTitle ?: "") }
266-
267-
val myTeams = RealmMyTeam.getMyTeamsByUserId(realm, settings)
268-
.map { team ->
269-
val notification = realm.where(RealmTeamNotification::class.java)
270-
.equalTo("parentId", team._id)
271-
.equalTo("type", "chat")
272-
.findFirst()
273-
val chatCount = realm.where(RealmNews::class.java)
274-
.equalTo("viewableBy", "teams")
275-
.equalTo("viewableId", team._id)
276-
.count()
277-
val showChatNotification = notification != null && notification.lastCount < chatCount
278-
279-
val current = Calendar.getInstance().timeInMillis
280-
val tomorrow = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 1) }
281-
val tasks = realm.where(RealmTeamTask::class.java)
282-
.equalTo("assignee", user?.id)
283-
.between("deadline", current, tomorrow.timeInMillis)
284-
.findAll()
285-
val showTaskNotification = tasks.isNotEmpty()
286-
287-
TeamData(
288-
id = team._id ?: "",
289-
name = team.name ?: "",
290-
teamType = team.teamType ?: "",
291-
showChatNotification = showChatNotification,
292-
showTaskNotification = showTaskNotification
293-
)
262+
private fun setUpMyLife(userId: String?) {
263+
databaseService.withRealm { realm ->
264+
val realmObjects = RealmMyLife.getMyLifeByUserId(mRealm, settings)
265+
if (realmObjects.isEmpty()) {
266+
if (!realm.isInTransaction) {
267+
realm.beginTransaction()
294268
}
295-
296-
val rawMylife: List<RealmMyLife> = RealmMyLife.getMyLifeByUserId(realm, settings)
297-
val myLife = rawMylife.filter { it.isVisible }
298-
.map { MyLifeData(it._id ?: "", it.title ?: "", it.imageId ?: "") }
299-
if (rawMylife.isEmpty()) {
300-
getMyLifeListBase(userId).forEach {
301-
realm.executeTransaction { r ->
302-
val ml = r.createObject(RealmMyLife::class.java, UUID.randomUUID().toString())
303-
ml.title = it.title
304-
ml.imageId = it.imageId
305-
ml.userId = userId
306-
ml.isVisible = true
307-
}
269+
val myLifeListBase = getMyLifeListBase(userId)
270+
var ml: RealmMyLife
271+
var weight = 1
272+
for (item in myLifeListBase) {
273+
ml = realm.createObject(RealmMyLife::class.java, UUID.randomUUID().toString())
274+
ml.title = item.title
275+
ml.imageId = item.imageId
276+
ml.weight = weight
277+
ml.userId = item.userId
278+
ml.isVisible = true
279+
weight++
308280
}
281+
realm.commitTransaction()
309282
}
310-
311-
312-
DashboardData(myLibrary, myCourses, myTeams, myLife)
313283
}
314284
}
315285

316-
private fun myLibraryItemClickAction(textView: TextView, library: MyLibraryData) {
286+
private fun myLibraryItemClickAction(textView: TextView, items: RealmMyLibrary?) {
317287
textView.setOnClickListener {
318-
val item = mRealm.where(RealmMyLibrary::class.java).equalTo("_id", library._id).findFirst()
319-
item?.let {
288+
items?.let {
320289
openResource(it)
321290
}
322291
}
@@ -366,48 +335,33 @@ open class BaseDashboardFragment : BaseDashboardFragmentPlugin(), NotificationCa
366335
view.findViewById<View>(R.id.txtFullName).setOnClickListener {
367336
homeItemClickListener?.openCallFragment(UserProfileFragment())
368337
}
369-
370338
viewLifecycleOwner.lifecycleScope.launch {
371-
showLoading(true)
372-
val data = loadDashboardData()
373-
renderDashboard(data, view)
374-
showLoading(false)
339+
myLibraryDiv(view)
375340
}
341+
initializeFlexBoxView(view, R.id.flexboxLayoutCourse, RealmMyCourse::class.java)
342+
initializeFlexBoxView(view, R.id.flexboxLayoutTeams, RealmMyTeam::class.java)
343+
initializeFlexBoxView(view, R.id.flexboxLayoutMyLife, RealmMyLife::class.java)
376344

345+
if (mRealm.isInTransaction) {
346+
mRealm.commitTransaction()
347+
}
377348
myCoursesResults = RealmMyCourse.getMyByUserId(mRealm, settings)
378349
myTeamsResults = RealmMyTeam.getMyTeamsByUserId(mRealm, settings)
350+
379351
myCoursesResults.addChangeListener(myCoursesChangeListener)
380352
myTeamsResults.addChangeListener(myTeamsChangeListener)
381353
}
382354

383-
private fun renderDashboard(data: DashboardData, view: View) {
384-
myLibraryDiv(data.myLibrary, view)
385-
setUpCourses(data.myCourses, view)
386-
setUpTeams(data.myTeams, view)
387-
setUpMyLife(data.myLife, view)
388-
}
389-
390-
private fun showLoading(loading: Boolean) {
391-
if (loading) {
392-
di = DialogUtils.CustomProgressDialog(requireContext())
393-
di?.show()
394-
} else {
395-
di?.dismiss()
396-
}
397-
}
398-
399355
private fun updateMyCoursesUI() {
400-
viewLifecycleOwner.lifecycleScope.launch {
401-
val data = loadDashboardData()
402-
setUpCourses(data.myCourses, requireView())
403-
}
356+
val flexboxLayout: FlexboxLayout = view?.findViewById(R.id.flexboxLayoutCourse) ?: return
357+
flexboxLayout.removeAllViews()
358+
setUpMyList(RealmMyCourse::class.java, flexboxLayout, requireView())
404359
}
405360

406361
private fun updateMyTeamsUI() {
407-
viewLifecycleOwner.lifecycleScope.launch {
408-
val data = loadDashboardData()
409-
setUpTeams(data.myTeams, requireView())
410-
}
362+
val flexboxLayout: FlexboxLayout = view?.findViewById(R.id.flexboxLayoutTeams) ?: return
363+
flexboxLayout.removeAllViews()
364+
setUpMyList(RealmMyTeam::class.java, flexboxLayout, requireView())
411365
}
412366

413367
override fun showResourceDownloadDialog() {

0 commit comments

Comments
 (0)