Share
Embed
Storing in Android. The new Way.
DevFest 2021 GDG Semarang
https://www.youtube.com/watch?v=8igPjcyLKgM
Ahmad Arif Faizin
December 19, 2021
Transcript
44 slides
-
Storing Data in Android. The new Way. Ahmad Arif Faizin
Curriculum Developer, Dicoding Indonesia Semarang -
Overview 1. File Storage ◦ Old ◦ New! 2. Preferences
◦ Old ◦ New! 3. Database ◦ Old ◦ New! -
The Diff… Jenis Penyimpanan Tipe Data Penyimpanan Lama Penyimpanan App-specific
storage Penyimpanan media privat Sampai aplikasi di-uninstall/clear data. Shared storage Penyimpanan media publik Tetap ada walaupun di-uninstall/clear data. Preference Key-Value Sampai aplikasi di-uninstall/clear data. Database Tabel data lokal Sampai aplikasi di-uninstall/clear data. -
Why We Need? 1. App Specific Storage : Untuk menyimpan
data yang hanya dipakai oleh aplikasi kamu aja. ◦ JSON ◦ Plan text ◦ Specific file 2. Shared Storage : Untuk menyimpan data yang bisa dishare ke yang lain. ◦ Media (foto, audio, video) ◦ Downloaded Document -
MediaStore API ✨ val resolver = applicationContext.contentResolver val photoCollection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStore.Images.Media.getContentUri( MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } val newPhotoDetails = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "My Photo.jpg") } val myPhotoUri = resolver.insert(photoCollection, newPhotoDetails) -
Storage Access Framework (SAF) const val CREATE_FILE = 1 private
fun createFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" putExtra(Intent.EXTRA_TITLE, "invoice.pdf") putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, CREATE_FILE) } -
Why We Need? 1. Untuk menyimpan data yang relatif kecil
menggunakan Key-Value ◦ Setting aplikasi ◦ Konfigurasi ◦ Login session -
Shared Preference var sharedPref: SharedPreferences = getSharedPreferences( "my_pref", Context.MODE_PRIVATE) •
Create Instance val editor: SharedPreferences.Editor = sharedPref.edit() editor.putString(NAME, "Arif") editor.putInt(AGE, 22) editor.apply() // OR : editor.commit() • Save Data var name = sharedPref.getString(NAME, "") var name = sharedPref.getInt(AGE, 0) • Get Data -
The Diff… Fitur SharedPreferences Preferences DataStore Proto DataStore Async API
✅ (hanya untuk membaca perubahan suatu nilai, via Listener) ✅ (via Flow) ✅ (via Flow) Synchronous API ✅ (namun non-safe thread ketika dipanggil dalam UI thread) ❌ ❌ Aman Ketika Dipanggil dalam UI Thread ❌ ✅ (dijalankan melalui Dispacter.IO secara otomatis) ✅ (dijalankan melalui Dispacter.IO secara otomatis) Memberitahu Jika Terjadi Eror ❌ ✅ ✅ Aman dari Runtime Exception ❌ ✅ ✅ Memiliki Transactional API ❌ ✅ ✅ Menangani Migrasi Data ❌ ✅ (dari SharedPreferences) ✅ (dari SharedPreferences) Type Safety ❌ ❌ ✅ (menggunakan Protocol Buffer) -
DataStore Preference ✨ • Create Instance • Save Data •
Get Data val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") private val THEME_KEY = booleanPreferencesKey("theme_setting") dataStore.edit { preferences -> preferences[THEME_KEY] = isDarkModeActive } dataStore.data.map { preferences -> preferences[THEME_KEY] ?: false } -
Proto DataStore Preference ✨ • Create Instance • Save Data
• Get Data private val Context.dataStore: DataStore<Setting> by dataStore( fileName = "settings", serializer = SettingsSerializer ) dataStore.updateData { preferences -> preferences.toBuilder() .setTheme(isDarkModeActive) .build() } dataStore.data.map { preferences -> preferences.theme } -
Why We Need? 1. Untuk menyimpan data yang memang full
offline 2. Menyimpan cache sementara data online ◦ Tetap bisa digunakan walaupun offline ◦ Lebih cepat ◦ Mengurangi request server ◦ Hemat baterai ◦ Hemat kuota -
Table Example id title author genre pages 1 Clean Code:
A Handbook of Agile Software Craftsmanship Robert C. Martin Programming 434 2 A Brief History of Time Stephen Hawking Science 212 3 Rich Dad, Poor Dad Robert T. Kiyosaki Business 195 4 The 7 Habits of Highly Effective People Stephen Covey Self-help 372 5 The Rainbow Troops Andrea Hirata Novel 304 -
Database Contracts class BookContract { class BookEntry : BaseColumns {
companion object { val TABLE_NAME = "book" val COLUMN_TITLE = "title" val COLUMN_AUTHOR = "author" val COLUMN_GENRE = "genre" val COLUMN_PAGES = "pages" } } } id title author genre pages -
SQLiteOpenHelper class BooksHelper( context: Context ) : SQLiteOpenHelper(context, DATABASE_NAME, null,
DATABASE_VERSION) { companion object { val DATABASE_VERSION = 1 val DATABASE_NAME = "Book.db" } override fun onCreate(db: SQLiteDatabase) { db.execSQL(SQL_CREATE_ENTRIES) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL(SQL_DELETE_ENTRIES) onCreate(db) } } -
Basic Database private val SQL_CREATE_ENTRIES="CREATE TABLE ${BookEntry.TABLE_NAME}"+ " (${BookEntry._ID} INTEGER
PRIMARY KEY," + " ${BookEntry.COLUMN_TITLE} TEXT," + " ${BookEntry.COLUMN_AUTHOR} TEXT," + " ${BookEntry.COLUMN_GENRE} TEXT," + " ${BookEntry.COLUMN_PAGES} INT)" • Create Database: "CREATE TABLE book (id INTEGER PRIMARY KEY, title TEXT, author TEXT, genre TEXT, pages INT)" private val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $BookEntry.TABLE_NAME" • Delete Database: "DROP TABLE IF EXISTS book" -
Insert Database // Gets the data repository in write mode
val dbHelper: BooksHelper = BooksHelper(this) val db: SQLiteDatabase = dbHelper.getWritableDatabase() // Create a new map of values, where column names are the keys val values = ContentValues() values.put(BookEntry.COLUMN_TITLE, "Clean Code") values.put(BookEntry.COLUMN_AUTHOR, "Robert C. Martin") values.put(BookEntry.COLUMN_GENRE, "Programming") values.put(BookEntry.COLUMN_PAGES, 434) // Insert the new row, returning the primary key value of the new row val newRowId: Long = db.insert(BookEntry.TABLE_NAME, null, values) -
Read Database val db: SQLiteDatabase = dbHelper.getReadableDatabase() val cursor: Cursor
= db.query( BookEntry.TABLE_NAME, // table null, // column null, // selection null, // selectionArgs null, // groupBy null, // having null, // orderBy null // limit ) -
The Diff… SQLite Room SQL Query Validation Runtime Check Compile
Time Check Boilerplate Cukup perlu banyak kode Lebih simpel dan mudah Support LiveData/Flow Perlu konfigurasi tambahan Native Support Change Schema Cukup Sulit Cukup mudah Get Java Object Perlu Cursor Bisa Langsung -
Entity • Entity dibuat dengan menggunakan POJO class/ data class
• 1 instance = 1 baris data • Member variable = Nama kolom @Entity(tableName = "book") data class BookEntity( @PrimaryKey (autoGenerate=true) val id: Int, @ColumnInfo(name = "title") val title: String?, @ColumnInfo(name = "author") val author: String?, @ColumnInfo(name = "genre") val genre: String?, @ColumnInfo(name = "pages") val pagesNumber: Int? ) -
DAO @Dao interface BookDao { @Query("SELECT * FROM book ORDER
BY id ASC") fun getAll(): LiveData<List<BookEntity>> @Query("SELECT * FROM book ORDER BY RANDOM() LIMIT 1") fun getRandomBook(): BookEntity @Query("SELECT * FROM book WHERE title LIKE :title OR author LIKE :author") fun findByTitleOrAuthor(title: String, author: String): List<BookEntity> @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(book: BookEntity) @Update fun update(book: BookEntity) @Delete fun delete(book: BookEntity) } -
Database @Database(entities = arrayOf(BookEntity::class), version = 1, exportSchema = false)
public abstract class AppDatabase : RoomDatabase() { abstract fun bookDao(): BookDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getDatabase(context: Context): AppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "database-name" ).build() } } } -
Relational Query @Dao interface UserBookDao { @Query( "SELECT user.name AS
userName, book.name AS bookName " + "FROM user, book " + "WHERE user.id = book.user_id" ) fun loadUserAndBookNames(): LiveData<List<UserBook>> } data class UserBook(val userName: String?, val bookName: String?) @Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) fun loadUserAndBookNames(): Map<User, List<Book>> Using Multimap ✨ -
Manual Migration private val MIGRATION_1_2 = object : Migration(1, 2)
{ override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `_new_book` (`id` INTEGER NOT NULL, `originalTitle` TEXT NOT NULL, PRIMARY KEY(`id`))") database.execSQL("INSERT INTO `_new_book` (id, oroginalTitle) SELECT id, name FROM Book") database.execSQL("DROP TABLE Book") database.execSQL("ALTER TABLE _new_book RENAME TO Book") database.execSQL("PRAGMA foreign_key_check(Book)") } } Room.databaseBuilder(applicationContext, BookDatabase::class.java, "book_database") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() -
AutoMigration ✨ @Database( entities = [Book::class], version = 2, autoMigrations
= [ AutoMigration(from = 1, to = 2, spec = BookDatabase.MyAutoMigration::class), ], exportSchema = true ) abstract class BookDatabase : RoomDatabase() { @RenameColumn(tableName = "Book", fromColumnName = "title", toColumnName = "originalTitle") class MyAutoMigration : AutoMigrationSpec ... } -
Recap! 1. Scoped Storage ◦ MediaStore API ◦ Storage Access
Framework/SAF 2. DataStore ◦ Preferences DataStore ◦ Proto DataStore 3. Room Database ◦ MultiMap Query ◦ AutoMigration