안드로이드 아키텍쳐 중 LiveData 를 통해 MVVM 을 적용한 MyToDo 앱 을 만들어보았습니다.
https://developer.android.com/topic/libraries/architecture/livedata?hl=ko
https://eunoia3jy.tistory.com/119
아래의 사진처럼 Recyclerview 를 이용하여 ToDo 아이템 목록을 만들고 추가/삭제 버튼을 통해 insert/Delete 하여 아이템 목록을 추가/삭제 하는 앱을 만들어보도록 할게요! 😬😬😬
💡 작성한 파일 목록 입니다.
1. build.gradle(:app)
2. Room 생성
2-1. TodoModel.kt
2-2. TodoDao.kt
2-3. TodoDatabase.kt
3. Repository 생성
3-1. TodoRepository.kt
4. ViewModel 생성
4-1. TodoViewModel.kt
5. View 생성
5-1. MainActivity.kt
5-2. activity_main.xml
5-3. TodoListAdapter.kt
5-4. item_list.xml
5-5. dialog_add.xml
1. build.gradle(:app)
Kotlin 언어를 사용하기 위해 상단 plugins {} 안에 id 'kotlin-kapt' 플러그인을 적용합니다.
그리고 하단 dependencies {} 안에 room, livedata, recyclerview, cardview 에 대한 라이브러리를 사용하기 위해 추가해 줍니다.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.eun.mytodokotlin_mvvm_01"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//room
implementation "androidx.room:room-runtime:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
//livedata
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
kapt 'android.arch.lifecycle:compiler:1.1.1'
//recyclerview, cardview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
}
Room 데이터 생성
룸 Room : 안드로이드 로컬 SQLite 데이터베이스를 사용하는 라이브러리.
https://developer.android.com/jetpack/androidx/releases/room?hl=ko
2-1. TodoModel.kt
data class 를 만들고 @Entity 속성을 통해 테이블 및 컬럼을 생성하는 TodoModel 클래스를 만듭니다.
package com.eun.mytodokotlin_mvvm_01.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tb_todo") //@Entity 의 tableName = : 테이블명 지정
data class TodoModel(
@PrimaryKey(autoGenerate = true) //@PrimaryKey 의 autoGenerate = : null 을 받으면 id 값을 자동으로 할당해준다
var id: Long?,
@ColumnInfo(name = "seq") //@ColumnInfo : 컬럼명 지정. 컬럼명을 변수명과 같이 쓰려면 생략 가능
var seq: Int,
@ColumnInfo(name = "title") //@ColumnInfo : 컬럼명 지정. 컬럼명을 변수명과 같이 쓰려면 생략 가능
var title: String,
@ColumnInfo(name = "content") //@ColumnInfo : 컬럼명 지정. 컬럼명을 변수명과 같이 쓰려면 생략 가능
var content: String,
@ColumnInfo(name = "createDate") //@ColumnInfo : 컬럼명 지정. 컬럼명을 변수명과 같이 쓰려면 생략 가능
var createDate: Long
) {
constructor() : this(null, 0, "", "", -1)
}
2-2. TodoDao.kt
SQL 작성을 위한 TodoDao 인터페이스를 만듭니다.
@Query, @Insert, @Update, @Delete 등의 어노테이션을 사용할 수 있습니다.
그 중 @Insert, @Update 는 onConflict 속성을 지정할 수 있어요.
package com.eun.mytodokotlin_mvvm_01.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
@Dao
interface TodoDao {
@Query("SELECT * FROM tb_todo ORDER BY SEQ ASC")
fun getTodoListAll(): LiveData<List<TodoModel>> //getAll 함수에 LiveData 를 반환
@Insert(onConflict = OnConflictStrategy.REPLACE) //@Insert 의 onConflict = : 중복된 데이터의 경우 어떻게 처리할 것인지에 대한 처리를 지정
fun insert(todo: TodoModel)
@Delete
fun delete(todo: TodoModel)
}
2-3. TodoDatabase.kt
실질적인 데이터베이스 인스턴스를 생성하는 TodoDatabase 클래스는 RoomDatabase 를 상속하는 추상 클래스로 생성합니다.
@Database 어노테이션을 이용해줍니다.
package com.eun.mytodokotlin_mvvm_01.data.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.eun.mytodokotlin_mvvm_01.data.dao.TodoDao
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
/*
* @Database 의 entites = : entity 정의
* @Database 의 version = : SQLite 버전 지정
* */
@Database(entities = [TodoModel::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
//데이터베이스 인스턴스를 싱글톤으로 사용하기 위해 companion object 안에 만들어준다
companion object {
private var INSTANCE: TodoDatabase? = null
//getInstance() : 여러 스레드가 접근하지 못하도록 synchronized로 설정
fun getInstance(context: Context): TodoDatabase? {
if (INSTANCE == null) {
synchronized(TodoDatabase::class) {
INSTANCE = Room.databaseBuilder( //Room.databaseBuilder 로 인스턴스를 생성
context.applicationContext,
TodoDatabase::class.java,
"tb_todo"
)
.fallbackToDestructiveMigration() //.fallbackToDestructiveMigration() : 데이터베이스가 갱신될 때 기존의 테이블을 버리고 새로 사용하도록 설정
.build()
}
}
return INSTANCE
}
}
}
3. Repository 생성
Repository : ViewModel 과 상호작용하기 위해 정리된(Clean) 데이터 API 를 들고 있는 클래스
VIewModel 이 직접 DB 나 서버에 접근하지 않고, Repository 에 접근하여 앱의 데이터를 관리하여 필요한 데이터를 가져옵니다.
3-1. TodoRepository.kt
TodoRepository 클래스 에서는 TodoModel, TodoDao, TodoDatabase 를 각각 초기화하고 ViewModel 에서 DB 접근을 요청할 때 수행할 함수를 만듭니다.
package com.eun.mytodokotlin_mvvm_01.data.repository
import android.app.Application
import androidx.lifecycle.LiveData
import com.eun.mytodokotlin_mvvm_01.data.dao.TodoDao
import com.eun.mytodokotlin_mvvm_01.data.database.TodoDatabase
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
import java.lang.Exception
/*
* 데이터베이스 혹은 네트워크 통신을 통하여 데이터를 얻는 기능을 분리
* ViewModel 에서는 이 Repository 를 통해 데이터를 얻는다
* */
class TodoRepository(application: Application) {
//database, dao todoItems 를 각각 초기화
private var todoDatabase: TodoDatabase = TodoDatabase.getInstance(application)!!
private var todoDao: TodoDao = todoDatabase.todoDao()
private var todoItems: LiveData<List<TodoModel>> = todoDao.getTodoListAll()
fun getTodoListAll(): LiveData<List<TodoModel>> {
return todoItems
}
fun insert(todoModel: TodoModel) {
try {
val thread =
Thread(Runnable { //별도 스레드를 통해 Room 데이터에 접근해야한다. 연산 시간이 오래 걸리는 작업은 메인 쓰레드가 아닌 별도의 쓰레드에서 하도록 되어있다. 그렇지 않으면 런타임에러 발생.
todoDao.insert(todoModel)
}).start()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun delete(todoModel: TodoModel) {
try {
val thread = Thread(Runnable {
todoDao.delete(todoModel)
})
thread.start()
} catch (e: Exception) {
}
}
}
4. ViewModel 생성
ViewModel : View 를 위한 데이터를 가지고 있다.
액티비티의 생명주기에서 자유로울 순 없지만 ViewModel 은 View 와 ViewModel 의 분리로 액티비티가 destroy 되었다가 다시 create 되어도 종료되지 않고 가지고 있다.
4-1. TodoViewModel.kt
데이터를 가져오는 TodoViewModel 클래스를 AndroidViewModel 를 상속하는 클래스로 생성합니다.
Repository 를 통해서 Room 데이터베이스의 인스턴스를 만들 때 Context 가 필요합니다.
만약 ViewModel 의 Context 를 사용하면 액티비티가 destroy 되는 경우 메모리 에러가 발생할 수 있어서 Application Context 를 사용하기 위해 application 을 인자로 받습니다.
package com.eun.mytodokotlin_mvvm_01.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
import com.eun.mytodokotlin_mvvm_01.data.repository.TodoRepository
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val todoRepository = TodoRepository(application)
private var todoItems =
todoRepository.getTodoListAll() //액티비티(View) 에서 ViewModel 의 todoItems 리스트를 observe 하고 리스트를 갱신
/* repsitory 에 넘겨 viewModel 의 기능 함수 구현 */
fun getTodoListAll(): LiveData<List<TodoModel>> {
return todoItems
}
fun insert(todoModel: TodoModel) {
todoRepository.insert(todoModel)
}
fun delete(todoModel: TodoModel) {
todoRepository.delete(todoModel)
}
}
5. View 생성
VIew : UI Controller 를 담당하는 Activity, Fragment
사용자와 상호작용하고 데이터의 변화를 감지하기 위한 observer 를 가지고 있다.
5-1. MainActivity.kt
package com.eun.mytodokotlin_mvvm_01.view
import android.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.eun.mytodokotlin_mvvm_01.R
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
import com.eun.mytodokotlin_mvvm_01.viewmodel.TodoViewModel
import java.util.*
import kotlin.collections.ArrayList
class MainActivity : AppCompatActivity() {
val TAG: String = MainActivity::class.java.name;
private lateinit var todoViewModel: TodoViewModel //TodoViewModel 인스턴스를 만들고, 이를 관찰
private lateinit var todoListAdapter: TodoListAdapter
private val todoItems: ArrayList<TodoModel> = ArrayList()
private val recyclerview_list: RecyclerView by lazy {
findViewById<RecyclerView>(R.id.recyclerview_list)
}
private val btn_add: Button by lazy {
findViewById<Button>(R.id.btn_add)
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "== onCreate ");
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// todoItems.run {
// add(
// TodoModel(
// null,
// 1,
// "방청소 1 ",
// "책상정리",
// Date().time
// )
// )
// add(
// TodoModel(
// null,
// 2,
// "방청소 2 ",
// "옷장정리",
// Date().time
// )
// )
// }
initViewModel()
initRecyclerview()
initBtnAdd()
}
/*
* ViewModel 설정
* View 에서 ViewModel 을 관찰하여 데이터가 변경될 때 내부적으로 자동으로 알 수 있도록 한다.
* ViewModel 은 View 와 ViewModel 의 분리로 액티비티가 destroy 되었다가 다시 create 되어도 종료되지 않고 가지고 있다.
* */
private fun initViewModel() {
todoViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(application)
.create(TodoViewModel::class.java)
todoViewModel.getTodoListAll().observe(this, androidx.lifecycle.Observer {
todoListAdapter.setTodoItems(it)
})
}
/*
* Recyclerview 설정
* Recyclerview adapter 와 LinearLayoutManager 를 만들고 연결
* */
private fun initRecyclerview() {
todoListAdapter =
TodoListAdapter({ todo -> deleteDialog(todo) }) //adapter 에 click 시 해야할 일을 (todo) -> Unit 파라미터로 넘겨준다
recyclerview_list.run {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = todoListAdapter
}
}
/*
* btn_add 설정
* */
private fun initBtnAdd() {
btn_add.setOnClickListener {
showAddTodoDialog()
}
}
/*
* Todo 리스트를 추가하는 Dialog 띄우기
* TodoModel 을 생성하여 리스트 add 해서 리스트를 갱신
* */
private fun showAddTodoDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_add, null)
val et_add_title: EditText by lazy {
dialogView.findViewById<EditText>(R.id.et_add_title)
}
val et_add_content: EditText by lazy {
dialogView.findViewById<EditText>(R.id.et_add_content)
}
var builder = AlertDialog.Builder(this)
val dialog = builder.setTitle("Todo 아이템 추가하기").setView(dialogView)
.setPositiveButton(
"확인"
) { dialogInterface, i ->
var id: Long? = null
val title = et_add_title.text.toString()
val content = et_add_content.text.toString()
val createdDate = Date().time
val todoModel = TodoModel(
id,
todoListAdapter.getItemCount() + 1,
title,
content,
createdDate
)
todoViewModel.insert(todoModel)
}
.setNegativeButton("취소", null)
.create()
dialog.show()
}
/*
* Todo 리스트를 삭제하는 Dialog 띄우기
* */
private fun deleteDialog(todoModel: TodoModel) {
val builder = AlertDialog.Builder(this)
builder.setMessage(todoModel.seq.toString()+" 번 Todo 아이템을 삭제할까요? ")
.setNegativeButton("취소") { _, _ -> }
.setPositiveButton("확인") { _, _ ->
todoViewModel.delete(todoModel)
}
.create()
builder.show()
}
}
5-2. activity_main.xml
MainActivity 를 구성하는 xml 입니다.
RecyclerView 에 item_list.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.MainActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="60dp"
android:background="#FFE08C"
android:elevation="10dp"
android:gravity="center"
android:text="My ToDo App"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/recyclerview_list"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/btn_add"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
tools:listitem="@layout/item_list" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_add"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#6799FF"
android:text="추가"
android:textSize="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5-3. TodoListAdapter.kt
TodoModel 의 리스트를 생상자로부터 전달받으며 RecycleView.Adapter 를 상속받고 RecyclerView.TodoViewHolder 를 뷰홀더로 갖는 TodoListAdapter 클래스 를 만듭니다.
package com.eun.mytodokotlin_mvvm_01.view
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.eun.mytodokotlin_mvvm_01.R
import com.eun.mytodokotlin_mvvm_01.data.model.TodoModel
import java.text.SimpleDateFormat
import java.util.*
class TodoListAdapter(val deletetItemClick: (TodoModel) -> Unit) :
RecyclerView.Adapter<TodoListAdapter.TodoViewHolder>() {
private var todoItems: List<TodoModel> = listOf()
/*
* 이 어뎁터가 아이템을 얼마나 가지고 있는지 얻는 함수
* */
override fun getItemCount(): Int {
Log.d("MainActivity", "todoItem getItemCount !!: " + todoItems.size);
return todoItems.size
}
/*
* 현재 아이템이 사용할 뷰홀더를 생성하여 반환하는 함수
* item_list 레이아웃을 사용하여 뷰를 생성하고 뷰홀더에 뷰를 전달하여 생성된 뷰홀더를 반환
* */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
val viewHolder = TodoViewHolder(view)
return viewHolder
}
/*
* 현재 아이템의 포지션에 대한 데이터 모델을 리스트에서 얻고
* holder 객체를 TodoViewHolder 로 형변환한 뒤 bind 메서드에 이 모델을 전달하여 데이터를 바인딩하도록 한다
* */
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val todoModel = todoItems[position]
todoModel.seq = position + 1;
val todoViewHolder = holder as TodoViewHolder
todoViewHolder.bind(todoModel)
}
/* 데이터베이스가 변경될 때마다 호출 */
fun setTodoItems(todoItems: List<TodoModel>) {
this.todoItems = todoItems
Log.d("MainActivity", "todoItem setTodoItems !!: " + todoItems.size);
notifyDataSetChanged()
}
/*
* 뷰홀더는 리스트를 스크롤하는 동안 뷰를 생성하고 다시 뷰의 구성요소를 찾는 행위를 반복하면서 생기는
* 성능저하를 방지하기 위해 미리 저장 해 놓고 빠르게 접근하기 위하여 사용하는 객체
* */
inner class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tv_seq = itemView.findViewById<TextView>(R.id.tv_seq)
private val tv_title = itemView.findViewById<TextView>(R.id.tv_title)
private val tv_content = itemView.findViewById<TextView>(R.id.tv_content)
private val tv_date = itemView.findViewById<TextView>(R.id.tv_date)
private val iv_delete = itemView.findViewById<ImageView>(R.id.iv_delete)
fun bind(todoModel: TodoModel) {
tv_seq.text = todoModel.seq.toString()
tv_title.text = todoModel.title
tv_content.text = todoModel.content
tv_date.text = todoModel.createDate.convertDateToString("yyyy.MM.dd HH:mm")
iv_delete.setOnClickListener {
deletetItemClick(todoModel)
}
}
}
}
/* createDate 을 Date to String */
fun Long.convertDateToString(format: String): String {
val simpleDateFormat = SimpleDateFormat(format)
return simpleDateFormat.format(Date(this))
}
5-4. item_list.xml
RecyclerView 에 들어가는 아이템 xml 입니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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:layout_margin="8dp"
app:cardCornerRadius="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="100">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginVertical="10dp"
android:layout_marginLeft="10dp"
android:layout_weight="85"
android:orientation="horizontal"
android:weightSum="100">
<!-- seq -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="15"
android:gravity="top"
android:orientation="vertical"
android:weightSum="100">
<TextView
android:id="@+id/tv_seq"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="6dp"
android:layout_weight="45"
android:background="@drawable/bg_round"
android:elevation="4dp"
android:gravity="center"
android:text="12"
android:textColor="@color/black"
android:textSize="12sp" />
</LinearLayout>
<!-- title, content, date -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="85"
android:orientation="vertical"
android:paddingLeft="10dp"
android:weightSum="100">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="44"
android:text="myTodo Title"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:layout_weight="30"
android:text="myTodo content"
android:textColor="@color/dark_gray_1"
android:textSize="15sp" />
<TextView
android:id="@+id/tv_date"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="26"
android:text="2021. 08. 03"
android:textColor="@color/dark_gray_2"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<!-- delete btn -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="15"
android:gravity="center"
android:orientation="vertical"
android:weightSum="100">
<ImageView
android:id="@+id/iv_delete"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="50"
android:src="@drawable/ic_baseline_delete_forever_24" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
iv_delete 의 src 이미지는 벡터(Vector) 이미지를 사용하였습니다. 아래 포스팅을 참고해주세요!
https://eunoia3jy.tistory.com/122
5-5. dialog_add.xml
메인화면에서 todo 리스트를 추가하기 위해 추가버튼 클릭 시 표시되는 dialog 의 View 입니다.
<?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:padding="16dp">
<EditText
android:id="@+id/et_add_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="제목을 입력하세요. "
android:textSize="15dp"
android:textColor="#000000"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_add_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="내용을 입력하세요. "
android:textSize="15dp"
android:textColor="#000000"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_add_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
결과 화면
감사합니당~!
'📱 안드로이드 Android ~ Kotlin' 카테고리의 다른 글
[안드로이드/Android] 클린아키텍처 Clean Architecture (0) | 2021.09.09 |
---|---|
[Android/Firebase] Firebase 프로젝트 생성 및 앱 추가 (0) | 2021.09.05 |
[Android/Kotiln] 데이터 바인딩 dataBinding 사용하기 (0) | 2021.09.02 |
[Android/Kotiln] MVVM 적용한 날씨 앱 만들기 (viewBinding, retrofit 사용) (0) | 2021.08.30 |
안드로이드 아키텍쳐 컴포넌트 AAC (Android Architecture Components) (0) | 2021.08.06 |