안드로이드 : diary – RecyclerView 포스트에서 일기 데이터를 내려받아 리스트로 보여주는 방법을 알아보았어.
이번에는 특정 날짜의 일기 항목을 클릭해서 일기 데이터 편집 화면으로 전환하는 방법을 구현해보려고 해.
그러기 위해서는 ViewHolder 를 클릭할 수 있는 방법을 구현해야 하거든.
어댑터 클래스 DiaryAdapter 에 아래와 같이 인터페이스와 리스너를 선언해주자.
DiaryAdapter.kt
class DiaryAdapter(private val diaries: List<Diary>) : RecyclerView.Adapter<DiaryAdapter.DiaryItemViewHolder>() {
interface ItemClickListner {
fun onItemClick(view: View, pos: Int)
}
private var itemClickListner: ItemClickListner? = null
fun setItemClickListener(listner: ItemClickListner) {
itemClickListner = listner
}
...
뷰홀더에 대해서 클릭리스너를 등록하고, itemClickListener 가 설정되어 있을 경우에 onItemClick 이 호출되게 하자.
DiaryAdapter.kt
...
override fun onBindViewHolder(holder: DiaryItemViewHolder, position: Int) {
holder.tvdate.text = diaries[position].date
holder.tvweekname.text = ""
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
dateFormat.parse(diaries[position].date)?.let {
val cal = Calendar.getInstance()
cal.time = it
val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
val weekNames = arrayListOf("일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일")
holder.tvweekname.text = weekNames[dayOfWeek - 1]
}
holder.tvcontent.text = diaries[position].content
holder.itemView.setOnClickListener {
itemClickListner?.onItemClick(it, position)
}
}
}
이제 어댑터에 ItemClickListener 를 설정해주자.
ListFragment.kt
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
val adapter: DiaryAdapter = recycler.adapter as DiaryAdapter
adapter.setItemClickListener(object: DiaryAdapter.ItemClickListner {
override fun onItemClick(view: View, pos: Int) {
findNavController().navigate(
R.id.action_listFragment_to_editFragment
, bundleOf("id" to diaries[pos].id)
)
}
})
return v
}
...
navigate 함수로 일기데이터의 고유번호를 전달하고 있는데, EditFragment 에서는 이 값을 받아서 처리해야겠지?
EditFragment.kt
...
class EditFragment : Fragment() {
// // TODO: Rename and change types of parameters
// private var param1: String? = null
// private var param2: String? = null
private var id: Long? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
// param1 = it.getString(ARG_PARAM1)
// param2 = it.getString(ARG_PARAM2)
id = it.getLong("id")
}
}
...
안드로이드 스튜디오가 샘플로 작성해 준 파라미터 관련 코드는 주석처리하고 getLong 으로 “id” 파라미터를 구하는 코드를 추가했어.
이제 fragment_edit.xml 에 일기 데이터를 표시하기 위한 UI 컴포넌트를 배치하고 일기 데이터를 읽어오는, 그리고 저장하는 RestAPI 호출 기능을 구현하면 되겠네.
fragment_edit.xml 의 UI 레이아웃은 아래와 같이 구성했어.
이번에는 일기 데이터를 가져오기 위한 /diary/{id} URL 의 RestAPI 호출 코드를 작성해보자.
DiaryService 에 아래 함수를 작성해줄께.
DiaryService.kt
interface DiaryService {
...
@GET("/diary/{id}")
fun getDiary(
@Header("Authorization") token: String
, @Path("id") id: Long
): Call<Diary>
}
EditFragment 클래스에는 위 함수를 사용하는 코드를 다음과 같이 작성해주면 돼.
EditFragment.kt
class EditFragment : Fragment() {
private var id: Long? = null
private lateinit var mainActivity: MainActivity
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://diary.woohahaapps.com:8080")
.addConverterFactory(GsonConverterFactory.create())
.build()
...
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity = context as MainActivity
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val v: View = inflater.inflate(R.layout.fragment_edit, container, false)
val buttonSaveDiary: Button = v.findViewById(R.id.button_save)
buttonSaveDiary.setOnClickListener {
findNavController().navigate(R.id.action_editFragment_to_listFragment)
}
val diaryService: DiaryService = retrofit.create(DiaryService::class.java)
id?.let {
val token = "Bearer=" + mainActivity.getPref("TOKEN", "")
diaryService.getDiary(token, id!!)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>,
response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful.not()) {
Log.d("ERROR", "CODE=" + response.code().toString())
}
response.body()?.let {
val editDate: EditText = v.findViewById(R.id.edit_date)
val editContent: EditText = v.findViewById(R.id.edit_content)
editDate.setText(it.date)
editContent.setText(it.content)
} ?: run {
Log.d("ERROR", "body is null")
}
}
override fun onFailure(call: Call<Diary>,
t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
return v
}
...
이렇게까지 코딩하고나면 일기 목록에서 항목을 클릭했을 때 해당 일기 항목을 수정할 수 있는 화면으로 전환이 되고, 일기 날짜와 일기 내용이 화면에 표시가 되지.
그러면 EditFragment 에서 일기 내용을 저장하는 RestAPI 호출을 해보도록 할께.
일기를 수정해서 저장할 때 사용하는 RestAPI 는 PUT 메소드를 사용하지.
Path 로 일기 고유번호를 전달하고, Request Body 로는 일기 데이터를 전달하고 있는데, 현재 구성되어 있는 Diary 클래스 멤버에는 email 이 없으니까 다음과 같이 추가해준 다음에 진행할께.
data class Diary(
@SerializedName("id") val id: Long
, @SerializedName("diary_date") val date: String
, @SerializedName("diary_content") val content: String
, @SerializedName("email") val email: String
)
EditFragment 에서 고유번호에 대한 일기 데이터를 수신받아서 Diary 클래스형 멤버로 저장하는 코드도 추가해볼께.
EditFragment.kt
...
class EditFragment : Fragment() {
private var id: Long? = null
private lateinit var mainActivity: MainActivity
private var diary: Diary? = null
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
id?.let {
val token = "Bearer=" + mainActivity.getPref("TOKEN", "")
diaryService.getDiary(token, id!!)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>,
response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful.not()) {
Log.d("ERROR", "CODE=" + response.code().toString())
}
response.body()?.let {
diary = it
val editDate: EditText = v.findViewById(R.id.edit_date)
val editContent: EditText = v.findViewById(R.id.edit_content)
editDate.setText(diary!!.date)
editContent.setText(diary!!.content)
} ?: run {
Log.d("ERROR", "body is null")
}
}
override fun onFailure(call: Call<Diary>,
t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
...
EditFragment 클래스에 Diary 클래스형 멤버를 추가한 상태에서 고유번호의 일기 데이터를 내려받았을 때 diary 변수에 이를 저장부터 해주고 UI 에 이 변수의 데이터를 설정해주는 방식으로 변경해봤어.
이렇게 수정하면 일기 날짜와 일기 내용을 수정하고 저장하게 되면 일기 날짜, 일기 내용 UI 의 데이터를 diary 변수에 대입한 후에 diary 데이터를 그대로 사용할 수가 있게 되지.
이제 /diary/{id} URL 을 PUT 메소드로 사용하는 코드를 추가해볼께.
button_save ID 의 버튼이 눌렸을 때 UI 에 입력되어 있는 데이터를 diary 에 우선 대입하고 RestAPI 를 호출하는 순서로 코딩하면 되겠지.
...
val buttonSaveDiary: Button = v.findViewById(R.id.button_save)
buttonSaveDiary.setOnClickListener {
val editDate: EditText = v.findViewById(R.id.edit_date)
val editContent: EditText = v.findViewById(R.id.edit_content)
diary?.date = editDate.text.toString()
diary?.content = editContent.text.toString()
findNavController().navigate(R.id.action_editFragment_to_listFragment)
}
...
diary 는 null 일 수도 있기 때문에 위 코드처럼 작성했는데, date 와 content 가 val 로 선언되어 있기 때문에 값을 대입할 수가 없다고 나와. 그래서 Diary 클래스의 멤버를 val 이 아닌 var 로 수정해줄께.
data class Diary(
@SerializedName("id") val id: Long
, @SerializedName("diary_date") var date: String
, @SerializedName("diary_content") var content: String
, @SerializedName("email") val email: String
)
id 와 email 은 변경될 일도 없고 변경되어서도 안되기 때문에 그대로 두었어.
이번에는 RestAPI를 호출할 차례야.
interface DiaryService {
...
@PUT("/diary/{id}")
fun saveDiary(
@Header("Authorization") token: String
, @Path("id") id: Long
, @Body params: Diary
): Call<Diary>
}
DiaryService 인터페이스에 새로 추가한 saveDiary 함수를 사용하는 코드를 작성해볼께.
EditFragment.kt
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val v: View = inflater.inflate(R.layout.fragment_edit, container, false)
val diaryService: DiaryService = retrofit.create(DiaryService::class.java)
val buttonSaveDiary: Button = v.findViewById(R.id.button_save)
buttonSaveDiary.setOnClickListener {
val editDate: EditText = v.findViewById(R.id.edit_date)
val editContent: EditText = v.findViewById(R.id.edit_content)
diary?.date = editDate.text.toString()
diary?.content = editContent.text.toString()
diary?.let { it1 ->
val token = "Bearer=" + mainActivity.getPref("TOKEN", "")
diaryService.saveDiary(token, it1.id, it1)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful.not()) {
Log.d("ERROR", "CODE=" + response.message())
}
response.body()?.let {
if (response.code() == 200) {
findNavController().navigate(R.id.action_editFragment_to_listFragment)
} else {
Log.d("ERROR", "CODE=" + response.code())
}
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
}
...
이렇게 수정해서 실행시켰더니 아래와 같은 에러가 발생했어.
java.io.EOFException: End of input at line 1 column 1 path $ 에러는 response 값이 null 인 경우에 발생하는 exception 이라고 하네.
참고: https://velog.io/@soyoung-dev/KotlinError-java.io.EOFException-End-of-input-at-line-1-column-1-path
위 링크를 참고해서 NullOnEmptyConverterFactory 클래스를 작성하고 아래와 같이 코드를 추가했어.
class EditFragment : Fragment() {
private var id: Long? = null
private lateinit var mainActivity: MainActivity
private var diary: Diary? = null
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://diary.woohahaapps.com:8080")
.addConverterFactory(NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
...
디버그모드로 실행시켜봤는데, 내가 예상했던 것과는 다른 데이터가 내려왔어.
response code 가 200 으로 성공한 경우에는 일기 데이터 목록으로 전환하도록 코드를 수정했어.
...
diary?.let { it1 ->
val token = "Bearer=" + mainActivity.getPref("TOKEN", "")
diaryService.saveDiary(token, it1.id, it1)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful) {
findNavController().navigate(R.id.action_editFragment_to_listFragment)
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
...
RestAPI 를 조금 더 효율적으로 작성하는 방법을 공부해야 할 것 같아.
어쨌든 기존 일기를 수정하는 작업까지 완료했어. 이번에는 새 일기를 작성하는 코드를 작성해보려고 해.
fragment_list.xml 에서 RecyclerView 하단에 버튼을 배치하고 새 일기 쓰기 버튼으로 사용하려고 해.
“일기 쓰기” 버튼에 대한 클릭리스너를 아래와 같이 작성해봤어. 사실 안드로이드 : diary – RecyclerView 에서 주석처리했던 코드를 주석해제시킨거야.
ListFragment.kt
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val v: View = inflater.inflate(R.layout.fragment_list, container, false)
val buttonNewDiary : Button = v.findViewById(R.id.button_new_diary)
buttonNewDiary.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_editFragment)
}
...
이제 “일기 쓰기” 버튼을 클릭하면 일기 작성 화면으로 이동하겠지만, EditFragment 클래스의 멤버 변수 id 와 diary 는 모두 null 로 설정된 상태겠지. 이런 상황에서 EditFragment 의 “Save Diary” 버튼을 클릭하면 POST 메소드의 /diary URL 을 호출하도록 해야 해.
DiaryService 클래스에 POST 메소드의 /diary URL 호출 함수를 작성해볼께.
interface DiaryService {
...
@POST("/diary")
fun createDiary(
@Header("Authorization") token: String
, @Body params: Diary
): Call<Diary>
}
fragment_edit.xml 에 배치된 “Save Diary” 클릭리스너 함수에서 diary 가 null 인 경우에 대한 처리코드는 다음과 같이 작성했어.
...
val buttonSaveDiary: Button = v.findViewById(R.id.button_save)
buttonSaveDiary.setOnClickListener {
val editDate: EditText = v.findViewById(R.id.edit_date)
val editContent: EditText = v.findViewById(R.id.edit_content)
diary?.date = editDate.text.toString()
diary?.content = editContent.text.toString()
val token = "Bearer=" + mainActivity.getPref("TOKEN", "")
val tokenParts = mainActivity.getPref("TOKEN", "").split(".")
val decoder = Base64.getUrlDecoder()
val header = String(decoder.decode(tokenParts[0]))
val payload = String(decoder.decode(tokenParts[1]))
val signature = String(decoder.decode(tokenParts[2]))
val email = JSONObject(payload).get("sub")
diary?.let { it1 ->
diaryService.saveDiary(token, it1.id!!, it1)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful) {
findNavController().navigate(R.id.action_editFragment_to_listFragment)
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
} ?: run {
val diaryNew = Diary(null, editDate.text.toString(), editContent.text.toString(),
email.toString()
)
Log.d("DATA", diaryNew.toString())
diaryService.createDiary(token, diaryNew)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful) {
findNavController().navigate(R.id.action_editFragment_to_listFragment)
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
}
...
EditFragment 클래스의 멤버변수 diary 가 null 이라는 것은 새로운 일기를 작성하는 경우이기 때문에 새 일기 데이터를 저장할 diaryNew 변수를 선언했어. 이 경우 id 가 null 일 수 있기 때문에 Diary 클래스의 id 를 null 허용으로 변경했지.
data class Diary(
@SerializedName("id") val id: Long?
, @SerializedName("diary_date") var date: String
, @SerializedName("diary_content") var content: String
, @SerializedName("email") var email: String
)
그리고 email 주소에 현재 로그인한 사용자의 email 주소값을 설정해주어야 하기 때문에 val 에서 var 로 수정하고, 현재의 jwt 토큰으로부터 로그인정보를 구해서 email 주소에 설정을 해주고 있어.
RestAPI 호출이 성공한 경우에는 일기 목록 화면으로 다시 이동시켰지.
이제 필수적인 기능은 모두 완성이 되었는데 프래그먼트 전환상의 문제가 있어. Navigation 의 Back 버튼을 사용했을 때 나타날 필요가 없는 프래그먼트가 여전히 보이는 문제점이지.
전환 히스토리에서 남겨두지 말아야 할 프래그먼트인 경우에는 popBackStack() 함수를 이용해서 프래그먼트를 전환하도록 수정했어.
LoginFragment.kt
...
response.body()?.let {
val result: ResponseLoginResult = response.body()!!
val token = result.body
mainActivity.setPref("TOKEN", token)
mainActivity.setLoginState(true)
//findNavController().navigate(R.id.action_loginFragment_to_listFragment)
findNavController().popBackStack()
}
...
EditFragment.kt
...
diary?.let { it1 ->
diaryService.saveDiary(token, it1.id!!, it1)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful) {
//findNavController().navigate(R.id.action_editFragment_to_listFragment)
findNavController().popBackStack()
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
} ?: run {
val diaryNew = Diary(null, editDate.text.toString(), editContent.text.toString(),
email.toString()
)
Log.d("DATA", diaryNew.toString())
diaryService.createDiary(token, diaryNew)
.enqueue(object: Callback<Diary> {
override fun onResponse(call: Call<Diary>, response: Response<Diary>) {
Log.d("DATA", response.toString())
if (response.isSuccessful) {
//findNavController().navigate(R.id.action_editFragment_to_listFragment)
findNavController().popBackStack()
}
}
override fun onFailure(call: Call<Diary>, t: Throwable) {
Log.d("FAIL", t.toString())
}
})
}
}
...
이렇게 완성된 diary 프로그램은 Back 버튼을 누르더라도 불필요한 프래그먼트가 보이지 않게 되지.