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

Storing in Android. The new Way.

Storing in Android. The new Way.

Ahmad Arif Faizin

December 19, 2021
Tweet

More Decks by Ahmad Arif Faizin

Other Decks in Technology

Transcript

  1. Storing Data in Android. The new Way. Ahmad Arif Faizin

    Curriculum Developer, Dicoding Indonesia Semarang
  2. Overview 1. File Storage ◦ Old ◦ New! 2. Preferences

    ◦ Old ◦ New! 3. Database ◦ Old ◦ New!
  3. 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.
  4. 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
  5. 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)
  6. 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) }
  7. Why We Need? 1. Untuk menyimpan data yang relatif kecil

    menggunakan Key-Value ◦ Setting aplikasi ◦ Konfigurasi ◦ Login session
  8. 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
  9. 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)
  10. 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 }
  11. 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 }
  12. 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
  13. 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
  14. 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
  15. 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) } }
  16. 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"
  17. 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)
  18. 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 )
  19. 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
  20. 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? )
  21. 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) }
  22. 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() } } }
  23. 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 ✨
  24. 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()
  25. 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 ... }
  26. Recap! 1. Scoped Storage ◦ MediaStore API ◦ Storage Access

    Framework/SAF 2. DataStore ◦ Preferences DataStore ◦ Proto DataStore 3. Room Database ◦ MultiMap Query ◦ AutoMigration