๐ 24๋ 5์ 20์ผ ~ 5์ 26์ผ |
|||||
์์์ผ |
ํ์์ผ |
์์์ผ | ๋ชฉ์์ผ | ๊ธ์์ผ ๋ฐ ์ฃผ๋ง | |
์ฌํ ๊ฐ์ ๋ณต์ต | โก ๋ฏธ์ธ๋จผ์ง์ฑ | ||||
๊ณผ์ ํด์ค | โก ๊ณผ์ ํด์ค ์๊ฐ | ||||
ํ ํ๋ก์ ํธ | โ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ - MVC -> MVVM - DataStore |
โ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ ๋ฐ ๋ง๋ฌด๋ฆฌ - UI ๋ง๋ฌด๋ฆฌ - ์ค๋ฅ ์์ |
โ ์ต์ข
๋จธ์ง โ ๋ฐํ ์ค๋น |
โ ๋ฐํ ์ค๋น | โ ๋ฐํ ๋ฐ ๋ง๋ฌด๋ฆฌ |
๋ฒ ์ด์ง๋ฐ ๊ฐ์ |
โ ์ด์ ๊ฐ์ ์๊ฐ (1/2/3๊ฐ) |
โ ๋ฒ ์ด์ง๋ฐ 9ํ์ฐจ (Retrofit) |
|||
Joyce ์์ ๋ ํ | โก TodoList ์ ์ (Room) |
โก ๋ฏธ์ธ๋จผ์ง V1.0 | โก ๋ฏธ์ธ๋จผ์ง V2.0 | โก ๋ฏธ์ธ๋จผ์ง V3.0 | |
์ฌํ ๋ชฉํ | |||||
- KIA ๊ฐ๋
ํ๊ธฐ - Android Developer ์ฝ๊ธฐ - ๊ฐ์ธํ๋ก์ ํธ UI ๊ตฌํ - ์ง๋ ํ๋ก์ ํธ ์ฝ๋ ๋ฏ์ด๋ณด๊ธฐ - ๋งํฌ ์๊ฐ - ์บ ํ ๊ณต์ ๊ต์ก์ด ๋๋๊ณ ๋๋ฉด, ์ฑ๋ฆฐ์ง/์คํ ๋ค๋ ์ฐจ๋ก๋ก ์๊ฐํ๊ธฐ - ์ ์ฐฝ๊ฒฝ ์ ๋ฆฌ |
1. ํ ํ๋ก์ ํธ : DataStore๋ก ์ ์ ์ด๋ฆ ์ ์ฅ ๊ตฌํํ๊ธฐ
Preferences Datastore๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํ๊ธฐ | Android Developers
์ด Codelab์์๋ ์ํ ์ฑ์ ์์ ํ์ฌ SharedPreferences๋ฅผ ๋์ฒดํ๋ ์๋ก์ด ํฅ์๋ ๋ฐ์ดํฐ ์ ์ฅ์ ์๋ฃจ์ ์ธ Jetpack Preferences Datastore๋ฅผ ํตํฉํฉ๋๋ค.
developer.android.com
- 1. Gradle ์ค์
- 2. Datastore ์ค์ (UserPreferences.kt)
DataStore๋ฅผ ์ด์ฉํ์ฌ ์ฌ์ฉ์ ์ด๋ฆ์ ์ ์ฅํ๊ณ ๋ถ๋ฌ์ฌ ์ ์๋๋ก ์ค์ - 3. Fragment ์ค์ (MyVideoFragment.kt์์ UI ๊ตฌ์ฑ, ์ ๋ณด ์
๋ฐ์ดํธ)
ํ ์คํธ๋ทฐ๋ฅผ ํด๋ฆญํ๋ฉด ๋ค์ด์ผ๋ก๊ทธ๋ฅผ ๋์ ์ฌ์ฉ์๊ฐ ์ด๋ฆ์ ์ ๋ ฅํ ์ ์๊ฒ ํ๊ธฐ
(1) build.gradle ์ค์
build.gradle
implementation ("androidx.datastore:datastore-preferences-android:1.1.1")
(2) Datastore ์ค์ (UserPreferences.kt)
UserPreferences.kt
// preferenceDataStore(name = "") ๋ฐ์ดํฐ์คํ ์ด ์ค์ ๋ฐ ์ด๋ฆ ์ค์
val Context.dataStore by preferencesDataStore(name = "user_prefs")
// DataStore๋ฅผ ํตํด ์ ์ฅ ๋ฐ ๋ถ๋ฌ์ค๋ ๊ธฐ๋ฅ
class UserPreferences(context: Context) {
private val dataStore = context.dataStore // ์์์ ์ ์ํ DataStore ์ฌ์ฉ
companion object { // ํด๋์ค์ ๋ชจ๋ ์ธ์คํด์ค์์ USER_NAME_KEY ๊ณตํต์ผ๋ก ์ฌ์ฉ๊ฐ๋ฅํ๋๋ก ์ ์ธ
val USER_NAME_KEY = stringPreferencesKey("user_name")
}
val userNameFlow = dataStore.data
.catch { exception -> // IO ์์ธ ์ ๋น ๊ธฐ๋ณธ์ค์ ๋ฐํ, ๊ธฐํ ์์ธ ์ฒ๋ฆฌ
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences -> // DataStore์์ ์ฝ์ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ํ์์ผ๋ก ๋ณํ
preferences[USER_NAME_KEY] ?: "์๋ฌด๊ฐ๋!"
}
suspend fun updateUserName(name: String) { // ๋น๋๊ธฐ ์คํ์ ์ํ suspend fun
dataStore.edit { preferences -> // DataStore์ ๋ฐ์ดํฐ ์ ์ฅ, preferences๋ ํ์ฌ ์ ์ฅ๋ ๋ฐ์ดํฐ
preferences[USER_NAME_KEY] = name + "๋!"
}
}
}
(3) Fragment ์ค์ (MyVideoFragment.kt์์ UI ๊ตฌ์ฑ, ์ ๋ณด ์ ๋ฐ์ดํธ)
class MyVideoFragment : Fragment() {
...
// userPreferences ์ด๊ธฐํ
private val userPreferences by lazy {
UserPreferences(requireContext())
}
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.myIdTv.setOnClickListener {
showEditNameDialog()
}
observeUserName()
}
// ํ
์คํธ ํด๋ฆญ ์ ๋์ฌ ๋ค์ด์ผ๋ก๊ทธ ์์ฑ
private fun showEditNameDialog() {
val editText = EditText(requireContext()).apply {
hint = "์ด๋ฆ์ ์
๋ ฅํ์ธ์"
}
AlertDialog.Builder(requireContext())
.setTitle("์ด๋ฆ์ ์์ ํด์")
.setView(editText)
.setPositiveButton("์ ์ฅํด์") { dialog, _ ->
val newName = editText.text.toString()
saveUserName(newName)
dialog.dismiss()
}
.setNegativeButton("์ทจ์ํด์") { dialog, _ ->
dialog.dismiss()
}
.show()
}
private fun saveUserName(newName: String) {
lifecycleScope.launch { // ์ฝ๋ฃจํด ๋น๋๊ธฐ
userPreferences.updateUserName(newName)
}
}
private fun observeUserName() {
lifecycleScope.launch { // ์ฝ๋ฃจํด ๋น๋๊ธฐ
userPreferences.userNameFlow.collect { userName -> // DataStore์์ ์ด๋ฆ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๊ฐ์งํ๊ณ ๋ทฐ์ ์
๋ฐ์ดํธ
binding.myIdTv.text = userName
}
}
}
...
(4) ๋ค์ด์ผ๋ก๊ทธ ์ปค์คํ
dialog_username.xml
<?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="wrap_content"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:background="@drawable/bg_btn_dialog_cancel"
android:padding="20dp">
<TextView
android:id="@+id/tv_username_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:fontFamily="@font/gangwonmodubold"
android:text="์ด๋ฆ์ ๋ณ๊ฒฝํด๋ณด์ธ์!"
android:textSize="25sp"
app:layout_constraintBottom_toTopOf="@+id/et_edit_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:backgroundTint="#F3F3F3"
android:ellipsize="end"
android:fontFamily="@font/gangwonmodubold"
android:singleLine="true"
android:hint="๋ณ๊ฒฝํ ์ด๋ฆ์ ์
๋ ฅํ์ธ์"
android:inputType="text"
android:textColor="@color/grey88"
android:textSize="25sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_username_title" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_username_confirm"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="125dp"
android:layout_height="46dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="5dp"
android:background="@drawable/bg_btn_dialog_confirm"
android:fontFamily="@font/gangwonmodubold"
android:text="ํ์ธ"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_username_cancel"
app:layout_constraintTop_toBottomOf="@+id/et_edit_username" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_username_cancel"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="125dp"
android:layout_height="46dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="5dp"
android:background="@drawable/bg_rv_searchitem"
android:fontFamily="@font/gangwonmodubold"
android:text="์ทจ์"
android:textColor="@color/primary_yellow"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/btn_username_confirm"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_edit_username" />
</androidx.constraintlayout.widget.ConstraintLayout>
showEditNameDialog ํจ์ ์์
private fun showEditNameDialog() {
val dialogBinding = DialogUsernameBinding.inflate(layoutInflater) // ๋ฐ์ธ๋ฉ์ผ๋ก ๊ตฌํ
val dialog = AlertDialog.Builder(requireContext())
.setView(dialogBinding.root)
.create()
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogBinding.btnUsernameConfirm.setOnClickListener {
val newName = dialogBinding.etEditUsername.text.toString()
saveUserName(newName)
dialog.dismiss()
}
dialogBinding.btnUsernameCancel.setOnClickListener {
dialog.dismiss()
}
dialog.show()
}
- ์ปค์คํ ๋์์ธ์ ๊ตฌํํ๊ธฐ ์ํด ๋ค์ด์ผ๋ก๊ทธ ๋น๋ ๋ฉ์๋๋ฅผ ์์ ํ๋ค.
- ๋ฐ์ธ๋ฉ์ด ํธํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ธ๋ฉ์ ์ฐ๊ฒฐํด์ฃผ์ด ์ฌ์ฉํ๋ค.
2. ํ ํ๋ก์ ํธ : ํ๋ก์ ํธ ์ ์ฒด์ ์ธ UI ์์ ์์
- ๋ชจ๋ ํ์ ๊ธฐ๋ฅ ๊ตฌํ์ด ๋๋๊ณ ๋์, ์คํ์ผ์ด ๋ค๋ฅด๊ฒ ๊ตฌํ๋ ์ ์ฒด์ ์ธ UI๋ฅผ ์์ ํ์๋ค.
- ์์ ๋ชฉ๋ก
- HomeFragment ๋ฐ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์์ดํ , ์คํผ๋
- DetailFragment
- SearchFragment ๋ฐ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์์ดํ
- MyFragment ๋ฐ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์์ดํ
๐จ HomeFragment
- ๋ค์ํ ๋ ์ด์์ ์์
- LinearLayout์ผ๋ก ์ค์ ๋์ด ์๋ ๊ฒ ๋ค์ Constraint๋ก ๋ณ๊ฒฝ
- ๊ฐ ์์๋ง๋ค์ margin ๋ฐ ์ฐ๊ฒฐ ๊ตฌ์กฐ ์ฌ์์
(1) ์๋จ ์์ด์ฝ ๋ฒกํฐ(SVG) ์์
- png๋ก ์ ์ฉ๋๋ ์์ด์ฝ์ ๋ฒกํฐ๋ก ์์
- ๋ฒกํฐ๋ก ๋ฑ๋กํ ์ ์๋ ํ ์คํธ ๋ก๊ณ ๋ PNG๋ก ๋ฑ๋กํ๋ฉด์, ํด์๋ ๊นจ์ง์ง ์๊ฒ wrap_content ์ฒ๋ฆฌ
(2) Background.xml ์์ฑ ๋ฐ ์ ์ฉ
bg_fragment_home.xml
<?xml version ="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:startColor="#FFC00A"
android:centerColor="#FFEDBA"
android:endColor="#FFFAEB"
android:centerX="0.1"
/>
</shape>
- ๊ธฐ์กด์ ์ด๋ฏธ์ง๋ก ๋ฐ๋ก๋ฐ๋ก ์ ์ฉ๋๋ ๋ฐฐ๊ฒฝ์ ํ๋์ ๊ทธ๋ผ๋์ธํธ๋ก ์ค์ ํ background.xml ์์ฑ
- centerX๋ก centerColor์ ์์์ ์ง์
(3) Bottom Navigation ์์
- height๊ฐ 70dp๋ก ์ค์ ๋์ด ์๋ ๊ฒ(์ผ์ชฝ)์ wrap_content๋ก ์์ ํ์ฌ ์ ์์ ์ผ๋ก ์ถ๋ ฅ๋๋๋ก ์์ (์ค๋ฅธ์ชฝ)
- ์ผ์ชฝ ์ฌ์ง์ฒ๋ผ ํด๋ฆญ ์ ์๊ธฐ๋ ํ์ ์ ๋๋ฉ์ด์ ์ญ์ (ItemRippleColor๋ฅผ ํฌ๋ช ์ผ๋ก ์ค์ )
(4) ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ๊ฐ ์์ดํ ๋ง๋ค ์ค์
- ๋จ์ํ๊ฒ ImageView๋ก ํ์๋ ๊ฒ๋ค์ ๋ชจ๋ ์นด๋๋ทฐ ํํ๋ก ์์
- ๊ฐ ์์ดํ ์ Figma ๊ธฐ๋ณธ ์ค๊ณ์ ๋ง๊ฒ ์์/๋ง์ง/๋ ์ด์์/ํฐํธ ๋ฑ ๋ชจ๋ ์์
- maxWidth ์์ดํ ์ ๋ง๊ฒ ์์
(5) Spinner ๋์์ธ ์ค์
[์๋๋ก์ด๋] ์ปค์คํ ์คํผ๋ ๋ง๋ค๊ธฐ
์์ ๊ฐ์ ๊ทธ๋ฆผ์ ์ปค์คํ ์คํผ๋๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค. ํ์ดํ ๋ชจ์์ ์์ด์ฝ์ผ๋ก ํ๋ layer-list๋ฅผ ๋ง๋ค์ด drawble ํด๋์ ์ถ๊ฐํด์ค๋๋ค. spinner_custom.xml color, icon, margin dp๋ฑ์ ์ ์ ํ ์กฐ์ ํด ์ค๋๋ค
ddangeun.tistory.com
(6) CustomDialog
override fun onBackPressed() {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog, null)
val builder = AlertDialog.Builder(this)
builder.setView(dialogView)
.setCancelable(false)
val alertDialog = builder.create()
// ๋ค์ด์ผ๋ก๊ทธ ๋ด๋ถ์ ๋ฒํผ ์ด๊ธฐํ ๋ฐ ํด๋ฆญ ์ด๋ฒคํธ ์ค์
dialogView.findViewById<AppCompatButton>(R.id.btn_confirm).setOnClickListener {
alertDialog.dismiss()
super.onBackPressed() // ํ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ฑ ์ข
๋ฃ
}
dialogView.findViewById<AppCompatButton>(R.id.btn_cancel).setOnClickListener {
alertDialog.dismiss() // ์ทจ์ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ค์ด์ผ๋ก๊ทธ ๋ซ๊ธฐ
}
alertDialog.show()
}
๐จ DetailFragment
- ํฐํธ ๋ณ๊ฒฝ
- API๋ก ๋ถ๋ฌ์ค๋ Thumbnail์ ํด์๋๊ฐ ๋ณ๊ฒฝ
๐จ SearchFragment
- ์ ๋ฒ์ฃผ WIL์ ์์ฑ ์๋ฃ
๐จ MyVideoFragment
- background src ์์
- ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์์ดํ ์์
- ๊ฐ์ข Margin, layout ์ฌ์ค์
3. ํ ํ๋ก์ ํธ : ๋ค๋น๊ฒ์ด์ ์ด๋ ์์ , ๊ฐ์ข ํธ๋ฌ๋ธ์ํ

