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

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

Avatar for 417.72KI 417.72KI
February 25, 2025

Reboot a personal app abandoned for 10 years with recent techs

Avatar for 417.72KI

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, ) } } } }