Slide 1

Slide 1 text

೥Ҏ্์ஔ͍ͯͨ͠ݸਓΞϓϦΛ ࠷ۙ ͷٕज़ͰϦϒʔτͨ͠࿩ ,PUMJOѪ޷ձWPM!4BOTBO

Slide 2

Slide 2 text

"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"] )

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

😅

Slide 6

Slide 6 text

WFS w .JHSBUF&DMJQTFQSPKFDUUP(SBEMF w .JHSBUF+BWBUP,PUMJO w 3FTUSVDUVSF6*XJUI+FUQBDL$PNQPTF w 'FUDITIPQMJTUGSPNXFCJOTUFBEPGCVJMUJOYNM w 3FQMBDFBETFSWJDF w .JHSBUFUP"EBQUJWF*DPOT w #VJMEBOEEFQMPZXJUI(JU)VC"DUJPOT w FUD

Slide 7

Slide 7 text

WFS .JHSBUF+BWBUP,PUMJO public enum Prefecture { શ౎ಓ෎ݝ,౦ژ౎,ਆಸ઒ݝ,ઍ༿ݝ,Ἒ৓ݝ,ಢ໦ݝ,࡛ۄݝ,ٶ৓ݝ,๺ւಓ,෱ౡݝ }

Slide 8

Slide 8 text

WFS .JHSBUF+BWBUP,PUMJO public enum Prefecture { ALL, TOKYO, KANAGAWA, CHIBA, IBARAKI, TOCHIGI, SAITAMA, MIYAGI, HOKKAIDO, FUKUSHIMA, KYOTO, NIGATA; private static final Map 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); } };

Slide 9

Slide 9 text

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::class) fun fromString(string: String) = Prefecture::class.sealedSubclasses .mapNotNull { it.objectInstance } .first { it.rawValue == string } } }

Slide 10

Slide 10 text

WFS 3FQMBDFBETFSWJDF

Slide 11

Slide 11 text

WFS 3FQMBDFBETFSWJDF

Slide 12

Slide 12 text

WFS 3FQMBDFBETFSWJDF

Slide 13

Slide 13 text

WFS w .JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN

Slide 14

Slide 14 text

.JHSBUFWBOJMMBDPEFXJUI42-JUFUP3PPN 3PPN w 03.BQQFSGPS42-JUFEBUBCBTF w .BEFCZ(PPHMF

Slide 15

Slide 15 text

.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); }

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

.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() } }

Slide 18

Slide 18 text

.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 } } }

Slide 19

Slide 19 text

.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") }

Slide 20

Slide 20 text

.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 @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) }

Slide 21

Slide 21 text

.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?, )

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

3FMFBTFEXJUIOPCVHTJO NJHSBUJPO🎉

Slide 26

Slide 26 text

#FGPSF 7BOJMMB42-JUF"OESPJE7JFX private class TravelListAdapter extends BaseAdapter { ... @Override public View getView(int position,View convertView,ViewGroup parent) { ... HashMap map = (HashMap) 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; }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

$PODMVTJPO w $PEFXSJUUFOTFWFSBMZFBSTBHPPGUFOEPFTOUNFFUDVSSFOUTUBOEBSET w 3FHVMBSNBJOUFOBODFDBOQSFWFOUTJHOJ fi DBOUJTTVFT w .JHSBUJOHMPDBMEBUBCBTFTDBOCFQBSUJDVMBSMZDIBMMFOHJOH TPQSPDFFE XJUIDBVUJPO

Slide 29

Slide 29 text

$PODMVTJPO

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

fi O