@Parcelize
@Entity(tableName = "student_table")
data class Student(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "student_id")
val id: Int? = null,
val name: String,
val grade: String,
): Parcelable
@Dao
interface StudentDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStudent(student: Student)
@Query("SELECT * FROM student_table")
fun getAllStudents(): LiveData<List<Student>>
@Delete
suspend fun deleteStudent(student: Student)
@Query("DELETE FROM student_table")
suspend fun deleteAllStudent()
}
@Database(entities = [Student::class], version = 1 )
abstract class StudentDatabase : RoomDatabase() {
abstract fun getStudentDao() : StudentDao
companion object {
private var instance: StudentDatabase? = null
fun getDatabase(context: Context): StudentDatabase {
if(instance == null) {
instance = Room.databaseBuilder(
context, StudentDatabase::class.java, "student_database"
)
.build()
}
return instance as StudentDatabase
}
}
}
class StudentRepository(private val studentDao: StudentDao) {
val getAllStudent: LiveData<List<Student>> = studentDao.getAllStudents()
@WorkerThread
suspend fun insert(student: Student) {
studentDao.insertStudent(student)
}
@WorkerThread
suspend fun delete(student: Student){
studentDao.deleteStudent(student)
}
}
class StudentViewModel(private val repository: StudentRepository) : ViewModel() {
val allStudents: LiveData<List<Student>> = repository.getAllStudent
fun insert(student: Student) = viewModelScope.launch {
repository.insert(student)
}
fun delete(student: Student) = viewModelScope.launch {
repository.delete(student)
}
}
class StudentViewModelFactory(private val repository: StudentRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(StudentViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return StudentViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
종속항목
이다. 그럼 왜 꼭 DI라는 개념을 만들어야 했을까? 그것을 알아보려면 먼저 클래스가 종속항목
을 어떻게 얻는지부터 알아보자.class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
다른곳에서 객체를 가져온다. Context
getter 및 getSystemService()
등이 이런식으로 동작한다고 함.
객체를 매개변수로 제공받음. 아마 3번의 방법을 가장 많이 사용하지않을까싶다. 내가 만들고자하는 클래스를 인스턴스화 할때 객체를 주입해서 사용하는것이다. 바로 아래왁같이 말이다.
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
class Car() {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car(engine)
car.engine = Engine()
car.start()
}
ServiceLocator
라는 걸 사용하면 된다고 한다. 얘도 디자인패턴이라고하는데.. 그냥 인스턴스에 다 때려박고 필요할때 꺼내쓰는 냉장고 같은 느낌이랄까?object ServiceLocator {
fun getEngine(): Engine = Engine()
}
class Car {
private val engine = ServiceLocator.getEngine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
class StudentsApplication: Application() {
val database by lazy { StudentDatabase.getDatabase(this) }
val repository by lazy { StudentRepository(database.getStudentDao()) }
}
// MainActivity.kt
class MainActivity : AppCompatActivity() {
companion object {
fun newIntent(context: Context): Intent = Intent(context, MainActivity::class.java)
}
private val binding by lazy {
MainActivityBinding.inflate(layoutInflater)
}
private val listAdapter by lazy {
StudentListAdapter()
}
private val studentViewModel: StudentViewModel by viewModels {
StudentViewModelFactory((application as StudentsApplication).repository)
}
private val addStudentLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val studentModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data?.getParcelableExtra(
AddStudentActivity.EXTRA_STUDENT,
Student::class.java
)
} else {
result.data?.getParcelableExtra(AddStudentActivity.EXTRA_STUDENT)
}
studentModel?.let {
studentViewModel.insert(it)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG
).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initView()
}
private fun initView() = with(binding) {
recyclerMain.adapter = listAdapter
recyclerMain.layoutManager = LinearLayoutManager(this@MainActivity)
studentViewModel.allStudents.observe(this@MainActivity) {students ->
listAdapter.submitList(students)
}
fabMain.setOnClickListener {
val intent = AddStudentActivity.newIntent(this@MainActivity)
addStudentLauncher.launch(intent)
}
}
}
// RecyclerView ListAdapter
class StudentListAdapter: ListAdapter<Student, StudentListAdapter.StudentHolder>(
object: DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean {
return oldItem == newItem
}
}
) {
class StudentHolder(private val binding: StudentItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Student)=with(binding) {
itemId.text = item.id.toString()
itemName.text = item.name
itemGrade.text = item.grade
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentHolder {
return StudentHolder(StudentItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: StudentHolder, position: Int) {
holder.bind(getItem(position))
}
}
// AddStudentActivity.kt
class AddStudentActivity : AppCompatActivity() {
companion object {
const val EXTRA_STUDENT = "extra_student"
fun newIntent(context: Context): Intent = Intent(context, AddStudentActivity::class.java)
}
private val binding by lazy {
AddStudentActivityBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initView()
}
private fun initView()=with(binding) {
submitAddStudent.setOnClickListener {
val intent = Intent()
if(TextUtils.isEmpty(nameAddStudent.text) || TextUtils.isEmpty(gradeAddStudent.text)) {
setResult(RESULT_CANCELED, intent)
} else {
intent.apply {
putExtra(EXTRA_STUDENT, Student(
name = nameAddStudent.text.toString(),
grade = gradeAddStudent.text.toString()
))
}
setResult(RESULT_OK, intent)
}
finish()
}
}
}
<!-- main_activity.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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_main"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:title="@string/main_toolbar_title"
app:titleTextColor="@color/white"
android:background="@color/main_toolbar_bg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/recycler_main"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_main"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="@dimen/recyclerview_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_main"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:src="@drawable/baseline_add_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- student_item.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="@color/recyclerview_item_bg"
android:orientation="horizontal">
<TextView
android:id="@+id/item_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/recyclerview_item_padding"
android:textSize="@dimen/recyclerview_item_text_size"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1" />
<TextView
android:id="@+id/item_hyphen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/recyclerview_item_padding"
android:text="@string/recyclerview_item_hyphen"
android:textSize="@dimen/recyclerview_item_text_size"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/item_id"
app:layout_constraintEnd_toStartOf="@+id/item_name"
app:layout_constraintStart_toEndOf="@+id/item_id"
app:layout_constraintTop_toTopOf="@+id/item_id" />
<TextView
android:id="@+id/item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/recyclerview_item_padding"
android:textSize="@dimen/recyclerview_item_text_size"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/item_id"
app:layout_constraintStart_toEndOf="@+id/item_hyphen"
app:layout_constraintTop_toTopOf="@+id/item_id"
tools:text="정나미" />
<TextView
android:id="@+id/item_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/recyclerview_item_padding"
android:textSize="@dimen/recyclerview_item_text_size"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/item_id"
app:layout_constraintStart_toEndOf="@+id/item_name"
app:layout_constraintTop_toTopOf="@+id/item_id"
tools:text="3학년" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- add_student_activity.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"
android:layout_margin="15dp"
tools:context=".ui.AddStudentActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_add_student"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/main_toolbar_bg"
app:layout_constraintBottom_toTopOf="@+id/name_add_student"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/main_toolbar_title"
app:titleTextColor="@color/white" />
<EditText
android:id="@+id/name_add_student"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:hint="@string/add_student_edittext_name_hint"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_add_student" />
<EditText
android:id="@+id/grade_add_student"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/add_student_edittext_grade_hint"
android:inputType="text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name_add_student" />
<Button
android:id="@+id/submit_add_student"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/add_student_submit_button_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>