[안드로이드/Android/Kotlin] TodoList 만들기 : 1편(MVVM/ROOM/ViewPager2 사용)

SooYeon Yeon·2022년 2월 24일
0

안드로이드/Android

목록 보기
20/25

목표

  • TodoList 만들기
  • 시작 전 Fragment: 할 일 CREATE 및 READ, 체크박스 클릭 시 진행 중으로 이동
  • 진행 중 Fragment: 진행 중인 할 일 READ, 체크박스 클릭 시 완료로 이동
  • 완료 Fragment: 완료 된 일 READ (취소선), 휴지통박스 클릭 시 DELETE
  • 시작 전, 진행 중, 완료는 ViewPager2로 이동 가능


사용 기술

  • Recyclerview
  • ViewPager2
  • mvvm databinding
  • Room database

1편에서는

  • MainActivity 구상
  • 3가지 Fragment 구상
  • recyclerview xml 구상
  • Adapter 구상

build.gralde 준비

apply plugin: 'kotlin-kapt'

android {
		buildFeatures {
        viewBinding true
    }
    dataBinding {
        enabled = true
    }
defaultConfig {
// 벡터 이미지 사용
vectorDrawables.useSupportLibrary = true
		}
}

dependencies {
	// room 사용
  implementation("androidx.room:room-runtime:2.3.0")
  annotationProcessor("androidx.room:room-compiler:2.3.0")
  kapt("androidx.room:room-compiler:2.3.0")
	implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
	kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val pagerAdapter = PagerAdapter(this)
        val tabName = listOf("시작 전", "진행 중", "완료")

        binding.viewpager.adapter = pagerAdapter
        TabLayoutMediator(binding.tabs, binding.viewpager) { tab, position ->
            tab.text = "${(tabName[position])}"
        }.attach()
    }

    private inner class PagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
        override fun getItemCount(): Int = 3
        override fun createFragment(position: Int): Fragment {
            return when (position) {
                0 -> BeforeFragment()
                1 -> MiddleFragment()
                else -> AfterFragment()
            }
        }
    }
}

DataBinding을 사용한다.

PagerAdapter을 이용해 시작 전 - 진행 중 - 완료 를 구성한다.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<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=".MainActivity">
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabs"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:elevation="0dp"/>
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/tabs"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

BeforeFragment, MiddleFragment, AfterFragment 구상

먼저 BaseFragment를 구성한다. 세 Fragment에 공통되는 부분. (생략 후 각각 Fragment()상속 해도 됨)

BaseFragment.kt

abstract class BaseFragment : Fragment() {
    //abstract val layoutResourceId: Int
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        Log.d("BASEFRAGMENT","onviewcreated")
        super.onViewCreated(view, savedInstanceState)
    }
}

BeforeFragment.kt

class BeforeFragment : BaseFragment() {
    private lateinit var binding: FragmentBeforeBinding
    private lateinit var mTodoViewModel: TodoViewModel
    private lateinit var mTodoAdapter: TodoAdapter
    private lateinit var todoText: String

    @Override
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_before, container, false)
        initRecyclerView(binding.recyclerviewBefore)
        initViewModel()
        binding.todoViewModel = mTodoViewModel
        binding.lifecycleOwner = this

        // ADD 버튼 클릭 시 데이터 추가
        mTodoViewModel.addBtnClickEvent.observe(viewLifecycleOwner, Observer {
            todoText = mTodoViewModel.text.value.toString()
            mTodoViewModel.insertTodo(TodoModel(null, todoText, "BEFORE"))
            mTodoViewModel.deleteText()
            HideEditTextKeyBoard()
        })

        // 체크박스 클릭 시 진행중으로 데이터 이동
        mTodoAdapter.setOnItemClickListener(object : TodoAdapter.OnItemClickListener {
            override fun onItemClick(v: View, data: TodoModel, pos: Int) {
                mTodoViewModel.updateTodoBeforeToMiddle(data.id)
            }
        })
        return binding.root
    }

    private fun initViewModel() {
        mTodoViewModel =
            ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
                .create(TodoViewModel::class.java)
        mTodoViewModel.getTodoBeforeList().observe(viewLifecycleOwner, Observer {
            mTodoAdapter.setTodoItems(it)
        })
    }

    private fun initRecyclerView(rcv: RecyclerView) {
        mTodoAdapter = TodoAdapter()
        rcv.run {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(context)
            adapter = mTodoAdapter
        }
    }
		// 키보드 내려주는 함수
    private fun HideEditTextKeyBoard() {
        val imm =
            this.activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(binding.todoTextBefore.windowToken, 0)
    }
}

