본문 바로가기

Android Project

[Android/Kotlin] 간단한 Todo List 앱만들기 - 2. 메인화면

 

이번에는 할일을 작성하고 수정,삭제까지 가능한 메인화면을 만들어봤다

 

<?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_gravity="center"
    tools:context=".activity.MainActivity">


    <ImageView
        android:id="@+id/imageView"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginEnd="10dp"
        app:layout_constraintBottom_toBottomOf="@+id/textView2"
        app:layout_constraintEnd_toStartOf="@+id/textView2"
        app:layout_constraintTop_toTopOf="@+id/textView2"
        app:srcCompat="@drawable/splashimg" />


    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:layout_marginEnd="140dp"
        android:fontFamily="@font/yeongdeok"
        android:text="To-Do List"
        android:textColor="@color/black"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="5dp"
        android:layout_marginTop="16dp"
        android:fontFamily="@font/yeongdeok"
        android:text="매일 할일을 기록해요!"
        android:textColor="@color/black"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="@+id/textView2"
        app:layout_constraintHorizontal_bias="0.56"
        app:layout_constraintStart_toStartOf="@+id/imageView"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview_todo"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:listitem="@layout/list_item_todo"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/btn_write"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="32dp"
        android:layout_marginBottom="24dp"
        android:backgroundTint="#F8BBD0"
        android:clickable="true"
        android:elevation="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/img_write" />


</androidx.constraintlayout.widget.ConstraintLayout>

activity_main.xml

 

리사이클러뷰를 사용해서 일정을 작성하고 수정,삭제할수있도록 디자인했고,

FloatingActionButton을 눌러서 일정을 작성할수있도록 디자인 했다

activity_main.xml

 

 

package com.example.todolist.model

import androidx.room.Entity
import androidx.room.PrimaryKey


@Entity
class TodoInfo {
    //데이터를 집어넣음(각각의 요소를 집어넣음)
    //리사이클러뷰 아이템 하나에 들어갈 데이터
    var todoContent : String = ""   // 내용
    var todoDate : String = ""      // 날짜


    @PrimaryKey(autoGenerate = true)        // PrimaryKey 생성 (기본키)
    var id : Int = 0                        // id라는 변수는 Int값을 가지고, 초기값은 0
}

TodoInfo.kt

 

여기에는 리사이클러뷰에 들어갈 데이터들을 집어넣었다

내용,날짜 데이터가 들어갈 수 있는 모델을 만들었음

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:orientation="horizontal">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">


    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="@font/yeongdeok"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="@color/black"
        android:text="내용"/>

    <TextView
        android:id="@+id/tv_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="@font/yeongdeok"
        android:textSize="16sp"
        android:textColor="#7C7C7C"
        android:text="2023-12-6"/>


    </LinearLayout>

    <ImageView
        android:id="@+id/btn_delete"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@drawable/img_delete"/>



</LinearLayout>

list_item_todo.xml

 

TodoInfo.kt에 만들어놓은 데이터들을 기반으로 리사이클러뷰 아이템 화면을 디자인했다. 

 

list_item_todo.xml

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">




    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:counterEnabled = "true"
        app:counterMaxLength="50">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/et_memo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fontFamily="@font/yeongdeok"
            android:layout_margin="16dp"
            android:singleLine="true"
            android:maxLines="1"
            android:maxLength="50"
            android:hint="할일을 기록해보세요!"/>


    </com.google.android.material.textfield.TextInputLayout>





</LinearLayout>

dialog_edit.xml

 

FloatingActionButton을 눌렀을때 나오는 작성버튼과 내용을 클릭하면 나오는 수정버튼의 Dialog를 표현하기위해 디자인화면을 만들었다

dialog_edit.xml

 

 

 

 

근데 지금까지 한건 실제 데이터가 아니라 UI상으로만 보이게 해놓은것이기 때문에 recyclerview adapter를 만들어서 동작구현을 해야한다

 

 

