preface

We described how the dynamic module team switched the dynamic widgets home page to Kotlin code, refactored them to the MVVM architecture, and added automated testing. After refactoring, the team’s development efficiency and release quality improved significantly. However, local database management is still a lot of SQL statement spelling, very difficult to extend and maintain, and writing automated tests is very difficult.

We strongly recommend that you use Room instead of SQLite. The benefits of the framework are that it saves writing a lot of template code, is easy to maintain, and is interface oriented and easy to extend. On the performance side, some students may think that using native SQL statements can optimize performance to the extreme, but often ignore the cost of maintaining a large number of SQL spelling statements, and the framework supports custom SQL statements.

Here’s how the dynamic module’s local cache can be safely and incrementally reconfigured from Sqlite to Room.

refactoring

Graph TD A(1. Combing business logic)-->B(2. Analyzing original code design) B-->C(3. Supplementary guardian test) C-->D(4. Simple design) D-->E(5. E-->F(6. Integration Acceptance Test)

1. Sort out the service logic

The original cache logic is relatively simple and provides two main functions.

  • Query the cached dynamic data
  • Save cached dynamic data (save all data from the last load)

2. Analyze the original code design

class LocalDataSource @Inject constructor(@ApplicationContext private var mContext: Context) : DataSource {

    // Check whether the cursor is empty
    override fun getDynamicListFromCache(a): List<Dynamic> {
        val dynamicList: MutableList<Dynamic> = ArrayList()
        val dataBaseHelper = DataBaseHelper(mContext)
        val c = dataBaseHelper.writableDatabase.query(DataBaseHelper.dynamic_info, null.null.null.null.null.null)
        if (c.moveToFirst()) { // Check whether the cursor is empty
            for (i in 0 until c.count) {
                c.move(i) // Move to the specified record
                val id = c.getInt(c.getColumnIndex(DataBaseHelper.id))
                val content = c.getString(c.getColumnIndex(DataBaseHelper.content))
                val date = c.getLong(c.getColumnIndex(DataBaseHelper.date))
                dynamicList.add(Dynamic(id, content, date))
            }
        }
        return dynamicList
    }

    override fun saveDynamicToCache(dynamicList: List<Dynamic>?) { val dataBaseHelper = DataBaseHelper(mContext) dynamicList? .let { dataBaseHelper.writableDatabase.delete(DataBaseHelper.dynamic_info,null.null)
            for ((id, content, date) in dynamicList) {
                val cv = ContentValues()
                cv.put(DataBaseHelper.id, id)
                cv.put(DataBaseHelper.content, content)
                cv.put(DataBaseHelper.date, date)
                dataBaseHelper.writableDatabase.insert(DataBaseHelper.dynamic_info, null, cv)
            }
        }
    }
}
Copy the code

Main problems:

  • Too much template code, not good for maintaining extensions
  • Without transaction management, there may be exceptions in extreme big data cases
  • There are no guardian tests

3. Supplementary guard tests

@RunWith(AndroidJUnit4::class)
@MediumTest
class LocalDataSourceTest {

    @Test
    fun `should get dynamic is empty when database has not data`() {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        //when
        val dynamicListFromCache = localDataSource.getDynamicListFromCache()
        //then
        assertThat(dynamicListFromCache).isEmpty()
    }

    @Test
    fun `should get dynamic success when database has data`() {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        localDataSource.saveDynamicToCache(getMockData())
        //when
        val dynamicListFromCache = localDataSource.getDynamicListFromCache()
        //then
        val dynamicOne = dynamicListFromCache[0]
        assertThat(dynamicOne.id).isEqualTo(1)
        assertThat(dynamicOne.content).isEqualTo("What a beautiful day!)
        assertThat(dynamicOne.date).isEqualTo(1615963675000L)
        val dynamicTwo = dynamicListFromCache[1]
        assertThat(dynamicTwo.id).isEqualTo(2)
        assertThat(dynamicTwo.content).isEqualTo("This series is worth watching!")
        assertThat(dynamicTwo.date).isEqualTo(1615963688000L)}private fun getMockData(a): ArrayList<Dynamic> {
        val dynamicList = ArrayList<Dynamic>()
        dynamicList.add(Dynamic(1."What a beautiful day!.1615963675000L))
        dynamicList.add(Dynamic(2."This series is worth watching!".1615963688000L))
        return dynamicList
    }
}
Copy the code

4. Simple design

Refactor in steps and verify in small steps.

  1. Replace the SQLiteOpenHelper management database with Room management, but maintain the original SQL operation
  2. Adjust the original SQL operation mode to ROOM Dao form
  3. Use Coroutine to manage asynchronous operations on the database
  4. Adjust existing test cases

5. Perform security reconstruction in small steps

Replace the SQLiteOpenHelper management database with Room management, but maintain the original SQL operation

  1. Bean modification, note that the fields and table names need to be the same as before
@Entity(tableName="dynamic_info")
data class Dynamic(@PrimaryKey @ColumnInfo(name = "id") val id: Int,
                   @ColumnInfo(name = "content") val content: String,
                   @ColumnInfo(name = "date") val date: Long) {
    @Ignore
    val formatDate = DateUtil.getDateToString(date)
}
Copy the code
  1. Manage using SupportSQLiteOpenHelper and continue with writableDatabase
class LocalDataSource @Inject constructor(@ApplicationContext private var mContext: Context) : DataSource {

    val db = Room.databaseBuilder(
            mContext,
            AppDatabase::class.java, "dynamic.db"
    ).build()


    // Check whether the cursor is empty
    override fun getDynamicListFromCache(a): List<Dynamic> {
        val dynamicList: MutableList<Dynamic> = ArrayList()
        val dataBaseHelper = db.openHelper
        val c = dataBaseHelper.writableDatabase.query("")
        if (c.moveToFirst()) { // Check whether the cursor is empty
            for (i in 0 until c.count) {
                c.move(i) // Move to the specified record
                val id = c.getInt(c.getColumnIndex(DataBaseHelper.id))
                val content = c.getString(c.getColumnIndex(DataBaseHelper.content))
                val date = c.getLong(c.getColumnIndex(DataBaseHelper.date))
                dynamicList.add(Dynamic(id, content, date))
            }
        }
        return dynamicList
    }

    override fun saveDynamicToCache(dynamicList: List<Dynamic>?) { val dataBaseHelper = db.openHelper dynamicList? .let { dataBaseHelper.writableDatabase.delete(DataBaseHelper.dynamic_info,null.null)
            for ((id, content, date) in dynamicList) {
                val cv = ContentValues()
                cv.put(DataBaseHelper.id, id)
                cv.put(DataBaseHelper.content, content)
                cv.put(DataBaseHelper.date, date)
                dataBaseHelper.writableDatabase.insert(DataBaseHelper.dynamic_info, SQLiteDatabase.CONFLICT_REPLACE, cv)
            }
        }
    }
}
Copy the code

Adjust the original SQL operation mode to ROOM Dao form

class LocalDataSource @Inject constructor(@ApplicationContext private var mContext: Context) : DataSource {

    private val db = Room.databaseBuilder(
            mContext,
            AppDatabase::class.java, "dynamic.db"
    ).build()


    // Check whether the cursor is empty
    override  fun getDynamicListFromCache(a): List<Dynamic> {
        return db.dynamicDao().getAll()
    }

    override  fun saveDynamicToCache(dynamicList: List<Dynamic>?) { dynamicList? .let { db.dynamicDao().deleteAll() db.dynamicDao().insertAll(*it.toTypedArray()) } }Copy the code

Use Coroutine to manage asynchronous operations on the database

@Dao
interface DynamicDao {
    @Query("SELECT * FROM dynamic_info")
    suspend fun getAll(a): List<Dynamic>

    @Insert
    suspend fun insertAll(vararg dynamic: Dynamic)


    @Query("DELETE FROM dynamic_info")
    suspend fun deleteAll(a)
}
Copy the code

Adjust existing test cases

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@MediumTest
class LocalDataSourceTest {
    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp(a) {
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown(a) {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @Test
    fun `should get dynamic is empty when database has not data`() = runBlocking {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        //when
        val dynamicListFromCache = localDataSource.getDynamicListFromCache()
        //then
        assertThat(dynamicListFromCache).isEmpty()
    }

    @Test
    fun `should get dynamic success when database has data`() = runBlocking {
        //given
        val localDataSource = LocalDataSource(ApplicationProvider.getApplicationContext())
        localDataSource.saveDynamicToCache(getMockData())
        //when
        val dynamicListFromCache = localDataSource.getDynamicListFromCache()
        //then
        val dynamicOne = dynamicListFromCache[0]
        assertThat(dynamicOne.id).isEqualTo(1)
        assertThat(dynamicOne.content).isEqualTo("What a beautiful day!)
        assertThat(dynamicOne.date).isEqualTo(1615963675000L)
        val dynamicTwo = dynamicListFromCache[1]
        assertThat(dynamicTwo.id).isEqualTo(2)
        assertThat(dynamicTwo.content).isEqualTo("This series is worth watching!")
        assertThat(dynamicTwo.date).isEqualTo(1615963688000L)}private fun getMockData(a): ArrayList<Dynamic> {
        val dynamicList = ArrayList<Dynamic>()
        dynamicList.add(Dynamic(1."What a beautiful day!.1615963675000L))
        dynamicList.add(Dynamic(2."This series is worth watching!".1615963688000L))
        return dynamicList
    }
}
Copy the code

6. Integration acceptance test

  1. Enabled all daemon tests of the Dynamic module. Succeeded
 ./gradlew dynamicBundle:testDUT
 ./gradlew dynamicDebug:testDUT 
Copy the code
  1. When DynamicDebug is running, the following exception occurs:
 java.lang.IllegalStateException: Pre-packaged database has an invalid schema: dynamic_info(com.cloud.disk.bundle.dynamic.Dynamic).
     Expected:
    TableInfo{name='dynamic_info', columns={date=Column{name='date', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, content=Column{name='content', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='dynamic_info', columns={date=Column{name='date', type='LONG', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, content=Column{name='content', type='VARCHAR(1024)', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
        at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:163)
       
Copy the code

This is because we were running on the JVM environment during the test phase and did not notice the data migration issue in advance. Here we need to use ROOM’s Migration mechanism for data backup and Migration.

  private val MIGRATION_1_2 = object : Migration(1.2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE dynamic_info RENAME TO dynamic_info_back_up")
            database.execSQL("CREATE TABLE dynamic_info ( id INTEGER PRIMARY KEY NOT NULL, content TEXT NOT NULL,date INTEGER NOT NULL)")
            database.execSQL("INSERT INTO dynamic_info (id, content,date) SELECT id, content,date FROM dynamic_info_back_up")}}private val db = Room.databaseBuilder(
            mContext,
            AppDatabase::class.java, "dynamic.db"
    ).addMigrations(MIGRATION_1_2).build()

Copy the code

See Migrating a Room Database, Migrating from SQlite to Room for more migration and testing

conclusion

In this article, we have shared the process of moving the dynamic module database from Sqlite to Room. Using the framework saves a lot of template code and is easy to maintain and extend.

The CloudDisk team has split into multiple repositories, but each module maintains its own third-party libraries. In addition to the inconsistency of versions, it can also lead to the bundle build with multiple versions of the three-party library.

Next, Mobile Application legacy System refactoring (16) – Gradle dependency management. We will continue to demonstrate the unified management of Gradle version of CloudDisk.

CloudDisk example code

CloudDisk

Series of links

Refactoring legacy Systems for mobile Applications (1) – Start

Refactoring legacy systems for mobile applications (2) – Architecture

Refactoring legacy systems for mobile applications (3) – Examples

Refactoring legacy Systems in Mobile Applications (4) – Analysis

Mobile application legacy System refactoring (5) – Refactoring methods

Refactoring legacy Systems for mobile applications (6) – Test

Mobile application legacy System refactoring (7) – Decoupled refactoring Demonstration (1)+ video demonstration

Refactoring legacy Systems for mobile applications (8) – Dependency Injection

Refactoring legacy systems for mobile applications (9) – Routing

Refactoring legacy Systems in Mobile applications (10) — Decoupled Refactoring (2)

Refactoring legacy systems for mobile applications (11) – Product management

Refactoring legacy mobile applications (12) – Compile the debug

Refactoring legacy mobile applications (13) – Compile the debugger

Refactoring legacy mobile applications (13) – Compile the debugger

Refactoring legacy Systems in Mobile Applications (14) – Examples of Kotlin+MVVM refactoring

The outline

about

  • Author: Huang Junbin
  • Blog: junbin. Tech
  • GitHub: junbin1011
  • Zhihu: @ JunBin