MiddleFragment.kt

class MiddleFragment : BaseFragment() {
    private lateinit var binding: FragmentMiddleBinding
    private lateinit var mTodoViewModel: TodoViewModel
    private lateinit var mTodoAdapter: TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_middle, container, false)
        initRecyclerView(binding.recyclerviewMiddle)
        initViewModel()

        // 체크박스 클릭 시 완료로 데이터 이동
        mTodoAdapter.setOnItemClickListener(object : TodoAdapter.OnItemClickListener {
            override fun onItemClick(v: View, data: TodoModel, pos: Int) {
                mTodoViewModel.updateTodoMiddleToAfter(data.id)
                Log.d("BUTTON", "checkbox")
            }
        })
        return binding.root
    }

    private fun initViewModel() {
        mTodoViewModel =
            ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
                .create(
                    TodoViewModel::class.java
                )
        mTodoViewModel.getTodoMiddleList().observe(viewLifecycleOwner, Observer {
            mTodoAdapter.setTodoItems(it)
        })
    }

    private fun initRecyclerView(rcv: RecyclerView) {
        mTodoAdapter = TodoAdapter()
        rcv.run {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(context)
            adapter = mTodoAdapter
        }
    }
}

AfterFragment.kt

class AfterFragment : BaseFragment() {
    private lateinit var binding: FragmentAfterBinding
    private lateinit var mTodoViewModel: TodoViewModel
    private lateinit var mTodoAdapter: TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_after, container, false)
        initRecyclerView(binding.recyclerviewAfter)
        initViewModel()

        // 체크박스 클릭 시 데이터 삭제
        mTodoAdapter.setOnItemClickListener(object : TodoAdapter.OnItemClickListener {
            override fun onItemClick(v: View, data: TodoModel, pos: Int) {
                mTodoViewModel.deleteTodo(data)
            }
        })
        return binding.root
    }

    private fun initViewModel() {
        mTodoViewModel =
            ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
                .create(
                    TodoViewModel::class.java
                )
        mTodoViewModel.getTodoAfterList().observe(viewLifecycleOwner, Observer {
            mTodoAdapter.setTodoItems(it)
        })
    }

    private fun initRecyclerView(rcv: RecyclerView) {
        mTodoAdapter = TodoAdapter()
        rcv.run {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(context)
            adapter = mTodoAdapter
        }
    }
}

TodoAdapter.kt

class TodoAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var todoItems: List<TodoModel> = listOf()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        Log.d("Adapter", "oncreateviewholder")
        val view = when (viewType){
            1 -> LayoutInflater.from(parent.context)
                .inflate(R.layout.item_recyclerview_after, parent, false)
            else -> LayoutInflater.from(parent.context)
                .inflate(R.layout.item_recyclerview, parent, false)
        }
        return TodoViewHolder(view)
    }

    override fun getItemViewType(position: Int): Int {
        val todoModel = todoItems[position]
        return if (todoModel.status == "AFTER") 1 else 0
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        Log.d("Adapter-onbind", todoItems.size.toString())
        val todoModel = todoItems[position]
        val todoViewHolder = holder as TodoViewHolder
        todoViewHolder.bind(todoModel)
    }

    override fun getItemCount(): Int = todoItems.size

    inner class TodoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val todo_text = itemView.findViewById<TextView>(R.id.todo_text)
        val imageButtonRectangle = itemView.findViewById<ImageButton>(R.id.imageButton_rectangle)
        fun bind(todoModel: TodoModel) {
            todo_text?.text = todoModel.description
            if (itemViewType==1){
                todo_text.paintFlags = todo_text.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
                todo_text.setTextColor(Color.parseColor("#808080"))
            }
            val pos = adapterPosition
            if (pos != RecyclerView.NO_POSITION) {
                imageButtonRectangle?.setOnClickListener {
                    listener?.onItemClick(itemView, todoModel, pos)
                }
            }
        }
    }

    interface OnItemClickListener {
        fun onItemClick(v: View, data: TodoModel, pos: Int)
    }

    private var listener: OnItemClickListener? = null
    fun setOnItemClickListener(listener: OnItemClickListener) {
        Log.d("Adapter", "setonitemclicklistener")
        this.listener = listener
    }

    fun setTodoItems(todoItems: List<TodoModel>) {
        this.todoItems = todoItems
        notifyDataSetChanged()
    }
}

fragment_before.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
    <variable
        name="todoViewModel"
        type="org.example.todolistapplication.viewmodel.TodoViewModel" />
    </data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <View
        android:id="@+id/borderView"
        android:layout_width="match_parent"
        android:layout_height="16dp"
        android:background="#FFFFFF"
        app:layout_constraintBottom_toTopOf="@+id/recyclerview_before"/>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview_before"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="@id/borderView"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
    <EditText
        android:id="@+id/todo_text_before"
        android:text="@={todoViewModel.text}"
        app:layout_constraintTop_toBottomOf="@id/recyclerview_before"
        android:background="@drawable/edit_text_background"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingHorizontal="24dp"
        android:paddingVertical="16dp"/>

    <ImageButton
        android:id="@+id/imageButton_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:background="@android:color/transparent"
        android:onClick="@{() -> todoViewModel.onClickAddButton()}"
        app:layout_constraintBottom_toBottomOf="@id/todo_text_before"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@id/todo_text_before"
        app:srcCompat="@drawable/ic_baseline_add_24" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

fragment_middle.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <View
        android:id="@+id/borderView"
        android:layout_width="match_parent"
        android:layout_height="16dp"
        android:background="#FFFFFF"
        app:layout_constraintBottom_toTopOf="@+id/recyclerview_middle"/>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview_middle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="@id/borderView"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

fragment_after.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview_after"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

item_recyclerview.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/todo_text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp"
        android:paddingRight="30dp"
        android:paddingVertical="16dp"
        android:background="@drawable/edit_text_background"/>

    <ImageButton
        android:id="@+id/imageButton_rectangle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        app:layout_constraintBottom_toBottomOf="@id/todo_text"
        app:layout_constraintEnd_toEndOf="@id/todo_text"
        app:layout_constraintTop_toTopOf="@id/todo_text"
        app:srcCompat="@drawable/ic_baseline_check_box_outline_blank_24"
        android:background="@android:color/transparent"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:background="#FFFFFF"
        app:layout_constraintTop_toBottomOf="@+id/todo_text"/>

</androidx.constraintlayout.widget.ConstraintLayout>

item_recyclerview_after.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <View
        android:id="@+id/borderView"
        android:layout_width="match_parent"
        android:layout_height="16dp"
        android:background="#FFFFFF"
        app:layout_constraintBottom_toTopOf="@+id/todo_text"/>
    <TextView
        android:id="@+id/todo_text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/borderView"
        android:background="@drawable/edit_text_background"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp"
        android:paddingRight="30dp"
        android:paddingVertical="16dp"/>

    <ImageButton
        android:id="@+id/imageButton_rectangle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        app:layout_constraintBottom_toBottomOf="@id/todo_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@id/todo_text"
        app:srcCompat="@drawable/ic_baseline_delete_24"
        android:background="@android:color/transparent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

이후 2편에서

  • Room Database 사용 및 Model
  • Repository
  • ViewModel

0개의 댓글