코딩 보카를 마치고…
프로젝트 소개
코딩 보카는 개발을 시작한 지 얼마 안된 초보자들을 위한 앱으로 다양한 개발 용어들과 개발관련 영단어들을 사용자의 수준에 맞게 레벨 별로 학습할 수 있게 도와주는 앱 프로젝트이다.
- 플랫폼 : 안드로이드/모바일
- 사용 기술 : Kotlin, XML, ViewBinding, Firebase, Glide, Room
- 진행 기간: 2022.11.06 ~ 2022.11.27, 2022.12.6 ~ 2022.12.7 (23일)
- 진행 인원: 개인 프로젝트 (1인)
- 깃허브 리포지토리
- 플레이 스토어
개발 배경
당시에 나는 현재도 그렇지만 그 때 당시에는 개발 관련된 용어에 대해 거의 모르는 편이였고 주위 친구들을 봐도 특별한 몇몇을 제외하면 내 상황과 크게 다른 편은 아니였다. 그래서 일단 개발하면서나 선배들과 이야기하다가
모르는 단어가 생기면 기록해두고 따로 일일히 찾아보는 식으로 했었는데 이럴바에는 그냥 단어장으로 영어 단어처럼 기본적으로 알아야 할 단어들을 외울 수 있는 앱이 있지 않을까? 해서 검색해봤으나
그런 앱은 존재하지 않았고 그렇다면 “그냥 내가 만들까?” 라는 생각이 들면서 어차피 친구들이랑 내년에 들어올 후배들도 코딩에 대해 배워야 하는데 이러한 코딩 주제로 앱을 만들면 그중 몇명만 깔아도 실제 유저들이 생기고 소수라도 수요가 있을 거 같길래 해당 프로젝트를 시작했다
사용 예제
구현 기능
- 레벨 시스템
- 날마다 유저가 암기한 단어와 목표치까지 얼마나 남았는 지를 보여주는 기능
- 유저의 스테이터스 값을 실시간으로 받아오는 기능
- 단어장
- 유저의 레벨에 맞는 단어장을 보여주는 기능
- 퀴즈
- 유저의 레벨에 맞는 퀴즈 문제를 보여주는 기능
- 퀴즈를 통과했다면 유저의 레벨을 증가시키는 기능
이슈
데이터가 교체될 시 앱이 크래시나는 오류
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
levelRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val value = dataSnapshot.value
binding.csWordLv.text = getString(R.string.cs_word_lv,value)
val wordRef = database.getReference("csWord").child("lv"+value.toString())
wordRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val value : ArrayList<Any> = dataSnapshot.value as ArrayList<Any>
for (i in 1..15){
val valueMap: HashMap<String,String> = value[i] as HashMap<String, String>
csWords.add(Word(valueMap["word"]!!.toString(),valueMap["mean"]!!.toString()))
}
binding.list.adapter?.notifyDataSetChanged()
}
override fun onCancelled(error: DatabaseError) {
Log.w(TAG, "Failed to read value.", error.toException())
}
})
}
override fun onCancelled(error: DatabaseError) {
Log.w(TAG, "Failed to read value.", error.toException())
}
})
내가 처음에 앱에서 Firebase Realtime Database에 저장되어 있는 사용자의 정보를 addValueEventListener 메서드를 사용해서 가져왔는데 평상시에는 괜찮으나 만약 사용자의 정보가 바뀌게 된다면 앱이 크래시가 나며 강제 종료되는 오류가 생겼었다.
해당 문제를 해결하기 위해 당시 원인이 무엇일까를 고민하다가 사용자의 정보를 addValueEventListener로 받아올 시 데이터베이스의 특정 경로에 대한 변경을 지속적으로 감지하는데 이러한 코드가 여러 Fragment에서 사용되고 있으니 해당 문제 때문에 그런 것이 아닐까? 라는 생각이 들어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
levelRef.get().addOnSuccessListener {
val value = it.value
binding.csWordLv.text = getString(R.string.cs_word_lv,value)
val wordRef = database.getReference("csWord").child("lv"+value.toString())
wordRef.get().addOnSuccessListener { it ->
val value : ArrayList<Any> = it.value as ArrayList<Any>
for (i in 1..15){
val valueMap: HashMap<String,String> = value[i] as HashMap<String, String>
csWords.add(Word(valueMap["word"]!!.toString(),valueMap["mean"]!!.toString()))
}
binding.list.adapter?.notifyDataSetChanged()
}
}
꼭 지속적으로 변화를 체크하지 않아도 되는 단어의 뜻과 단어 같은 것들은 데이터베이스에서 값을 한번만 가져오는 addOnSuccessListener로 변경했고
사용자 정보나 데이터베이스 정보가 변경될 시 앱이 강제 종료가 되는 문제를 해결할 수 있었다.
RecyclerView View 재사용으로 인한 문제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class EnglishWordAdapter (private val englishwords : List<Word>) :
RecyclerView.Adapter<EnglishWordAdapter.EnglishWordViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EnglishWordViewHolder {
val binding = ListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
return EnglishWordViewHolder(binding).also {
binding.wordBox.setOnClickListener {
if (binding.listItemText2.visibility == View.VISIBLE){
binding.listItemText2.visibility = View.GONE
binding.listItemText3.visibility = View.VISIBLE
}
else {
binding.listItemText2.visibility = View.VISIBLE
binding.listItemText3.visibility = View.GONE
}
}
}
}
override fun onBindViewHolder(holder: EnglishWordViewHolder, position: Int) {
holder.bind(englishwords[position])
}
override fun getItemCount(): Int {
return englishwords.size
}
class EnglishWordViewHolder(private val binding: ListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(englishword : Word){
binding.listItemText1.text = englishword.word
binding.listItemText4.text = englishword.mean
}
}
}
처음에 내가 구상했던 로직으로는 기본적으로는 단어가 보이고 뷰를 클릭하면 설명이 보이는 것을 생각하고 코드를 짰으나 RecyclerView가 재사용되다보니 위에서 뜻 보기를 누르고 화면을 스크롤하면 내가 뜻보기를 선택하지 않은 단어 또한 뜻이 나오는 문제를 겪고 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private var chkWord: String = ""
class EnglishWordAdapter (private val englishwords : List<Word>) :
RecyclerView.Adapter<EnglishWordAdapter.EnglishWordViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EnglishWordViewHolder {
val binding = ListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
return EnglishWordViewHolder(binding).also {
binding.wordBox.setOnClickListener {
if (binding.listItemText2.visibility == View.VISIBLE){
binding.listItemText2.visibility = View.GONE
binding.listItemText3.visibility = View.VISIBLE
}
else {
binding.listItemText2.visibility = View.VISIBLE
binding.listItemText3.visibility = View.GONE
}
chkWord = binding.listItemText1.text as String
}
}
}
}
해당 문제를 해결하기 위해 chk라는 변수를 하나 만들어서 확인 변수를 통해 뷰가 재사용되더라도 다른 단어에 뜻이 열리지 않게 만들었다.
RecyclerView item 짤리는 문제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:background="@color/backgroundColor"
tools:context=".fragment.CsWordFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70sp"
android:layout_gravity="bottom"
android:background="@drawable/nav_background"
android:baselineAligned="false"
android:gravity="bottom"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:id="@+id/home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/home"
android:src="@drawable/home" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/home_text"
android:textColor="#c8c8c8" />
</LinearLayout>
<LinearLayout
android:id="@+id/words"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/words_icon"
android:src="@drawable/words_f" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/words"
android:textColor="#D7263D" />
</LinearLayout>
<LinearLayout
android:id="@+id/learning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/study"
android:src="@drawable/learning" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/study_text"
android:textColor="#c8c8c8" />
</LinearLayout>
<LinearLayout
android:id="@+id/setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/setting"
android:src="@drawable/setting" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/setting_text"
android:textColor="#c8c8c8" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
CS단어장을 만들 때 RecyclerView에 들어가는 item이 짤리는 문제가 발생하여
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:background="@color/backgroundColor"
tools:context=".fragment.CsWordFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:layout_weight="1"
tools:listitem="@layout/list_item"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70sp"
android:layout_gravity="bottom"
android:background="@drawable/nav_background"
android:baselineAligned="false"
android:gravity="bottom"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:id="@+id/home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/home"
android:src="@drawable/home" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/home_text"
android:textColor="#c8c8c8" />
</LinearLayout>
<LinearLayout
android:id="@+id/words"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/words_icon"
android:src="@drawable/words_f" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/words"
android:textColor="#D7263D" />
</LinearLayout>
<LinearLayout
android:id="@+id/learning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/study"
android:src="@drawable/learning" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/study_text"
android:textColor="#c8c8c8" />
</LinearLayout>
<LinearLayout
android:id="@+id/setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10sp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/setting"
android:src="@drawable/setting" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/setting_text"
android:textColor="#c8c8c8" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
위와 같이 recyclerview를 LinearLayout로 한번 더 감싸고 layout_weight를 통해 해당 문제를 해결했다.
느낀점
이번 프로젝트는 내가 진행한 최초의 어느 정도 규모가 있는 앱 프로젝트로 내게는 굉장히 뜻깊은 앱이다 처음으로 다양한 기능을 가진 앱을 만들어 보았으며
처음에 생각한 “코딩 주제로 앱을 만들면 그중 몇명만 깔아도 실제 유저들이 생기고 소수라도 수요가 있을 거 같다”라는 예상이 적중하여 처음으로 100명 이상이 설치한 앱이 되었다. 하지만 1학년 때 만든 미숙한 앱이니 만큼 버그가 많았고 위에서 본 것처럼 해결된 버그도 있지만 반면 출시 당시 미처 발견하지 못했거나 해결하지 못한 버그들이 아직 남아있는 점이 맘에 걸린다.
또한 개발자에게 문의하기 기능을 추가하여 실제 유저들의 건의 사항에 대해 들어볼 수 있었는데 대부분 컨텐츠가 부족하다거나 디자인이 마음에 별로다 라는 피드백을 들었고 그동안은 학교 수업과 iOS를 배우느라 시간이 부족하여 넘겼으나 이번 년도 안에는 디자인부터 레벨 시스템 등을 싹 다 갈아엎고 컨텐츠 추가와 버그까지 해결하여 다시 한번 지금보다 더 자세히 회고록을 작성하려고 한다.