Add Dependency
implementation 'androidx.room:room-runtime:2.4.3'
kapt 'androidx.room:room-compiler:2.4.3'
implementation 'androidx.room:room-ktx:2.4.3'
Creating Package
클린아키텍처 & Hilt 를 따른다고 가정하고, 다음과 같이 패키지를 만든다

Preparing Entity & DAO & Database
utils → Constants.kt
object Constants {
const val CONTACTS_TABLE = "contacts_table"
const val CONTACTS_DATABASE = "contacts_database"
}
db → ContactsEntity.kt
@Entity(tableName = CONTACTS_TABLE)
data class ContactsEntity(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
var name: String = "",
var phone: String = "",
)
db → ContactsDao.kt (we will add more Query and function later)
@Dao
interface ContactsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveContact(contactsEntity: ContactsEntity)
@Query("SELECT * FROM $CONTACTS_TABLE")
fun getAllContacts(): Flow<MutableList<ContactsEntity>>
}
db → ContactsDB.kt
@Database(entities = [ContactsEntity::class],
version = 1,
exportSchema = false)
abstract class ContactsDB : RoomDatabase() {
abstract fun contactsDao(): ContactsDao
}
Preparing UI part
각각 item 담당하는 item_contacts.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="5dp"
app:cardElevation="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fontFamily="sans-serif-light"
android:maxLines="1"
android:padding="8dp"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Name Contact" />
<TextView
android:id="@+id/tvPhone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingBottom="8dp"
android:textColor="@color/black"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName"
tools:ignore="RtlSymmetry"
tools:text="Phone Number" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml
?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:background="@color/teal_700"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/menu_toolbar"
app:subtitle="All your contacts"
app:subtitleTextColor="#C6FFFFFF"
app:title="Contacts List"
app:titleTextColor="@color/white" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/listBody"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="5dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvContacts"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:clipToPadding="false"
android:paddingBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnShowDialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_baseline_person_add_alt_1_24"
app:backgroundTint="@color/teal_700"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white"
tools:ignore="SpeakableTextPresentCheck" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/emptyBody"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<include layout="@layout/empty_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/loading"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Creating Fragment Dialog
class AddContactFragment : DialogFragment() {
...
}
xml도 만들어줌
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/rounded_win"
android:padding="15dp"
tools:context=".ui.add.AddContactFragment">
<ImageView
android:id="@+id/imgClose"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/ic_baseline_clear_24"
app:layout_constraintBottom_toTopOf="@id/tvTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="32dp"
android:paddingVertical="8dp"
android:text="Add your contact infomation"
android:textColor="#575757"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/edtName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imgClose" />
<EditText
android:id="@+id/edtName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="Name"
android:maxLines="1"
android:minHeight="48dp"
android:padding="10dp"
android:textColorHint="#009688"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<EditText
android:id="@+id/edtPhone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="top"
android:hint="Phone"
android:inputType="number"
android:maxLines="1"
android:minHeight="48dp"
android:padding="10dp"
android:textColorHint="#009688"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edtName" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:paddingHorizontal="50dp"
android:text="Save"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
app:backgroundTint="@color/teal_700"
app:cornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edtPhone" />
</androidx.constraintlayout.widget.ConstraintLayout>
Create Application Class & Module Class
진입점을 지정하는 application
@HiltAndroidApp
class MyApp : Application() { }
진입점 manifest에 지정해줌
<application
android:name=".MyApp"
...
>
...
</application>
di패키지 내에서 db모듈 생성해줌
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context) = Room.databaseBuilder(
context, ContactsDB::class.java, CONTACTS_DATABASE
).allowMainThreadQueries()
.fallbackToDestructiveMigration().build()
@Provides
@Singleton
fun provideDao(db:ContactsDB) = db.contactsDao()
@Provides
fun provideEntity()=ContactsEntity()
}
Creating a Repository class
class DatabaseRepository @Inject constructor(private val dao: ContactsDao) {
suspend fun saveContact(entity : ContactsEntity)=dao.saveContact(entity)
fun getAllContacts()=dao.getAllContacts()
}
Creating a DataStatus Class
data class DataStatus<out T>(
val status: Status,
val data: T? = null,
val message: String? = null,
val isEmpty: Boolean? = false
) {
enum class Status {
LOADING, SUCCESS, ERROR
}
companion object {
fun <T> loading(): DataStatus<T> {
return DataStatus(Status.LOADING)
}
fun <T> success(data: T? ,isEmpty: Boolean?): DataStatus<T> {
return DataStatus(Status.SUCCESS, data, isEmpty = isEmpty)
}
fun <T> error(error: String): DataStatus<T> {
return DataStatus(Status.ERROR, message = error)
}
}
}
Creating a ViewModel Class
@HiltViewModel
class DatabaseViewModel @Inject constructor(
private val repository: DatabaseRepository
) : ViewModel() {
private val _contactsList = MutableLiveData<DataStatus<List<ContactsEntity>>>()
val contactsList : LiveData<DataStatus<List<ContactsEntity>>>
get() = _contactsList
//뷰모델 외부에 MutableLiveData를 노출하지 않기 위함 -> 캡슐화
fun saveContact(entity: ContactsEntity) = viewModelScope.launch {
repository.saveContact(entity)
}
fun getAllContacts() = viewModelScope.launch {
_contactsList.postValue(DataStatus.loading())
repository.getAllContacts()
.catch { _contactsList.postValue(DataStatus.error(it.message.toString())) }
.collect { _contactsList.postValue(DataStatus.success(it, it.isEmpty())) }
}
}
Creating an Adapter
diffCallback과 함께합니다~
@Singleton
class ContactsAdapter @Inject constructor() : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
private lateinit var binding: ItemContactsBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding = ItemContactsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setData(differ.currentList[position])
}
override fun getItemCount(): Int {
return differ.currentList.size
}
inner class ViewHolder : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun setData(item: ContactsEntity) {
binding.apply {
tvName.text = item.name
tvPhone.text = item.phone
}
}
}
private val differCallback = object : DiffUtil.ItemCallback<ContactsEntity>() {
override fun areItemsTheSame(oldItem: ContactsEntity, newItem: ContactsEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ContactsEntity, newItem: ContactsEntity): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
}
Add @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
프래그먼트에도 entry point 달아줌
@AndroidEntryPoint
class AddContactFragment : DialogFragment() {
...
}
Working Main Activity
필요한거 주입해주기
@Inject
lateinit var contactsAdapter: ContactsAdapter
private val viewModel: DatabaseViewModel by viewModels()
@Inject
lateinit var entity: ContactsEntity
private val viewModel: DatabaseViewModel by viewModels()
Uploaded by N2T