//리사이클러뷰 어뎁터를 상속 받아야지 데이터연동을 할수있음

class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {

    private var lstTodo : ArrayList<TodoInfo> = ArrayList()   //TodoInfo라는 데이터를 가진 배열리스트형태를 lasTodo변수에 담겠다
    private lateinit var roomDatabase: TodoDatabase


//    // init = 클래스가 생설될때 가징먼저 호출되는것
//    init {
//        // 샘플 리스트 아이템 인스턴스 생성(임의로 만든거) (room DB연동 후에는 삭제함)
//        val todoItem = TodoInfo()
//        todoItem.todoContent = "To Do List앱 만들기!"
//        todoItem.todoDate = "2023-06-01"
//        lstTodo.add(todoItem)
//
//        // 샘플 리스트 아이템 인스턴스 생성
//        val todoItem2 = TodoInfo()
//        todoItem2.todoContent = "블로그 작성하기"
//        todoItem2.todoDate = "2023-06-01"
//        lstTodo.add(todoItem2)
//
//        // 샘플 리스트 아이템 인스턴스 생성
//        val todoItem3 = TodoInfo()
//        todoItem3.todoContent = "기말 공부하기!"
//        todoItem3.todoDate = "2023-06-01"
//        lstTodo.add(todoItem3)
//
//    }


    fun addListItem(todoItem: TodoInfo) {
        // 4,0,1,2,3
        lstTodo.add(0, todoItem)    //최신꺼가 젤 처음나오게 하려면 0추가 (아이템 추가될때마다 최신순서로 배치)
    }


    inner class TodoViewHolder(private val binding: ListItemTodoBinding) : RecyclerView.ViewHolder(binding.root) { // binding.root에 있는 binding은 앞쪽에 선언했던 변수임
    // viewHolder = 각 리스트 아이템들을 보관하는 객체
        fun bind(todoItem : TodoInfo) {
            // 리스트 뷰 데이터를 UI에 연동
            binding.tvContent.setText(todoItem.todoContent)
            binding.tvDate.setText(todoItem.todoDate)


            // 삭제 버튼 클릭했을때
            binding.btnDelete.setOnClickListener {
                // 삭제 버튼 클릭 시 내부 로직 구현

                //다이얼로그 띄우기
                AlertDialog.Builder(binding.root.context)
//                    .setTitle("삭제")
                    .setMessage("정말 삭제하시겠습니까?")
                    .setPositiveButton("네",DialogInterface.OnClickListener { dialog, which ->

                        CoroutineScope(Dispatchers.IO).launch {
                            // db에도 적용해주기
                            val innerLstTodo = roomDatabase.todoDao().getAllReadData()     //현재 db에 저장되어있는 데이터를 모두 가지고와서 arraylist형태로 만듬
                            for (item in innerLstTodo) {
                                if (item.todoContent == todoItem.todoContent && item.todoDate == todoItem.todoDate){
                                    // database item 삭제
                                    roomDatabase.todoDao().deleteTodoDate(item)
                                }
                            }

                            // ui 삭제
                            lstTodo.remove(todoItem)    //lstTodo 배열에 담겨있는 todoItem 데이터들이 삭제
                            (binding.root.context as Activity).runOnUiThread {
                                notifyDataSetChanged()
                                Toast.makeText(binding.root.context,"삭제되었습니다", Toast.LENGTH_SHORT).show()
                            }
                        }

                    })

                    .setNegativeButton("아니오",DialogInterface.OnClickListener { dialog, which ->

                    })
                    .show()
            }


        // 리스트 수정
        binding.root.setOnClickListener {     // binding.root는 list_item_todo.xml에 있는 아이템을 나타냄
            val bindingDialog = DialogEditBinding.inflate(LayoutInflater.from(binding.root.context), binding.root, false)
//            // 기존에 작성한 데이터 보여주기
//            bindingDialog.etMemo.setText(todoItem.todoContent)

            AlertDialog.Builder(binding.root.context)
                .setTitle("수정하기")                 //팝업제목
                .setView(bindingDialog.root)        //bindingDialog를 넣는것
                .setPositiveButton("수정완료", DialogInterface.OnClickListener { dialog, which ->
                    CoroutineScope(Dispatchers.IO).launch {
                        // db에도 적용해주기
                        val innerLstTodo = roomDatabase.todoDao().getAllReadData()     //현재 db에 저장되어있는 데이터를 모두 가지고와서 arraylist형태로 만듬
                        for (item in innerLstTodo) {
                            if (item.todoContent == todoItem.todoContent && item.todoDate == todoItem.todoDate){
                                // database item 수정
                                item.todoContent =  bindingDialog.etMemo.text.toString()
                                item.todoDate = SimpleDateFormat("yyyy-MM-dd").format(Date())
                                roomDatabase.todoDao().updateTodoDate(item)
                            }
                        }

                        // 작성완료버튼 눌렀을때 동작
                        // ui 수정
                        todoItem.todoContent =  bindingDialog.etMemo.text.toString()                // bindingDialog.etMemo.text.toString() 를 사용해서 사용자가 입력한 값으로 가져오기
                        todoItem.todoDate = SimpleDateFormat("yyyy-MM-dd").format(Date())    //최신의 date값 넣어줌 (몇분몇초인지 HH:mm:ss)

                        // 리스트 수정 (array list 수정)
                        lstTodo.set(adapterPosition, todoItem)

                        (binding.root.context as Activity).runOnUiThread {
                            notifyDataSetChanged()
                        }
                    }

                })
                .setNegativeButton("취소", DialogInterface.OnClickListener { dialog, which ->
                    //취소버튼 눌렀을때 동작
                })
                .show() //마지막에 이걸 적어줘야 정상적으로 작동
        }

        }

    }




    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        // viewHolder가 만들어질때
        // 각 리스트 아이템이 1개씩 구성될때마다 이 오버라이드 메소드가 호출됨
        val binding = ListItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        // roomDatabase 초기화
        roomDatabase = TodoDatabase.getInstance(binding.root.context)!!

