Skip to content

Commit 7b44b5f

Browse files
author
Mihai-Cristian Condrea
committed
fix: Resolved database migration crashes and improved App Core Manager
- Addressed issues causing crashes during database migrations. - Enhanced the App Core Manager for improved stability and performance.
1 parent 0a3c101 commit 7b44b5f

File tree

15 files changed

+292
-188
lines changed

15 files changed

+292
-188
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ android {
1919
applicationId = "com.d4rk.androidtutorials"
2020
minSdk = 23
2121
targetSdk = 35
22-
versionCode = 98
22+
versionCode = 103
2323
versionName = "1.1.1"
2424
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2525
resourceConfigurations += listOf(

app/src/main/kotlin/com/d4rk/androidtutorials/data/core/AppCoreManager.kt

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ package com.d4rk.androidtutorials.data.core
55
import android.annotation.SuppressLint
66
import android.app.Activity
77
import android.app.Application
8+
import android.database.sqlite.SQLiteException
89
import android.os.Bundle
10+
import android.util.Log
911
import androidx.lifecycle.Lifecycle
1012
import androidx.lifecycle.LifecycleObserver
1113
import androidx.lifecycle.OnLifecycleEvent
1214
import androidx.lifecycle.ProcessLifecycleOwner
1315
import androidx.multidex.MultiDexApplication
1416
import androidx.room.Room
17+
import androidx.room.migration.Migration
1518
import com.d4rk.androidtutorials.data.client.KtorClient
1619
import com.d4rk.androidtutorials.data.core.ads.AdsCoreManager
1720
import com.d4rk.androidtutorials.data.core.datastore.DataStoreCoreManager
1821
import com.d4rk.androidtutorials.data.database.AppDatabase
19-
import com.d4rk.androidtutorials.data.database.MIGRATION_1_2
22+
import com.d4rk.androidtutorials.data.database.migrations.MIGRATION_1_2
23+
import com.d4rk.androidtutorials.data.database.migrations.MIGRATION_2_3
2024
import com.d4rk.androidtutorials.data.datastore.DataStore
2125
import com.d4rk.androidtutorials.utils.error.ErrorHandler.handleInitializationFailure
2226
import io.ktor.client.HttpClient
@@ -27,6 +31,7 @@ import kotlinx.coroutines.async
2731
import kotlinx.coroutines.awaitAll
2832
import kotlinx.coroutines.coroutineScope
2933
import kotlinx.coroutines.launch
34+
import kotlinx.coroutines.supervisorScope
3035

3136
class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCallbacks ,
3237
LifecycleObserver {
@@ -51,7 +56,7 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
5156
}
5257
}
5358

54-
private suspend fun initializeApp() = coroutineScope {
59+
private suspend fun initializeApp() = supervisorScope {
5560
val ktor = async { initializeKtorClient() }
5661
val dataBase = async { initializeDatabase() }
5762
val dataStore = async { initializeDataStore() }
@@ -87,15 +92,28 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
8792
private suspend fun initializeDatabase() {
8893
runCatching {
8994
database = Room.databaseBuilder(
90-
context = this@AppCoreManager ,
91-
klass = AppDatabase::class.java ,
95+
context = this@AppCoreManager,
96+
klass = AppDatabase::class.java,
9297
name = "Android Studio Tutorials"
93-
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().fallbackToDestructiveMigrationOnDowngrade().build()
98+
)
99+
.addMigrations(migrations = getMigrations())
100+
.fallbackToDestructiveMigration()
101+
.fallbackToDestructiveMigrationOnDowngrade()
102+
.build()
103+
104+
database.openHelper.writableDatabase
94105
}.onFailure {
95106
handleDatabaseError(exception = it as Exception)
96107
}
97108
}
98109

110+
private fun getMigrations() : Array<Migration> {
111+
return arrayOf(
112+
MIGRATION_1_2 ,
113+
MIGRATION_2_3 ,
114+
)
115+
}
116+
99117
private suspend fun initializeDataStore() {
100118
runCatching {
101119
dataStore = DataStore.getInstance(context = this@AppCoreManager)
@@ -131,19 +149,25 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
131149
}
132150

133151
private suspend fun handleDatabaseError(exception : Exception) {
134-
(exception as? IllegalStateException)
135-
?.takeIf { it.message?.contains("Migration failed") == true }
136-
?.let { eraseDatabase() }
152+
if (exception is SQLiteException || (exception is IllegalStateException && exception.message?.contains(other = "Migration failed") == true)) {
153+
eraseDatabase()
154+
}
137155
}
138156

139157
private suspend fun eraseDatabase() {
140158
runCatching {
141159
deleteDatabase("Android Studio Tutorials")
142160
}.onSuccess {
143161
initializeDatabase()
162+
}.onFailure {
163+
logDatabaseError(exception = it as Exception)
144164
}
145165
}
146166

167+
private fun logDatabaseError(exception : Exception) {
168+
Log.e("AppCoreManager" , "Database error: ${exception.message}" , exception)
169+
}
170+
147171
private fun markAppAsLoaded() {
148172
isAppLoaded = true
149173
}

app/src/main/kotlin/com/d4rk/androidtutorials/data/core/ads/AdsCoreManager.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,18 @@ open class AdsCoreManager(protected val context : Context) {
4444
}
4545
isLoadingAd = true
4646
val request : AdRequest = AdRequest.Builder().build()
47-
@Suppress("DEPRECATION") AppOpenAd.load(context ,
48-
AdsConstants.APP_OPEN_UNIT_ID ,
49-
request ,
50-
AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT ,
51-
object : AppOpenAd.AppOpenAdLoadCallback() {
52-
override fun onAdLoaded(ad : AppOpenAd) {
53-
appOpenAd = ad
54-
isLoadingAd = false
55-
loadTime = Date().time
56-
}
47+
@Suppress("DEPRECATION")
48+
AppOpenAd.load(context , AdsConstants.APP_OPEN_UNIT_ID , request , AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT , object : AppOpenAd.AppOpenAdLoadCallback() {
49+
override fun onAdLoaded(ad : AppOpenAd) {
50+
appOpenAd = ad
51+
isLoadingAd = false
52+
loadTime = Date().time
53+
}
5754

58-
override fun onAdFailedToLoad(loadAdError : LoadAdError) {
59-
isLoadingAd = false
60-
}
61-
})
55+
override fun onAdFailedToLoad(loadAdError : LoadAdError) {
56+
isLoadingAd = false
57+
}
58+
})
6259
}
6360

6461
private fun wasLoadTimeLessThanNHoursAgo() : Boolean {
@@ -77,7 +74,8 @@ open class AdsCoreManager(protected val context : Context) {
7774
onShowAdCompleteListener = object : OnShowAdCompleteListener {
7875
override fun onShowAdComplete() {
7976
}
80-
})
77+
}
78+
)
8179
}
8280

8381
fun showAdIfAvailable(

app/src/main/kotlin/com/d4rk/androidtutorials/data/core/datastore/DataStoreCoreManager.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import kotlinx.coroutines.flow.firstOrNull
1212

1313
open class DataStoreCoreManager(protected val context : Context) {
1414

15-
var isDataStoreLoaded : Boolean = false
1615
var dataStore : DataStore = AppCoreManager.dataStore
1716

18-
suspend fun initializeDataStore() : Boolean = coroutineScope {
17+
suspend fun initializeDataStore() = coroutineScope {
1918

2019
listOf(async {
2120
dataStore.getStartupPage().firstOrNull() ?: BottomBarRoutes.HOME
@@ -40,8 +39,5 @@ open class DataStoreCoreManager(protected val context : Context) {
4039
} , async {
4140
dataStore.usageAndDiagnostics.firstOrNull() ?: ! BuildConfig.DEBUG
4241
}).awaitAll()
43-
44-
isDataStoreLoaded = true
45-
return@coroutineScope this@DataStoreCoreManager.isDataStoreLoaded
4642
}
4743
}

app/src/main/kotlin/com/d4rk/androidtutorials/data/database/AppDatabase.kt

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,10 @@ package com.d4rk.androidtutorials.data.database
22

33
import androidx.room.Database
44
import androidx.room.RoomDatabase
5-
import androidx.room.TypeConverters
6-
import androidx.room.migration.Migration
7-
import androidx.sqlite.db.SupportSQLiteDatabase
8-
import com.d4rk.androidtutorials.data.database.converters.Converters
95
import com.d4rk.androidtutorials.data.database.dao.FavoriteLessonsDao
106
import com.d4rk.androidtutorials.data.database.table.FavoriteLessonTable
117

12-
@Database(entities = [FavoriteLessonTable::class] , version = 2 , exportSchema = false)
13-
@TypeConverters(Converters::class)
8+
@Database(entities = [FavoriteLessonTable::class] , version = 3 , exportSchema = false)
149
abstract class AppDatabase : RoomDatabase() {
1510
abstract fun favoriteLessonsDao() : FavoriteLessonsDao
16-
}
17-
18-
val MIGRATION_1_2 = object : Migration(1 , 2) {
19-
override fun migrate(db : SupportSQLiteDatabase) {
20-
db.execSQL(
21-
sql = """
22-
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
23-
`lessonId` TEXT PRIMARY KEY NOT NULL,
24-
`lessonTitle` TEXT NOT NULL,
25-
`lessonDescription` TEXT NOT NULL,
26-
`lessonType` TEXT NOT NULL,
27-
`lessonTags` TEXT NOT NULL,
28-
`thumbnailImageUrl` TEXT NOT NULL,
29-
`squareImageUrl` TEXT NOT NULL,
30-
`deepLinkPath` TEXT NOT NULL,
31-
`isFavorite` INTEGER NOT NULL
32-
)
33-
"""
34-
)
35-
36-
db.execSQL(
37-
sql = """
38-
INSERT INTO `Favorite Lessons_new` (`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
39-
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`)
40-
SELECT `id`, `title`, `description`, `type`, `tags`, `bannerImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
41-
FROM `Favorite Lessons`
42-
"""
43-
)
44-
45-
db.execSQL(sql = "DROP TABLE `Favorite Lessons`")
46-
47-
db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
48-
}
4911
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.d4rk.androidtutorials.data.database.migrations
2+
3+
import androidx.room.migration.Migration
4+
import androidx.sqlite.db.SupportSQLiteDatabase
5+
6+
val MIGRATION_1_2 : Migration = object : Migration(1 , 2) {
7+
override fun migrate(db : SupportSQLiteDatabase) {
8+
db.execSQL(
9+
sql = """
10+
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
11+
`lessonId` TEXT PRIMARY KEY NOT NULL,
12+
`lessonTitle` TEXT NOT NULL,
13+
`lessonDescription` TEXT NOT NULL,
14+
`lessonType` TEXT NOT NULL,
15+
`lessonTags` TEXT NOT NULL,
16+
`thumbnailImageUrl` TEXT NOT NULL,
17+
`squareImageUrl` TEXT NOT NULL,
18+
`deepLinkPath` TEXT NOT NULL,
19+
`isFavorite` INTEGER NOT NULL
20+
)
21+
""".trimIndent()
22+
)
23+
24+
db.execSQL(
25+
sql = """
26+
INSERT INTO `Favorite Lessons_new` (
27+
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
28+
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
29+
)
30+
SELECT
31+
`lessonId`, -- or whatever your original PK column was
32+
`title`,
33+
`description`,
34+
`type`,
35+
`tags`,
36+
`bannerImageUrl`, -- this becomes thumbnailImageUrl
37+
`squareImageUrl`,
38+
`deepLinkPath`,
39+
`isFavorite`
40+
FROM `Favorite Lessons`
41+
""".trimIndent()
42+
)
43+
44+
db.execSQL(sql = "DROP TABLE `Favorite Lessons`")
45+
46+
db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
47+
}
48+
}
49+
50+
val MIGRATION_2_3 : Migration = object : Migration(2 , 3) {
51+
override fun migrate(db : SupportSQLiteDatabase) {
52+
db.execSQL(
53+
sql = """
54+
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
55+
`lessonId` TEXT PRIMARY KEY NOT NULL,
56+
`lessonTitle` TEXT NOT NULL,
57+
`lessonDescription` TEXT NOT NULL,
58+
`lessonType` TEXT NOT NULL,
59+
`lessonTags` TEXT NOT NULL,
60+
`thumbnailImageUrl` TEXT NOT NULL,
61+
`squareImageUrl` TEXT NOT NULL,
62+
`deepLinkPath` TEXT NOT NULL,
63+
`isFavorite` INTEGER NOT NULL
64+
)
65+
""".trimIndent()
66+
)
67+
68+
db.execSQL(
69+
sql = """
70+
INSERT INTO `Favorite Lessons_new` (
71+
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
72+
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
73+
)
74+
SELECT
75+
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
76+
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
77+
FROM `Favorite Lessons`
78+
""".trimIndent()
79+
)
80+
81+
db.execSQL(sql = "DROP TABLE `Favorite Lessons`")
82+
83+
db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
84+
}
85+
}

app/src/main/kotlin/com/d4rk/androidtutorials/ui/components/layouts/NoLessonsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.d4rk.androidtutorials.R
2323

2424
@Composable
2525
fun NoLessonsScreen(
26-
text : Int = R.string.lesson_not_found ,
26+
text : Int = R.string.no_lessons_found ,
2727
icon : ImageVector = Icons.Default.Info ,
2828
iconDescription : String = "No lessons icon"
2929
) {

app/src/main/kotlin/com/d4rk/androidtutorials/ui/screens/favorites/FavoritesScreen.kt

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.animation.core.Transition
55
import androidx.compose.animation.core.animateFloat
66
import androidx.compose.animation.core.updateTransition
77
import androidx.compose.material.icons.Icons
8+
import androidx.compose.material.icons.outlined.ErrorOutline
89
import androidx.compose.material.icons.outlined.HeartBroken
910
import androidx.compose.runtime.Composable
1011
import androidx.compose.runtime.collectAsState
@@ -41,21 +42,29 @@ fun FavoritesScreen() {
4142
onDismiss = { viewModel.dismissErrorDialog() })
4243
}
4344

44-
if (isLoading) {
45-
LoadingScreen(progressAlpha)
46-
}
47-
else {
48-
favoriteLessons.firstOrNull()?.lessons?.let { lessonList ->
49-
if (lessonList.isEmpty()) {
50-
NoLessonsScreen(
51-
text = R.string.no_favorite_lessons_found , icon = Icons.Outlined.HeartBroken
45+
when {
46+
isLoading -> {
47+
LoadingScreen(progressAlpha)
48+
}
49+
50+
else -> {
51+
when (val lessons = favoriteLessons.firstOrNull()?.lessons) {
52+
null -> NoLessonsScreen(
53+
text = R.string.error_loading_favorites,
54+
icon = Icons.Outlined.ErrorOutline
5255
)
53-
}
54-
else {
55-
LessonListLayout(
56-
lessons = lessonList , context = context , visibilityStates = visibilityStates
56+
57+
emptyList<UiHomeScreen>() -> NoLessonsScreen(
58+
text = R.string.no_favorite_lessons_found,
59+
icon = Icons.Outlined.HeartBroken
60+
)
61+
62+
else -> LessonListLayout(
63+
lessons = lessons,
64+
context = context,
65+
visibilityStates = visibilityStates
5766
)
5867
}
59-
} ?: NoLessonsScreen()
68+
}
6069
}
6170
}

0 commit comments

Comments
 (0)