본문 바로가기
Android Project/Extras..

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

by juwon2 2023. 12. 6.

 

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

 

<?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앱이다!

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

 

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