5. View 생성
(1) Main Activity 생성
activity_main.xml
연락처가 보여지는 화면을 그린 레이아웃 파일
- contact_recyclerview : 연락처 recyclerview id
- add_button : 새로운 연락처 추가 버튼 id
<?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=".view.ui.MainActivity">
<androidx.appcompat.widget.Toolbar
...
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/contact_recyclerview"
android:layout_margin="15dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@id/add_button"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/add_button"
android:backgroundTint="@color/darkGray"
android:src="@drawable/ic_round_add_24"
android:layout_margin="20dp"
app:borderWidth="0dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
item_contact.xml
RecyclerView에 보여지는 하나의 연락처에 대한 레이아웃
- item_textview_initial : name의 첫글자를 나타내는 TextView id
- item_textview_name : 연락처 주인의 이름(name)을 나타내는 TextView id
- item_textivew_number : 연락처 주인의 번호(number)를 나타내는 TextView id
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="5dp">
<TextView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="@+id/item_textview_initial"
android:padding="5dp"
android:layout_margin="15dp"
android:background="@drawable/item_initial_background"
android:gravity="center"
android:textColor="@color/white"
android:textSize="20sp"
android:text="H"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
...>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/item_textview_name"
android:textSize="17sp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="Your name"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/item_textview_number"
android:textSize="16sp"
android:textColor="@color/black"
android:text="Your number"/>
</LinearLayout>
</LinearLayout>
(2) RecyclerView 생성
해당 Adapter는 ListAdpater를 상속받고 있음을 알 수 있다.
RecyclerView의 데이터 로드 및 업데이트를 보다 효율적으로 동작하게 하기 위해 사용한 Adapter로 DiffUtil.ItemCallback을 사용한다는 점에서 기존의 Adapter와는 다르다. ListAdapter에 대한 자세한 내용은 다음 포스팅을 참고하면 좋을 것 같다.
class ContactAdapter : ListAdapter<Contact, ContactAdapter.ContactViewHolder>(ContactComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
return ContactViewHolder.create(parent)
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
val current = getItem(position)
holder.bind(current)
}
class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val nameTextView: TextView = itemView.findViewById<TextView>(R.id.item_textview_name)
private val numberTextView: TextView = itemView.findViewById(R.id.item_textview_number)
private val initialTextView: TextView = itemView.findViewById(R.id.item_textview_initial)
fun bind(contact: Contact) {
nameTextView.text = contact.name
numberTextView.text = contact.number
initialTextView.text = contact.name[0].uppercase()
}
companion object {
fun create(parent: ViewGroup): ContactViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contact, parent, false)
return ContactViewHolder(view)
}
}
}
class ContactComparator : DiffUtil.ItemCallback<Contact>() {
override fun areItemsTheSame(oldItem: Contact, newItem: Contact): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Contact, newItem: Contact): Boolean {
return oldItem.number == newItem.number
}
}
}
ListAdapter에 대한 자세한 내용은 아래 포스팅 참고!
(3) NewContact Activity 생성
activity_new_contact.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=".view.ui.NewContactActivity"
android:layout_gravity="center_vertical">
<TextView
.../>
<LinearLayout
android:id="@+id/new_linear1"
...>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Name"
android:textColor="@color/black"
android:textSize="16sp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/new_name_edittext"
android:textColor="@color/black"
android:textSize="17sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/new_linear2"
...>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Number"
android:textColor="@color/black"
android:textSize="16sp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/new_number_edittext"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
<Button
android:id="@+id/new_save_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save the Contact"
android:textColor="@color/white"
android:backgroundTint="@color/darkGray"
android:textSize="15sp"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
class NewContactActivity : AppCompatActivity() {
private lateinit var nameEditText: EditText
private lateinit var numberEditText: EditText
private lateinit var saveButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_contact)
nameEditText = findViewById(R.id.new_name_edittext)
numberEditText = findViewById(R.id.new_number_edittext)
saveButton = findViewById(R.id.new_save_button)
saveButton.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(numberEditText.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val name = nameEditText.text.toString()
val number = numberEditText.text.toString()
replyIntent.putExtra("name", name)
replyIntent.putExtra("number", number)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
}
6. repository 및 데이터베이스 인스턴스화
repository와 데이터베이스를 싱글톤으로 하여 전체 앱에서 하나씩만 사용하고자 하기 때문에 Application 클래스의 멤버로 해당 인스턴스들을 생성하는 것이다. 이렇게 구현하면 매번 구성하지 않고 필요할 때마다 Application에서 가져올 수 있기 때문!
class ContactApplication : Application() {
val applicationScope = CoroutineScope(SupervisorJob()) // 7번에서 설명
// 이러한 객체들은 앱을 시작할 때가 아닌 처음으로 필요할 때 생성해야 하기 때문에 by lazy
val database by lazy { ContactDatabase.getDatabase(this, applicationScope) }
val repository by lazy { ContactRepository(database!!.contactDao()) }
}
AndroidManifest.xml
Application 클래스를 생성했기 때문에 AndroidManifest 파일의 application의 android:name을 해당 클래스로 설정한다!
<application
android:name=".ContactApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ArchPractice">
...
</application>
7. 데이터베이스 채우기
앱을 만들 때마다 모든 콘텐츠를 삭제하고 데이터베이스를 다시 채우려면 RoomDatabase.Callback을 만들고 onCreate()를 재정의하면 된다. Room 작업은 메인 스레드에서 할 수 없으므로 onCreate()는 IO Dispatcher에서 코루틴을 실행한다.
코루틴을 실행하기 위해서는 CoroutineScope이 필요하기 때문에, getDatabase() 메서드에 scope 매개변수를 추가하여 해당 메소드를 업데이트 하도록 하자! DB를 채우는 작업은 UI의 lifecycle과는 관련이 없기 때문에 viewModelScope와 같은 CoroutineScope를 사용하면 안된다. 따라서 위의 ContactApplication에서 선언한 applicationScope를 사용한다.
@Database(entities = arrayOf(Contact::class), version = 1, exportSchema = false)
abstract class ContactDatabase: RoomDatabase() {
abstract fun contactDao(): ContactDAO
private class ContactDatabaseCallback(private val scope: CoroutineScope) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch {
var contactDao = database.contactDao()
contactDao.deleteAll()
}
}
}
}
companion object {
private var INSTANCE: ContactDatabase? = null
// 여러 스레드가 접근하지 못하도록 synchronized로 설정
fun getDatabase(context: Context, scope: CoroutineScope): ContactDatabase? {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ContactDatabase::class.java,
"contact"
).addCallback(ContactDatabaseCallback(scope)) // build 전 콜백 추가
.build()
INSTANCE = instance
instance
}
}
}
}
8. 데이터와 연결
MainActivity에 ViewModel을 만들기 위해 viewModel 위임을 사용하여 ContactViewModelFActory의 인스턴스를 전달하였다. 이는 ContactApplication의 repository에 기반하여 생성된다.
또, onCreate()에서 ContactViewModel의 contacts라는 LiveData 속성 Observer(관찰자)를 추가한다. → observe()
여기에서 adapter.submitList()를 통해서 recyclerView를 업데이트 시켜준다는 것을 알 수 있다 → ListAdapter 사용
class MainActivity : AppCompatActivity() {
private val contactViewModel: ContactViewModel by viewModels {
ContactViewModelFactory((application as ContactApplication).repository)
}
private lateinit var getResult: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.contact_recyclerview)
val adapter = ContactAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
contactViewModel.contacts.observe(this, Observer { contacts ->
contacts.let { adapter.submitList(it) }
})
getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val name: String = it.data?.getStringExtra("name").toString()
val number: String = it.data?.getStringExtra("number").toString()
val contact = Contact(name, number)
contactViewModel.insert(contact)
} else {
Toast.makeText(applicationContext, "empty not saved", Toast.LENGTH_SHORT).show()
}
}
val addButton = findViewById<FloatingActionButton>(R.id.add_button)
addButton.setOnClickListener {
val intent = Intent(this@MainActivity, NewContactActivity::class.java)
getResult.launch(intent)
}
}
}
MainActivity에서 NewContactActivity의 registerForActivityResult() 코드를 추가한다.
Activity가 RESULT_OK로 반환되면 contactViewModel의 insert() 메서드를 호출하여 반환된 연락처를 데이터베이스에 삽입! 반환된 데이터는 이전의 NewContactActivity에서 setResult()로 넘어온다는 것을 알 수 있다!
deprecated된 startActivityForResult를 대신한 registerForActivityResult()는
Activity Resut API를 사용한 코드로 아래 포스팅을 참고!
< 결과 >
reference >
'ANDROID > Android Jetpack' 카테고리의 다른 글
[Android/Jetpack] AAC - WorkManager (2) (0) | 2022.01.31 |
---|---|
[Android/Jetpack] AAC - WorkManager (Persistent Background) (0) | 2022.01.27 |
[Android/Jetpack] AAC - Paging (0) | 2022.01.19 |
[Android/Jetpack] Recyclerview Adpater 대신 ListAdapter 적용하기 (0) | 2021.09.27 |
[Android/Jetpack] Room + LiveData + ViewModel : 코루틴을 이용한 예제(1) (0) | 2021.09.24 |
[Android/Jetpack] AAC - Data Binding (0) | 2021.08.11 |
[Android/Jetpack] AAC - Room (0) | 2021.08.03 |
[Android/Jetpack] AAC - ViewModel (0) | 2021.07.27 |
댓글