Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Team-01][Android] Todo 화면 및 레포지토리 구현 #151

Open
wants to merge 11 commits into
base: team-01
Choose a base branch
from
32 changes: 27 additions & 5 deletions Android/app/src/main/java/com/example/todo_list/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todo_list.data.TasksRepository
import com.example.todo_list.databinding.ActivityMainBinding
import com.example.todo_list.history.HistoryAdapter
import com.example.todo_list.history.HistoryViewModel
import com.example.todo_list.tasks.data.Task
import com.google.android.material.navigation.NavigationView

class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
Expand All @@ -24,17 +24,17 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte

historyViewModel = ViewModelProvider(this, ViewModelFactory(TasksRepository())).get(HistoryViewModel::class.java)

val adapter = HistoryAdapter()
binding.recyclerviewHistory.adapter = adapter
binding.recyclerviewHistory.layoutManager = LinearLayoutManager(this)
val historyAdapter = HistoryAdapter()
binding.recyclerviewHistory.adapter = historyAdapter
binding.btnMenu.setOnClickListener {
binding.mainLayout.openDrawer(GravityCompat.END)
historyViewModel.getHistories()
}

binding.btnClose.setOnClickListener { binding.mainLayout.closeDrawer(GravityCompat.END) }
binding.naviView.setNavigationItemSelectedListener(this)

historyViewModel.historyList.observe(this) { adapter.submitList(it) }
historyViewModel.historyList.observe(this) { historyAdapter.submitList(it) }
historyViewModel.checkLoading.observe(this) {
if (it) {
binding.spinner.visibility = View.VISIBLE
Expand All @@ -44,6 +44,28 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding.recyclerviewHistory.visibility = View.VISIBLE
}
}

val task1 = Task(
1,
"테스트하기1",
"콘텐츠테스트1",
"jung",
"doing",
"2022-04-06T15:30:00.000+09:00",
"2022-04-06T15:30:00.000+09:00"
)

val task2 = Task(
2,
"테스트하기2",
"콘텐츠테스트2",
"park",
"todo",
"2022-04-06T15:30:00.000+09:00",
"2022-04-06T15:30:00.000+09:00"
)

binding.todoTodoView.addTasks(listOf(task1, task2))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요런 기능은 별도의 함수로 묶으면 보기에 조금 더 편해서 나중에 좋은 점이 있습니다. 혹은 task 가 바뀌게 되더라도 별도의 함수로 묶어 놓으면 바꿔야 할 부분이 명확하게 보일거 같습니다. 분리된 메소드의 위치는 activity/viewmodel 두 군데가 있을텐데 어느 부분이 더 일관적일지 보셔서 추가하시면 좋을거같네요~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ViewModel을 앱 전체에서 사용할 수 있도록 파일명 및 위치를 변경했습니다.
ViewModel에서 함수로 묶어 사용하는 것이 더 나을 것 같다고 판단해서 ViewModel에 구현했습니다.
감사합니다!

}

override fun onNavigationItemSelected(item: MenuItem): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.example.todo_list.tasks

import android.graphics.Canvas
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.todo_list.R
import kotlin.math.min

class ItemTouchHelperCallback(val listener: ItemTouchHelperListener) : ItemTouchHelper.Callback() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스가 왜 필요했는지에 대한 주석이 있어도 좋습니다. 리사이클러뷰를 그냥 사용하지 않고 추가 기능을 넣은 이유라던가, 이게 없으면 무엇이 동작하지 않는다던가 등의 설명정도면 어떨까요? 혹은, 클래스 이름 자체를 어떤 역할을 하는것인지 구체적으로 적어봐도 좋겠네요.


private var clamp = 0f
private var currentDx = 0f

override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
Log.d("TAGcode", "${ItemTouchHelper.LEFT}")
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}

override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return listener.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
Log.d("TAGonSwiped1", "$direction")
Log.d("TAGonSwiped2", "${viewHolder.adapterPosition}") //<< 몇번째 목록을 조작했는지
// direction = 방향 >> 왼쪽으로 스와이프했을때 작동
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

direction 에 대한 주석 좋아보입니다! 조금 자세하게 적으면 direction 범위가 무엇일 때 방향이 어딘지 적어볼 수도 있겠네요.

listener.onItemSwipe(viewHolder.adapterPosition)

}

// 터치,스와이프등 뷰에 변화가 생길겨여우
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val view = getView(viewHolder)
val isClamped = getTag(viewHolder)
val newX = clampViewPositionHorizontal(
dX,
isClamped,
isCurrentlyActive
) // newX 만큼 이동(고정 시 이동 위치/고정 해제 시 이동 위치 결정)

// 고정시킬 시 애니메이션 추가
if (newX == -clamp) {
getView(viewHolder).animate().translationX(-clamp).setDuration(100L).start()
return
}

currentDx = newX
getDefaultUIUtil().onDraw(
c,
recyclerView,
view,
newX,
dY,
actionState,
isCurrentlyActive
)
}
}

fun setClamp(clamp: Float) { this.clamp = clamp }
private fun getView(viewHolder: RecyclerView.ViewHolder) : View = viewHolder.itemView.findViewById(R.id.todo_item)
private fun getTag(viewHolder: RecyclerView.ViewHolder) : Boolean = viewHolder.itemView.tag as? Boolean ?: false