        return TodoViewHolder(binding)
    }

    override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
        // viewHolder가 연결될때(바인딩될때)
        holder.bind(lstTodo[position])
    }

    override fun getItemCount(): Int {
        // 리스트 총 개수
        return lstTodo.size
    }

}

TodoAdapter.kt

 

여기서 내부 DB인 room databse까지 연결해주었다. room database를 연결해주지 않으면 앱을 나갔다들어왔을때 여태까지 한 결과가 반영되지않는다. 따라서 추가,수정,삭제부분에 database를 연결해주었다

room database는 내부데이터라서 안드로이드 스튜디오 내부에서 쓰일수있어서 간편하게 쓸수있는 database이다

 

내용을 누르면 Dialog가 나와서 수정할수있고, 삭제 이미지를 누르면 삭제가 되도록 구현했다

 

 

 

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding   //binding 준비
    private lateinit var todoAdapter: TodoAdapter       //adapter 준비
    private lateinit var roomDatabase: TodoDatabase


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate((layoutInflater)) //실제 activity_main.xml에 있는 리사이클러뷰를 연동
        setContentView(binding.root)


        // 어뎁터 생성
        todoAdapter = TodoAdapter()

        // 리사이클러뷰에 어뎁터 세팅
        binding.recyclerviewTodo.adapter = todoAdapter      //어뎁터를 이미 만들어놨기 때문에 연결바로가능

        // roomDatabase 초기화
        roomDatabase = TodoDatabase.getInstance(applicationContext)!!

        // 전체 데이터 load(가져옴) (코루틴 사용 - 비동기처리[순서상관없이 위에서부터 순차적으로 실행되는것])
        CoroutineScope(Dispatchers.IO).launch {
            val lstTodo = roomDatabase.todoDao().getAllReadData() as ArrayList<TodoInfo> //전체데이터 가져옴
            //어뎁터쪽에 데이터 전달
            for (todoItem in lstTodo) {
                todoAdapter.addListItem(todoItem)
            }
            // UI Thread에서 처리
            runOnUiThread {
                todoAdapter.notifyDataSetChanged()
            }
        }





        // 플로팅 작성하기 버튼 클릭했을때 (AlerDialg로 팝업창 만들기)
        binding.btnWrite.setOnClickListener {
            //val binding = ListItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)  이해 참고용
            val bindingDialog = DialogEditBinding.inflate(LayoutInflater.from(binding.root.context), binding.root, false)

            AlertDialog.Builder(this)
                .setTitle("할일 기록하기")            //팝업제목
                .setView(bindingDialog.root)        //bindingDialog를 넣는것
                .setPositiveButton("작성완료", DialogInterface.OnClickListener { dialog, which ->
                    // 작성완료버튼 눌렀을때 동작
                    val todoItem = TodoInfo()   //todoItem에 사용자가 입력한값 넣음
                    todoItem.todoContent =  bindingDialog.etMemo.text.toString()                // bindingDialog.etMemo.text.toString() 를 사용해서 사용자가 입력한 값으로 가져오기
                    todoItem.todoDate = SimpleDateFormat("yyyy-MM-dd").format(Date())    //최신의 date값 넣어줌 (몇분몇초인지 HH:mm:ss)
                    todoAdapter.addListItem(todoItem)   // adepter쪽으로 리스트아이템이 전달

                    CoroutineScope(Dispatchers.IO).launch {     //코루틴 사용
                        roomDatabase.todoDao().insertTodoData(todoItem) // 데이터베이스에도 클래스데이터 삽입
                        runOnUiThread {
                            todoAdapter.notifyDataSetChanged()  // 리스트 새로고침
                        }
                    }
                })

                .setNegativeButton("취소", DialogInterface.OnClickListener { dialog, which ->
                    //취소버튼 눌렀을때 동작
                })
                .show() //마지막에 이걸 적어줘야 정상적으로 작동


        }
    }
}

 

