Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reboot a personal app abandoned for 10 years wi...

417.72KI
February 25, 2025

Reboot a personal app abandoned for 10 years with recent techs

417.72KI

February 25, 2025
Tweet

More Decks by 417.72KI

Other Decks in Programming

Transcript

  1. "CPVU data class Me( val name = “(redacted)", val twitter

    = "417_72ki", val gitHub = "417-72KI", val workAt = "**********", val job = "iOS engineer", val communities = ["love_swift", "Chiba.swift"] )
  2. WFS .JHSBUF+BWBUP,PUMJO public enum Prefecture { ALL, TOKYO, KANAGAWA, CHIBA,

    IBARAKI, TOCHIGI, SAITAMA, MIYAGI, HOKKAIDO, FUKUSHIMA, KYOTO, NIGATA; private static final Map<String, Prefecture> stringToPrefectureMap = new HashMap<>() { { put("શ౎ಓ෎ݝ", ALL); put("౦ژ౎", TOKYO); put("ਆಸ઒ݝ", KANAGAWA); put("ઍ༿ݝ", CHIBA); put("Ἒ৓ݝ", IBARAKI); put("ಢ໦ݝ", TOCHIGI); put("࡛ۄݝ", SAITAMA); put("ٶ৓ݝ", MIYAGI); put("๺ւಓ", HOKKAIDO); put("෱ౡݝ", FUKUSHIMA); put("ژ౎෎", KYOTO); put("৽ׁݝ", NIGATA); } };
  3. WFS .JHSBUF+BWBUP,PUMJO @Serializable(Prefecture.Companion.Serializer::class) @Parcelize sealed class Prefecture(override val rawValue: String,

    @StringRes val stringRes: Int) : RawStringRepresentable { data object Tokyo : Prefecture("tokyo", R.string.prefecture_tokyo) data object Kanagawa : Prefecture("kanagawa", R.string.prefecture_kanagawa) data object Chiba : Prefecture("chiba", R.string.prefecture_chiba) data object Ibaraki : Prefecture("ibaraki", R.string.prefecture_ibaraki) data object Tochigi : Prefecture("tochigi", R.string.prefecture_tochigi) data object Saitama : Prefecture("saitama", R.string.prefecture_saitama) data object Miyagi : Prefecture("miyagi", R.string.prefecture_miyagi) data object Hokkaido : Prefecture("hokkaido", R.string.prefecture_hokkaido) data object Fukushima : Prefecture("fukushima", R.string.prefecture_fukushima) data object Kyoto : Prefecture("kyoto", R.string.prefecture_kyoto) data object Nigata : Prefecture("nigata", R.string.prefecture_nigata) companion object { object Serializer : RawStringRepresentable.Companion.Serializer<Prefecture>(Prefecture::class) fun fromString(string: String) = Prefecture::class.sealedSubclasses .mapNotNull { it.objectInstance } .first { it.rawValue == string } } }
  4. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %#)FMQFSKBWB W @Override public void onCreate(SQLiteDatabase db) { String

    sql = "create table " + TABLE_TRAVEL + " (" + COLUMN_SHOP_ID + " int, " + COLUMN_DATE + " String, " + COLUMN_TIME + " String, " + COLUMN_SIZE + " String, " + COLUMN_TOPPING + " String," + COLUMN_COMMENT + " String" + ")"; db.execSQL(sql); }
  5. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %#)FMQFSLU W override fun onCreate(db: SQLiteDatabase) { val sql

    = when (version) { 1 -> """create table $TABLE_TRAVEL ( $COLUMN_SHOP_ID int, $COLUMN_DATE String, $COLUMN_TIME String, $COLUMN_SIZE String, $COLUMN_TOPPING String, $COLUMN_COMMENT String )""".trimIndent() 2 -> """create table $TABLE_TRAVEL ( $COLUMN_SHOP_ID int, $COLUMN_DATETIME String, $COLUMN_SIZE String, $COLUMN_TOPPING String, $COLUMN_COMMENT String )""".trimIndent() else -> "" } db.execSQL(sql) }
  6. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %#)FMQFSLU W override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion:

    Int) { try { db.beginTransaction() if (oldVersion <= 1 && newVersion >= 2) { val table = TABLE_TRAVEL db.execSQL("ALTER TABLE $table RENAME TO tmp_$table") onCreate(db) db.execSQL( """INSERT INTO $table ( $COLUMN_SHOP_ID, $COLUMN_DATETIME, $COLUMN_SIZE, $COLUMN_TOPPING, $COLUMN_COMMENT ) SELECT $COLUMN_SHOP_ID, datetime( substr($COLUMN_DATE, 0, 5)||'-'|| substr($COLUMN_DATE, 6, 2)||'-'|| substr($COLUMN_DATE, 9, 2)||' '|| substr($COLUMN_TIME, 0, 3)||':'|| substr($COLUMN_TIME, 4, 2), 'utc' ), $COLUMN_SIZE, $COLUMN_TOPPING, $COLUMN_COMMENT FROM tmp_$table """.trimIndent(), ) db.execSQL("DROP TABLE tmp_$table") db.setTransactionSuccessful() } } finally { db.endTransaction() } }
  7. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %BUB#BTFLU WXJUI3PPN @Database( entities = [TravelEntity::class], version = 3,

    exportSchema = true, ) @TypeConverters( DateTimeConverter::class, SizeConverter::class, RamenTypeConverter::class, ) abstract class TravelDataBase : RoomDatabase() { abstract fun dao(): TravelDao companion object { private var instance: TravelDataBase? = null fun getInstance( context: Context, name: String?, ): TravelDataBase { if (instance == null) { instance = if (name == null) { Room.inMemoryDatabaseBuilder(context, TravelDataBase::class.java) } else { Room.databaseBuilder(context, TravelDataBase::class.java, name) }.allowMainThreadQueries() .setQueryCallback( queryCallback, Executors.newSingleThreadExecutor(), ) .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) .build() } return instance as TravelDataBase } } }
  8. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %BUB#BTFLU WXJUI3PPN private val MIGRATION_1_2 = Migration(1, 2) {

    Log.d("MIGRATION_1_2", "Migration start") ... Log.d("MIGRATION_1_2", "Migration done") } private val MIGRATION_2_3 = Migration(2, 3) { Log.d("MIGRATION_2_3", "Migration start") ... Log.d("MIGRATION_2_3", "Migration done") }
  9. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN %BPLU @Dao interface TravelDao { @Query("SELECT * FROM travel

    ORDER BY datetime DESC LIMIT :limit OFFSET :offset") suspend fun fetch( limit: Int = 100, offset: Int = 0, ): List<TravelEntity> @Query("SELECT * FROM travel WHERE datetime = :datetime") suspend fun find(datetime: Instant): TravelEntity? @Insert suspend fun register(data: TravelEntity) @Update suspend fun update(data: TravelEntity) @Delete suspend fun delete(data: TravelEntity) }
  10. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN &OUJUZLU @Entity(tableName = "travel", primaryKeys = ["shop_id", "datetime"]) data

    class TravelEntity( @ColumnInfo(name = "shop_id") val shopId: Int, @ColumnInfo(name = "datetime") val dateTime: Instant, val size: Size, val type: Type, val topping: String?, val comment: String?, )
  11. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN #BDLVQ42-JUF fi MFUPEFCVHNJHSBUJPO @HiltAndroidApp class Application : Application() {

    override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { backupForDBMigration() } } private fun backupForDBMigration() { val dbFile = getDatabasePath(dbFileName) Log.d(TAG, "DB file: $dbFile, exists: ${dbFile.exists()}") val backupFile = getDatabasePath(dbFileName + "_bak") Log.d(TAG, "Backup file: $backupFile, exists: ${backupFile.exists()}") when { backupFile.exists() -> backupFile.copyTo(dbFile, overwrite = true) dbFile.exists() -> dbFile.copyTo(backupFile) } }
  12. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN 5FTUNJHSBUJPO @Test fun migrateFromV1() = runTest { val db

    = TravelDataBase.getInstance(ApplicationProvider.getApplicationContext(), "v1.db") val list = db.dao().fetch() assertAll( { assertEqual(list.count(), testData.count()) }, { val offSetHours = OffsetDateTime.now().offset.get(ChronoField.OFFSET_SECONDS) / 3600 list.zip(testData) .map { it.copy( second = it.second.let { // Offset for GitHub Actions it.copy(dateTime = it.dateTime.plus(9 - offSetHours.toLong(), ChronoUnit.HOURS)) }, ) } .forEach { assertEqual(it.first, it.second) } }, { ShadowLog.getLogs().let { assert(it.any { it.tag == "MIGRATION_1_2" }) { "`MIGRATION_1_2` must be run" } assert(it.any { it.tag == "MIGRATION_2_3" }) { "`MIGRATION_2_3` must be run" } } }, ) }
  13. .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN 5FTUNJHSBUJPO @Test fun migrateFromV2() = runTest { val db

    = TravelDataBase.getInstance(ApplicationProvider.getApplicationContext(), "v2.db") val list = db.dao().fetch() assertAll( { assertEqual(list.count(), testData.count()) }, { list.zip(testData) .forEach { assertEqual(it.first, it.second) } }, { ShadowLog.getLogs().let { assert(it.none { it.tag == "MIGRATION_1_2" }) { "`MIGRATION_1_2` must not be run" } assert(it.any { it.tag == "MIGRATION_2_3" }) { "`MIGRATION_2_3` must be run" } } }, ) }
  14. #FGPSF 7BOJMMB42-JUF "OESPJE7JFX private class TravelListAdapter extends BaseAdapter { ...

    @Override public View getView(int position,View convertView,ViewGroup parent) { ... HashMap<String, Object> map = (HashMap<String, Object>) getItem(position); int id = (Integer) map.get(DBHelper.COLUMN_SHOP_ID); Shop shop = shopList.getShopById(id); if(map != null){ shopName = (TextView) v.findViewById(R.id.shop); dateTime = (TextView) v.findViewById(R.id.datetime); size = (TextView) v.findViewById(R.id.size); topping = (TextView) v.findViewById(R.id.topping); memo = (TextView) v.findViewById(R.id.memo); shopName.setText(shop.getName()); String date = (String)map.get(DBHelper.COLUMN_DATE); String time = (String)map.get(DBHelper.COLUMN_TIME); dateTime.setText(date+" "+time); topping.setText((String) map.get(DBHelper.COLUMN_TOPPING)); size.setText((String) map.get(DBHelper.COLUMN_SIZE)); memo.setText((String) map.get(DBHelper.COLUMN_COMMENT)); } return v; }
  15. "GUFS 3PPN +FUQBDL$PNQPTF @Composable private fun RecordView(record: Record) { Column(

    verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .padding(vertical = 4.dp) .fillMaxWidth() .combinedClickable( onClick = { onClick?.let { it(record) } }, onLongClick = { showChoiceDialog = true }, ), ) { val locale = Locale.getDefault() val formatter = DateFormat.getBestDateTimePattern(locale, "yMMMMdEEEEHm") .let { DateTimeFormatter.ofPattern(it).withLocale(locale) } val dateTime = record.dateTime.format(formatter) Text( text = record.shop?.name ?: stringResource(R.string.no_shop_info), style = MaterialTheme.typography.headlineSmall, ) Text( text = dateTime, style = MaterialTheme.typography.bodyLarge, ) Text( text = "${record.size.label()} ${record.type.label()}", style = MaterialTheme.typography.bodyMedium, ) record.topping?.let { Text( text = it, style = MaterialTheme.typography.bodyMedium, ) } record.comment?.let { if (it.isNotEmpty()) { Text( text = it, style = MaterialTheme.typography.bodyMedium, ) } } } }