- ์ด๋ฒ ์ฃผ ๋ด๋ด ๋ค์ํ ํํ์ ํธ๋ฌ๋ธ์ ๊ฒช๊ณ , ๊ทธ๊ฑธ ํด๊ฒฐํ๋ ๊ณผ์ ์ ๊ฑฐ์ณค๋ค.
- ํ ๋ ธ์ ๋ฐ GitHub Repository์ ์ํค๋ก ๋ชจ๋ ์์ฑ๋์ด ์์ผ๋ฏ๋ก ๋ธ๋ก๊ทธ์ ๋ฐ๋ก ์ ์ง ์๊ฒ ๋ค.
4. ํ ํ๋ก์ ํธ : ๋ฐํ ๋ฐ ๋ง๋ฌด๋ฆฌ
- ํ์๋ค๊ณผ ํจ๊ป ์ด๊ธฐ ์์ด์ดํ๋ ์ ์์ ๋ฐ PPT๋ฅผ ์ ์ํ๋ค.
- ํ๋ก์ ํธ๊ฐ ์ ๋ง๋ฌด๋ฆฌ ๋ผ์ ๊ธฐ๋ถ์ด ์ข๋ค.
- ์ด๋ฒ ํ์ด ์ํต์ด ์ ๋๊ณ ์๋ก์๋ก ์๋ ค์ฃผ๋ฉด์ ํด์, ์ฝ๋์ ๋ํ ์ดํด๋๊ฐ ๋งค์ฐ ๋์์ก๋ค.
5. ๋ฒ ์ด์ง ๋ง์ง๋ง ๊ฐ์
์ฑ ์ํคํ ์ฒ ๊ฐ์ด๋ | Android Developers
์ด ํ์ด์ง๋ Cloud Translation API๋ฅผ ํตํด ๋ฒ์ญ๋์์ต๋๋ค. ์ฑ ์ํคํ ์ฒ ๊ฐ์ด๋ ์ปฌ๋ ์ ์ ์ฌ์ฉํด ์ ๋ฆฌํ๊ธฐ ๋ด ํ๊ฒฝ์ค์ ์ ๊ธฐ์ค์ผ๋ก ์ฝํ ์ธ ๋ฅผ ์ ์ฅํ๊ณ ๋ถ๋ฅํ์ธ์. ์ด ๊ฐ์ด๋์๋ ๊ณ ํ์ง์ ๊ฐ๋ ฅํ
developer.android.com
- guide to app architecture ์ด๊ฑฐ ๋ค ์ฝ์ง ์์ผ๋ฉด ์๋๋ก์ด๋ ๊ฐ๋ฐ ์ด๋์ ํ๋ค๊ณ ๋งํ๋ฉด ์๋จ
- ๋จ์ํ ์ฑ ์ํคํ ์ฒ๋ ํด๋ฆฐ์ํคํ ์ฒ๊ฐ ์๋. ์์์ ์๋ ๊นํ๋ธ ์ฝ๋๋ ์ด๋ฐ ๊ฑฐ ์ฝ์ด๋ผ
- data (network-di,model,retrofit,source / repository)
- domain(di, model, repository-์ธํฐํ์ด์ค๋ก ๊ตฌํ, ์ค์ ์์ ์ data์ repository, usecase)
- ๋ฌธ์์ Dependency injection(์์กด์ฑ ์ฃผ์
) ๋ ๋ค ์์ผ๋๊น ๋ค ์ฝ๊ธฐ.
์ต์ํด์ง๊ธฐ ์ํด์๋ ๋ฐ๋ณต ์๋ฌ๋ก ์ด๋ฐ ๊ฒ ์๊ตฌ๋ ํ๊ณ ๊ณ์ ์ฝ์ด์ผ ํจ - ์ฐ์ตํ๊ธฐ API : CATAAS.com
- ๋ชจ๋ API๋ Docs๋ฅผ ์ฐธ์กฐํด์ผ ํจ
๐ญ Retrospect
์ด๋ฒ ํํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๋ง์ ๋ถ๋ถ์์ ์ฑ์ฅํ ๊ฒ ๊ฐ๋ค. ์ ๋ฒ์ฃผ์ ๊ณ ๋ฏผํ๋ ๋ถ๋ถ์ ์ ๋ง ๋ชจ๋ ํด๊ฒฐ๋์๊ณ , ํํ๋ก์ ํธ์ ๋ง์ ๊ธฐ์ฌ๋ฅผ ํ ์ ์์๊ณ , ์ดํด๋๋ ๋งค์ฐ ๋์์ก๋ค. ์ด๋ฒ ํ๊ณผ ํจ๊ปํ๋ฉฐ 'ํ'์ด๋ ๊ฑด ๊ฐ๊ฐ์ธ์ ์ค๋ ฅ๋ณด๋ค๋ ์๋ก๊ฐ์ ์ํตํ ์ ์๋ ์ญ๋์ด ํจ์ฌ ๋ ์ค์ํ ๊ฒ์ด๋ผ ์ฒด๊ฐํ๋ค. ๋ฐํํ์๋ง์ ๋ฐ๋ก 6์ฃผ๊ฐ์ ์ต์ข ํ๋ก์ ํธ๊ฐ ์์์ธ๋ฐ, ์ต์ข ๋๋ ์ผ๋ง๋ ์ฑ์ฅํ ์ ์์๊น ๊ธฐ๋๋๋ค.
๋ง๋ฌด๋ฆฌ ๋๊น์ง ์ํ์!
โ