MainActiviy.kt

 

 

 

//Room database의 기본틀
@Database(entities = [TodoInfo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun todoDao() : TodoDao


    //companion object를 만들어서 getInstance라는 메소드만 호출하면 바로 불러올수있도록
    companion object {
        private var instance : TodoDatabase? = null
        @Synchronized
        fun getInstance(context : Context) : TodoDatabase? {
            if (instance == null) {
                synchronized((TodoDatabase :: class)) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        TodoDatabase :: class.java,
                        "todo-database"
                    ).build()
                }
            }
            return instance
        }
    }

}

todoDatabase.kt

 

 

 

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.example.todolist.model.TodoInfo


// 데이터베이스에 접근할수있는 객체를 만듬(Dao) - CRUD
@Dao
interface TodoDao {

    // database table에 삽입(추가)
    @Insert
    fun insertTodoData(todoInfo: TodoInfo)

    // database table에 기존에 존재하는 데이터를 수정
    @Update
    fun updateTodoDate(todoInfo: TodoInfo)

    // database table에 기존에 존재하는 데이터를 삭제
    @Delete
    fun deleteTodoDate(todoInfo: TodoInfo)

    // database table에 전체 데이터를 가져옴 (read-조회)
    @Query("SELECT * FROM TodoInfo ORDER BY todoDate") // 전체데이터 조회한다 TodoInfo테이블로부터
    fun getAllReadData(): List<TodoInfo>


}

TodoDao.kt

 

database를 사용해서 실제 데이터들을 추가,수정,삭제 할수있도록했다

 

 

 

 

 

To do list

 

최종적으로 구현된 To do list앱이다!

작성,수정,삭제가 가능하고 앱을 나갔다가 들어와도 기존에 작성했던 데이터들이 그대로 남아있다!

 

아직까지 완벽하게 이해가 된것같지는 않아서 코드 많이 쳐보고 눈에 익혀야겠다!