본문 바로가기
ANDROID/Android Jetpack

[Android/Jetpack] Room + LiveData + ViewModel : 코루틴을 이용한 예제(2)

by 주 녕 2021. 9. 25.
728x90

https://developer.android.com/

 

 

앱 아키텍처

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에 대한 자세한 내용은 아래 포스팅 참고!

 

[Android] Recyclerview Adpater 대신 ListAdapter 적용하기

Room + LiveData를 이용한 MVVM 패턴 실습을 진행하던 중, RecyclerView의 Adapter에 ListAdapter를 적용하는 예제를 참고하게 되어 보다 자세히 알아보기 위해 포스팅 하게 되었다. ListAdapter 구글 I/O 2018 ..

junyoung-developer.tistory.com

 

(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를 사용한 코드아래 포스팅을 참고!

 

[Android] Activity Result API : startActivityForResult() deprecated 해결

안드로이드 인스타그램 클론 프로젝트를 진행하던 중에 새로운 문제를 만났다. 오랜만에 안드로이드 스튜디오와 코틀린을 최신 버전으로 업데이트 받았는데, startActivityForResult() 메소드가 depreca

junyoung-developer.tistory.com

 

< 결과 >


 

reference >

 

 

728x90

댓글