private fun clampViewPositionHorizontal(
dX: Float,
isClamped: Boolean,
isCurrentlyActive: Boolean
) : Float {
// RIGHT 방향으로 swipe 막기
val max = 0f

// 고정할 수 있으면
val newX = if (isClamped) {
// 현재 swipe 중이면 swipe되는 영역 제한
if (isCurrentlyActive)
// 오른쪽 swipe일 때
if (dX < 0) dX/3 - clamp
// 왼쪽 swipe일 때
else dX - clamp
// swipe 중이 아니면 고정시키기
else -clamp
}
// 고정할 수 없으면 newX는 스와이프한 만큼
else dX / 2

// newX가 0보다 작은지 확인
return min(newX, max)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.todo_list.tasks

interface ItemTouchHelperListener {
fun onItemMove(from_position: Int, to_position: Int): Boolean
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이쪽 변수 이름도 되도록이면 컨벤션에 맞게 부탁드립니다!

fun onItemSwipe(position: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.todo_list.tasks

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.todo_list.R
import com.example.todo_list.databinding.TodoItemBinding
import com.example.todo_list.tasks.data.Task

class TodoAdapter : ListAdapter<Task, TodoAdapter.TodoViewHolder>(diffUtil),
ItemTouchHelperListener {
inner class TodoViewHolder(private val binding: TodoItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(task: Task) {
binding.task = task
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DataBindingUtil.inflate<TodoItemBinding>(
LayoutInflater.from(parent.context),
R.layout.todo_item,
parent,
false
).let {
TodoViewHolder(it)
}

override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}

override fun onItemMove(from_position: Int, to_position: Int): Boolean {
return true
}

override fun onItemSwipe(position: Int) {
}
}

private val diffUtil = object : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem == newItem
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.todo_list.tasks

import android.widget.TextView
import androidx.databinding.BindingAdapter

@BindingAdapter("setTitle")
fun setTitle(view: TextView, title: String) {
view.text = title
}

@BindingAdapter("setContent")
fun setContent(view: TextView, content: String) {
view.text = content
}

@BindingAdapter("setUser")
fun setUser(view: TextView, user: String) {
view.text = user
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 값들이 String이 주입되는 형태인데, BindingAdapter을 새로 사용하신 이유가 있으실까요?
(android:text) 를 사용하지 않고)

65 changes: 65 additions & 0 deletions Android/app/src/main/java/com/example/todo_list/tasks/TasksView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.todo_list.tasks

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageButton
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.todo_list.R
import com.example.todo_list.tasks.data.Task

class TasksView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {

private lateinit var tvTitle: TextView
private lateinit var tvBadgeCount: TextView
private lateinit var btnAddTask: ImageButton
private lateinit var recyclerViewTodo: RecyclerView
private val todosAdapter = TodoAdapter()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 코드처럼 뷰 변수를 따로 선언하고 커스텀뷰 구성을 할 수도 있으나, 요즘은 ViewBinding 을 이용하여 이런 코드들을 간단하게 만드는 편입니다. 즉 Activity/Fragment 등이 아닌 커스텀 뷰 에서도 바인딩을 사용 할 수 있습니다.

사용하는게 어렵지 않고 꽤 많은 코드를 줄일 수 있는데, 학습이라 생각하고 ViewBinding 을 사용하도록 구현해보는것을 추천드립니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

binding을 이용해 뷰를 참조하도록 수정했습니다.
감사합니다!

init {
initViews()
initAttributes(attrs)
}

private fun initAttributes(attrs: AttributeSet?) {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.TodoView,
0,
0).apply {
try {
tvTitle.text = getString(R.styleable.TodoView_title)
tvBadgeCount.text = getString(R.styleable.TodoView_badge_count)
} finally {
recycle()
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 뷰 요소를 만드는 기본적인 방법입니다!

추가로, 위에 말씀드린 뷰바인딩/데이터 바인딩을 이용한 방법으로 구현하면 이런 필드 또한 별도 Styleable 로 선언하지 않고 사용 할 수도 있습니다. binding 레이아웃에서 사용하는 뷰는 해당 커스텀 뷰의 필드를 참조하여 xml 에서 설정 가능한 어트리뷰트를 자동으로 만들어줍니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 머리로는 이해가 되는데 코드가 안따라줍니다...🥲 좀 더 고민해보고 적용해보도록 하겠습니다!
감사합니다 !


private fun initViews() {
val layoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE
) as LayoutInflater

addView(
layoutInflater.inflate(R.layout.tasks_view, this, false)
)

tvTitle = findViewById(R.id.todo_title)
tvBadgeCount = findViewById(R.id.todo_badge)
btnAddTask = findViewById(R.id.btn_task_add)
recyclerViewTodo = findViewById(R.id.recyclerview_todo)

recyclerViewTodo.adapter = todosAdapter
recyclerViewTodo.setHasFixedSize(true)
val touchHelper = ItemTouchHelperCallback(todosAdapter)
val helper = ItemTouchHelper(touchHelper)
helper.attachToRecyclerView(recyclerViewTodo)
}

fun addTasks(tasks: List<Task>) {
todosAdapter.submitList(tasks)
}
}
5 changes: 5 additions & 0 deletions Android/app/src/main/res/drawable/ic_baseline_add_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#BAB8B8"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
7 changes: 7 additions & 0 deletions Android/app/src/main/res/drawable/todo_badge_bg.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">

<solid android:color="@color/number_badge_bg" />

</shape>
Loading