Ver3.5 ๐ ์๋๋ก์ด๋ Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผํ์ฉํ ์ฑ ๊ฐ๋ฐ ์์ฐ์ฑ ํฅ์

๐ ์๋๋ก์ด๋ Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ
ํ์ฉํ ์ฑ ๊ฐ๋ฐ ์์ฐ์ฑ ํฅ์
๊ตฌ๊ธ์ด ๋ง๋ ์ต๊ฐ ๋๊ตฌ ๋ชจ์์ผ๋ก ๊ฐ๋ฐ ์๋ 10๋ฐฐ ์ฌ๋ฆฌ๊ธฐ
์ผ, ์์งํ ๋งํด๋ณผ๊ฒ. ์๋๋ก์ด๋ ์ฑ ๊ฐ๋ฐํ๋ค๊ฐ "์ด๊ฑฐ ์ ์ด๋ ๊ฒ ๋ณต์กํด?" ๋ผ๊ณ ๋๋ ์ ์์ด? ๐คฏ
์กํฐ๋นํฐ ์๋ช
์ฃผ๊ธฐ ๊ด๋ฆฌํ๋ค๊ฐ ๋จธ๋ฆฌ ํฐ์ง๊ณ , ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ฒ๋ฆฌํ๋ค๊ฐ ์ฑ ํฌ๋์ ๋๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ง๋ค๊ฐ ๋ฐค์๊ณ ...
๊ทผ๋ฐ ์์์, ๊ตฌ๊ธ์ด ์ด ๋ชจ๋ ๊ณ ํต์ ํด๊ฒฐํ๋ ค๊ณ ๋ง๋ ๊ฒ ๋ฐ๋ก Jetpack์ด์ผ! ๐
Jetpack์ ๋จ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ๋๊ฐ ์๋์ผ. ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๋ชจ์๋์ ๊ฑฐ๋ํ ๋๊ตฌ ๋ชจ์์ง์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ.
2018๋
๊ตฌ๊ธ I/O์์ ์ฒ์ ๋ฐํ๋๊ณ , ์ง๊ธ์ ์๋๋ก์ด๋ ๊ฐ๋ฐ์๋ผ๋ฉด ๊ฑฐ์ ํ์๋ก ์ฐ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์ฌ๊ธฐ ๋ค ๋ค์ด์์ด.
์ค๋์ ์ด Jetpack์ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ํ๋ํ๋ ๋ฏ์ด๋ณด๋ฉด์, ์ด๋ป๊ฒ ํ๋ฉด ๊ฐ๋ฐ ์์ฐ์ฑ์ ์ง์ง๋ก ์ฌ๋ฆด ์ ์๋์ง ๊ฐ์ด ์์๋ณด์! ๐ช
๐ค Jetpack์ด ๋ญ๋ฐ? ์ ์จ์ผ ํด?
Jetpack์ ํ ๋ง๋๋ก ์ ์ํ๋ฉด "์๋๋ก์ด๋ ๊ฐ๋ฐ์์ ๊ณ ํต์ ์ค์ฌ์ฃผ๋ ๊ตฌ๊ธ์ ์ ๋ฌผ ์ธํธ"์ผ. ๐
๊ณต์์ ์ผ๋ก๋ "์๋๋ก์ด๋ ์ฑ์ ๋ ์ฝ๊ฒ ๊ฐ๋ฐํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ๋๊ตฌ, ๊ฐ์ด๋์ ๋ชจ์"์ด๋ผ๊ณ ์ ์๋ผ ์์ด.
1. ํ์ ํธํ์ฑ ๋ณด์ฅ โ ๊ตฌ๋ฒ์ ์๋๋ก์ด๋์์๋ ์ต์ ๊ธฐ๋ฅ์ ์ธ ์ ์์ด. AppCompat์ด ๋ํ์ ์ธ ์์ผ.
2. ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋ ๊ฐ์ โ ๋ฐ๋ณต์ ์ผ๋ก ์จ์ผ ํ๋ ์ฝ๋๋ฅผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋์ ์ฒ๋ฆฌํด์ค.
3. ๊ตฌ๊ธ ๊ณต์ ์ง์ โ ๊ตฌ๊ธ์ด ์ง์ ๋ง๋ค๊ณ ์ ์ง๋ณด์ํ๋๊น ์์ ์ฑ์ด ๋ณด์ฅ๋ผ.
Jetpack ์ด์ ์๋ ์ด๋ ๋๊ณ ? ๐คฆ
์๋ฅผ ๋ค์ด ํ๋ฉด ํ์ ํ ๋ ๋ฐ์ดํฐ ์ ์งํ๋ ค๋ฉด onSaveInstanceState()์ ์ง์ ๋ฐ์ดํฐ ์ ์ฅํ๊ณ , onRestoreInstanceState()์์ ๋ณต์ํ๋ ์ฝ๋๋ฅผ ๋งค๋ฒ ์จ์ผ ํ์ด.
๊ทผ๋ฐ ์ง๊ธ์? ViewModel ํ๋๋ฉด ๋์ด์ผ. ํ๋ฉด์ด ํ์ ํด๋ ๋ฐ์ดํฐ๊ฐ ์์์ ์ด์๋จ์. ๐ฏ
Jetpack์ ํฌ๊ฒ 4๊ฐ์ง ์นดํ
๊ณ ๋ฆฌ๋ก ๋๋์ด:
๐๏ธ Architecture ๐จ UI โ๏ธ Behavior ๐ง Foundation
์ค๋์ ์ด ์ค์์ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๊ฐ์ฅ ์ง์ ์ ์ธ ์ํฅ์ ์ฃผ๋ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์ง์ค์ ์ผ๋ก ํํค์ณ๋ณผ ๊ฑฐ์ผ!
๐ง ViewModel + LiveData: ์๋ช ์ฃผ๊ธฐ ์ง์ฅ์์ ํ์ถ!
์๋๋ก์ด๋ ๊ฐ๋ฐ์์ ๊ฐ์ฅ ์
๋ช
๋์ ๊ฒ ๋ญ์ง ์์? ๋ฐ๋ก ์๋ช
์ฃผ๊ธฐ(Lifecycle) ๊ด๋ฆฌ์ผ. ๐ฑ
์กํฐ๋นํฐ๊ฐ ์์ฑ๋๊ณ , ์ผ์์ ์ง๋๊ณ , ์ฌ๊ฐ๋๊ณ , ํ๊ดด๋๋ ๊ณผ์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๊ด๋ฆฌํ๋๋๊ฐ ์ง์ง ๊ณจ์น๊ฑฐ๋ฆฌ์๊ฑฐ๋ .
ํ๋ฉด ํ์ ํ ๋ฒ์ ์ฑ์ด ๋ป์ด๋ฒ๋ฆฌ๋ ๊ฒฝํ... ๋ค๋ค ํ ๋ฒ์ฏค์ ํด๋ดค์ ๊ฑฐ์ผ. ๐
๐ฆ ViewModel์ด๋?
ViewModel์ UI ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ํด๋์ค์ผ.
ํต์ฌ์ ํ๋ฉด ๊ตฌ์ฑ ๋ณ๊ฒฝ(ํ๋ฉด ํ์ ๋ฑ)์์ ์ด์๋จ๋๋ค๋ ๊ฑฐ์ผ!
์กํฐ๋นํฐ๋ ํ๋๊ทธ๋จผํธ๊ฐ ํ๊ดด๋๊ณ ์ฌ์์ฑ๋์ด๋ ViewModel์ ๋ฉ๋ชจ๋ฆฌ์ ๊ทธ๋๋ก ์ ์ง๋ผ.
// ViewModel ํด๋์ค ์ ์
class MainViewModel : ViewModel() {
private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
override fun onCleared() {
super.onCleared()
// ViewModel์ด ์์ ํ ์๋ฉธ๋ ๋ ํธ์ถ
// ๋ฆฌ์์ค ์ ๋ฆฌ ์ฝ๋ ์ฌ๊ธฐ์!
}
}
// Activity์์ ์ฌ์ฉ
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// LiveData ๊ด์ฐฐ
viewModel.count.observe(this) { count ->
binding.tvCount.text = count.toString()
}
binding.btnIncrement.setOnClickListener {
viewModel.increment()
}
}
}
by viewModels()๋ Kotlin ์์ ํ๋กํผํฐ๋ฅผ ํ์ฉํ ๊ฑฐ์ผ. ๋ด๋ถ์ ์ผ๋ก ViewModelProvider๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด๊ฑธ ์ฐ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด์ ธ. KTX ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ด์ผ ์ฌ์ฉ ๊ฐ๋ฅํด!
๐ก LiveData๋?
LiveData๋ ์๋ช
์ฃผ๊ธฐ๋ฅผ ์ธ์ํ๋ ๊ด์ฐฐ ๊ฐ๋ฅํ ๋ฐ์ดํฐ ํ๋์ผ.
์ฝ๊ฒ ๋งํ๋ฉด, ๋ฐ์ดํฐ๊ฐ ๋ฐ๋๋ฉด ์๋์ผ๋ก UI์ ์๋ ค์ฃผ๋ ๋๋ํ ๋ฐ์ดํฐ ์ปจํ
์ด๋์ผ.
๊ทธ๋ฆฌ๊ณ ์๋ช
์ฃผ๊ธฐ๋ฅผ ์๊ณ ์์ด์, ์กํฐ๋นํฐ๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์ ๋๋ ์
๋ฐ์ดํธ๋ฅผ ๋ณด๋ด์ง ์์. ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฑฑ์ ์์! ๐
์์ฆ์ LiveData ๋์ StateFlow๋ SharedFlow๋ฅผ ์ฐ๋ ์ถ์ธ์ผ.
Kotlin Coroutines์ ๋ ์ ์ด์ธ๋ฆฌ๊ฑฐ๋ . ํ์ง๋ง LiveData๋ ์ฌ์ ํ ํ์ญ์ด๊ณ , ํนํ Java ํ๋ก์ ํธ์์๋ LiveData๊ฐ ๋ ํธํด.
LiveData โ Java ์นํ์ , ์๋ช ์ฃผ๊ธฐ ์๋ ๊ด๋ฆฌ
StateFlow โ Kotlin Coroutines ์นํ์ , ๋ ์ ์ฐํ ์ฒ๋ฆฌ
ViewModel๊ณผ LiveData๋ฅผ ํจ๊ป ์ฐ๋ฉด MVVM(Model-View-ViewModel) ์ํคํ
์ฒ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
UI ๋ก์ง๊ณผ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ถ๋ฆฌ๋๋๊น ํ
์คํธ๋ ์ฌ์์ง๊ณ , ์ฝ๋ ์ ์ง๋ณด์๋ ํจ์ฌ ํธํด์ ธ. ๐
๐๏ธ Room: SQLite๋ฅผ ์ธ๊ฐ๋ต๊ฒ ์ฐ๋ ๋ฒ
SQLite ์ง์ ์จ๋ณธ ์ฌ๋? ๐
SQLiteOpenHelper ์์๋ฐ๊ณ , onCreate()์ CREATE TABLE ์ฟผ๋ฆฌ ์ฐ๊ณ , Cursor๋ก ๋ฐ์ดํฐ ์ฝ๊ณ ...
์๊ฐ๋ง ํด๋ ์๊ฐ๋ฝ์ด ์ํ์ค์ง ์์? ๐ญ
Room์ ์ด ๋ชจ๋ ๊ณ ํต์ ์์ ์ฃผ๋ SQLite ์์ ๊ตฌ์ถ๋ ์ถ์ํ ๋ ์ด์ด์ผ!
Room์ ํต์ฌ ๊ตฌ์ฑ์์๋ ๋ฑ 3๊ฐ์ง์ผ:
@Entity ์ด๋
ธํ
์ด์
์ผ๋ก ์ ์ํด.@Dao ์ด๋
ธํ
์ด์
์ฌ์ฉ.@Database ์ด๋
ธํ
์ด์
์ผ๋ก ์ ์ํ๊ณ ์ฑ๊ธํค์ผ๋ก ๊ด๋ฆฌํด.// 1. Entity ์ ์
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "user_name")
val name: String,
val email: String,
val createdAt: Long = System.currentTimeMillis()
)
// 2. DAO ์ ์
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY createdAt DESC")
fun getAllUsers(): Flow<List<User>> // Flow๋ก ๋ฐ์ํ ์ฒ๋ฆฌ!
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: Int)
}
// 3. Database ์ ์
@Database(
entities = [User::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
์ปดํ์ผ ํ์ ๊ฒ์ฆ โ SQL ์ฟผ๋ฆฌ ์ค๋ฅ๋ฅผ ์ปดํ์ผ ์์ ์ ์ก์์ค. ๋ฐํ์์ ์ฑ์ด ํฐ์ง๋ ์ํฉ์ ๋ฐฉ์ง!
LiveData/Flow ์ง์ โ DB ๋ณ๊ฒฝ์ฌํญ์ ์๋์ผ๋ก UI์ ๋ฐ์ํ ์ ์์ด.
Migration ์ง์ โ DB ์คํค๋ง ๋ณ๊ฒฝ ์ ๋ง์ด๊ทธ๋ ์ด์ ์ฝ๋๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌ ๊ฐ๋ฅ.
ํ ์คํธ ์ฉ์ด์ฑ โ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ๋จ์ ํ ์คํธ ๊ฐ๋ฅ.
ํนํ Flow<List<User>>๋ฅผ ๋ฐํํ๋ DAO ๋ฉ์๋๋ ์ง์ง ๊ฐ๋ ฅํด!
๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ณ๊ฒฝ์ด ์๊ธฐ๋ฉด ์๋์ผ๋ก ์ ๋ฐ์ดํฐ๋ฅผ emitํด์ UI๊ฐ ์์์ ์
๋ฐ์ดํธ๋ผ.
์ด๊ฑธ ViewModel์ StateFlow์ ์ฐ๊ฒฐํ๋ฉด ์์ ์๋ํ๋ ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ์ด ์์ฑ๋๋ ๊ฑฐ์ผ! ๐
๐บ๏ธ Navigation Component: ํ๋ฉด ์ ํ์ ์ ์ธ๊ณ
์์ ์ ํ๋๊ทธ๋จผํธ ์ ํ ์ฝ๋ ์ง๋ณธ ์ ์์ด? ๐ค
FragmentManager์ beginTransaction()ํ๊ณ , replace()ํ๊ณ , addToBackStack()ํ๊ณ ...
๋ฐฑ์คํ ๊ด๋ฆฌํ๋ค๊ฐ ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ ๋์์ด ์ด์ํด์ง๊ณ , ํ๋ฉด ์ ํ ์ ๋๋ฉ์ด์
์ถ๊ฐํ๋ ค๋ฉด ๋ ์ฝ๋๊ฐ ๋์ด๋๊ณ .
Navigation Component๋ ์ด ๋ชจ๋ ๊ฑธ ์๊ฐ์ ์ธ ๊ทธ๋ํ๋ก ๊ด๋ฆฌํ๊ฒ ํด์ค! ๐จ
Navigation์ ํต์ฌ ๊ฐ๋
| ๊ฐ๋ | ์ค๋ช |
|---|---|
| NavGraph | ๋ชจ๋ ํ๋ฉด๊ณผ ํ๋ฉด ๊ฐ ์ด๋ ๊ฒฝ๋ก๋ฅผ ์ ์ํ๋ XML ํ์ผ |
| NavHost | NavGraph์ ํ์ฌ ๋ชฉ์ ์ง๋ฅผ ํ์ํ๋ ์ปจํ ์ด๋ (NavHostFragment) |
| NavController | NavHost ๋ด์์ ์ฑ ํ์์ ๊ด๋ฆฌํ๋ ๊ฐ์ฒด |
| Destination | NavGraph์์ ์ด๋ํ ์ ์๋ ๊ฐ ํ๋ฉด (Fragment, Activity, Dialog) |
| Action | Destination ๊ฐ์ ์ด๋ ๊ฒฝ๋ก ์ ์ |
| Safe Args | ํ๋ฉด ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ์ ํ์ ์์ ํ๊ฒ ์ฒ๋ฆฌํ๋ ํ๋ฌ๊ทธ์ธ |
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.HomeFragment"
android:label="ํ">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.DetailFragment"
android:label="์์ธ">
<argument
android:name="itemId"
app:argType="integer"/>
</fragment>
</navigation>
// ํ๋ฉด ์ด๋ (Safe Args ์ฌ์ฉ)
val action = HomeFragmentDirections
.actionHomeToDetail(itemId = 42)
findNavController().navigate(action)
// ๋ค๋ก๊ฐ๊ธฐ
findNavController().navigateUp()
// BottomNavigationView์ ์ฐ๋
val navController = findNavController(R.id.nav_host_fragment)
binding.bottomNav.setupWithNavController(navController)
ํ๋ฉด ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌํ ๋ Bundle์ ์ง์ put/get ํ๋ ๋ฐฉ์์ ํ์ ์ค๋ฅ๊ฐ ๋ฐํ์์ ๋ฐ์ํ ์ ์์ด.
Safe Args๋ NavGraph์ argument ์ ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์ ์์ ํ ์ฝ๋๋ฅผ ์๋ ์์ฑํด์ค.
build.gradle์ ํ๋ฌ๊ทธ์ธ ์ถ๊ฐํ๋ฉด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ!
Navigation Component์ ๋ ๋ค๋ฅธ ๊ฐ์ ์ ๋ฅ๋งํฌ(Deep Link) ์ง์์ด์ผ.
์ธ๋ถ์์ ํน์ ํ๋ฉด์ผ๋ก ๋ฐ๋ก ์ง์
ํ๋ ๊ธฐ๋ฅ์ NavGraph์์ ์ ์ธ์ ์ผ๋ก ์ ์ํ ์ ์์ด.
URL ํจํด ๋งคํ๋ ์ง์ํ๋๊น ์น ๋งํฌ์์ ์ฑ ํน์ ํ๋ฉด์ผ๋ก ์ฐ๊ฒฐํ๋ ๊ฒ๋ ์ฝ๊ฒ ๊ตฌํ ๊ฐ๋ฅํด! ๐
โ๏ธ WorkManager: ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ ์ต๊ฐ์
๋ฐฑ๊ทธ๋ผ์ด๋ ์์
... ์๋๋ก์ด๋ ๊ฐ๋ฐ์์ ์ง์ง ์
๋ชฝ ๊ฐ์ ์์ญ์ด์ผ. ๐ฐ
AsyncTask๋ deprecated๋๊ณ , Service๋ ๋ฐฐํฐ๋ฆฌ ์ก์๋จน๋๋ค๊ณ ์์คํ
์ด ์ฃฝ์ฌ๋ฒ๋ฆฌ๊ณ ,
JobScheduler๋ API 21 ์ด์์์๋ง ๋๊ณ , AlarmManager๋ ๋์ฆ ๋ชจ๋์์ ์ ๋๊ณ ...
์ด ๋ชจ๋ ํผ๋์ WorkManager๊ฐ ํด๊ฒฐํด์ค! ๐ฆธ
API 23+ โ JobScheduler
API 14-22 โ Firebase JobDispatcher ๋๋ AlarmManager + BroadcastReceiver
๊ฐ๋ฐ์๋ ๊ทธ๋ฅ WorkManager API๋ง ์ฐ๋ฉด ๋ผ. ๋ด๋ถ ๊ตฌํ์ ์์์ ์ต์ ํํด์ค! ๐ฏ
WorkManager๋ ๋ณด์ฅ๋ ์คํ(Guaranteed Execution)์ด ํต์ฌ์ด์ผ.
์ฑ์ด ์ข
๋ฃ๋๊ฑฐ๋ ๊ธฐ๊ธฐ๊ฐ ์ฌ์์๋์ด๋ ์์
์ด ์คํ๋๋ ๊ฑธ ๋ณด์ฅํด์ค.
๋จ, ์ฆ๊ฐ์ ์ธ ์คํ์ด ํ์ํ ์์
๋ณด๋ค๋ ์ง์ฐ ๊ฐ๋ฅํ๊ณ ์ ๋ขฐ์ฑ์ด ํ์ํ ์์
์ ์ ํฉํด.
// Worker ํด๋์ค ์ ์
class ImageUploadWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val imageUri = inputData.getString("image_uri")
?: return Result.failure()
return try {
// ์ด๋ฏธ์ง ์
๋ก๋ ๋ก์ง
uploadImage(imageUri)
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry() // ์ต๋ 3๋ฒ ์ฌ์๋
} else {
Result.failure()
}
}
}
}
// WorkRequest ์์ฑ ๋ฐ ์คํ
val uploadWorkRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>()
.setInputData(workDataOf("image_uri" to imageUri))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // ๋คํธ์ํฌ ์ฐ๊ฒฐ ์์๋ง
.setRequiresBatteryNotLow(true) // ๋ฐฐํฐ๋ฆฌ ๋ถ์กฑ ์ ์คํ ์ ํจ
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
// ์์
์ํ ๊ด์ฐฐ
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(this) { workInfo ->
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> showSuccess()
WorkInfo.State.FAILED -> showError()
WorkInfo.State.RUNNING -> showProgress()
else -> {}
}
}
WorkManager์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค
then()์ผ๋ก ์์ฐจ ์คํ, combine()์ผ๋ก ๋ณ๋ ฌ ์คํ ํ ํฉ์น๊ธฐ!PeriodicWorkRequest๋ก 15๋ถ ์ด์ ๊ฐ๊ฒฉ์ ๋ฐ๋ณต ์์
์ค์ ๊ฐ๋ฅ. ๋ฐ์ดํฐ ๋๊ธฐํ์ ๋ฑ!enqueueUniqueWork()๋ก ๊ฐ์ ์์
์ด ์ค๋ณต ์คํ๋๋ ๊ฑธ ๋ฐฉ์งํ ์ ์์ด.setProgress()๋ก ์์
์งํ๋ฅ ์ ์ค์๊ฐ์ผ๋ก UI์ ์ ๋ฌ ๊ฐ๋ฅ!// ์ด๋ฏธ์ง ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ: ์์ถ โ ํํฐ ์ ์ฉ โ ์
๋ก๋
WorkManager.getInstance(context)
.beginWith(compressWorkRequest)
.then(filterWorkRequest)
.then(uploadWorkRequest)
.enqueue()
๐ Hilt: ์์กด์ฑ ์ฃผ์ ์ ์ฝ๊ฒ!
์์กด์ฑ ์ฃผ์
(Dependency Injection, DI)... ์ฒ์ ๋ค์ผ๋ฉด ๋ญ๊ฐ ์ด๋ ค์ ๋ณด์ด์ง? ๐
๊ทผ๋ฐ ๊ฐ๋
์ ๊ฐ๋จํด. "ํ์ํ ๊ฐ์ฒด๋ฅผ ์ง์ ๋ง๋ค์ง ๋ง๊ณ , ์ธ๋ถ์์ ๋ฐ์๋ผ"๋ ๊ฑฐ์ผ.
์๋ฅผ ๋ค์ด Repository๊ฐ ํ์ํ ViewModel์ด ์์ ๋, ViewModel ์์์ Repository๋ฅผ ์ง์ ์์ฑํ๋ ๊ฒ ์๋๋ผ ์ธ๋ถ์์ ์ฃผ์
๋ฐ๋ ๋ฐฉ์์ด์ผ.
Hilt๋ Dagger2 ๊ธฐ๋ฐ์ ์๋๋ก์ด๋ ์ ์ฉ DI ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
Dagger2๊ฐ ๋๋ฌด ๋ณต์กํ๋ค๋ ๋ถ๋ง์ ๋ฐ์ํด์ ๊ตฌ๊ธ์ด ์๋๋ก์ด๋์ ์ต์ ํ๋ ๋ฒ์ ์ผ๋ก ๋ง๋ ๊ฑฐ์ผ.
์ด๋
ธํ
์ด์
๋ช ๊ฐ๋ง ์ถ๊ฐํ๋ฉด DI ์ค์ ์ด ๋๋! ๐
// Application ํด๋์ค์ @HiltAndroidApp ์ถ๊ฐ
@HiltAndroidApp
class MyApplication : Application()
// Activity์ @AndroidEntryPoint ์ถ๊ฐ
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// ViewModel์ ์๋์ผ๋ก ์์กด์ฑ ์ฃผ์
๋จ
private val viewModel: MainViewModel by viewModels()
}
// ViewModel์ @HiltViewModel ์ถ๊ฐ
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
// userRepository๊ฐ ์๋์ผ๋ก ์ฃผ์
๋จ!
}
// Repository ์ ์
class UserRepository @Inject constructor(
private val userDao: UserDao,
private val apiService: ApiService
) {
fun getUsers() = userDao.getAllUsers()
}
// Module๋ก ์ธํฐํ์ด์ค ๋ฐ์ธ๋ฉ
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
// ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ฐ์ฒด ์ ๊ณต
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
๋ณด์ผ๋ฌํ๋ ์ดํธ ๊ฐ์ โ Dagger2 ๋๋น ์ฝ๋๋์ด ๋ํญ ์ค์ด๋ค์ด.
ํ ์คํธ ์ฉ์ด์ฑ โ ์์กด์ฑ์ ์ฝ๊ฒ ๊ต์ฒดํ ์ ์์ด์ ๋จ์ ํ ์คํธ๊ฐ ํธํด์ ธ.
์ปดํ์ผ ํ์ ๊ฒ์ฆ โ ์์กด์ฑ ๊ทธ๋ํ ์ค๋ฅ๋ฅผ ์ปดํ์ผ ์์ ์ ๋ฐ๊ฒฌ ๊ฐ๋ฅ.
ํ์คํ โ ๊ตฌ๊ธ ๊ณต์ ๊ถ์ฅ DI ์๋ฃจ์ ์ด๋ผ ํ ๋ด ์ฝ๋ ์ผ๊ด์ฑ ์ ์ง์ ์ข์.
Hilt๋ฅผ ์ฐ๋ฉด ์ฝ๋ ๊ตฌ์กฐ๊ฐ ํจ์ฌ ๊น๋ํด์ง๊ณ , ํนํ ํ
์คํธ ์ฝ๋ ์์ฑ์ด ์์ฒญ ํธํด์ ธ.
ํ
์คํธํ ๋ ์ค์ Repository ๋์ Mock Repository๋ฅผ ์ฃผ์
ํ๋ฉด ๋๋๊น, ๋คํธ์ํฌ ์์ด๋ ViewModel ํ
์คํธ๊ฐ ๊ฐ๋ฅํด! ๐งช
๐จ Jetpack Compose: UI ๊ฐ๋ฐ์ ํ๋ช
์, ์ด์ ์ง์ง ํซํ ๋
์์ ์๊ฐํ ์๊ฐ์ด์ผ! ๐ฅ
Jetpack Compose๋ 2021๋
์ ์์ ๋ฒ์ ์ด ์ถ์๋ ์๋๋ก์ด๋์ ์๋ก์ด UI ํดํท์ด์ผ.
๊ธฐ์กด์ XML ๊ธฐ๋ฐ UI ๊ฐ๋ฐ ๋ฐฉ์์ ์์ ํ ๋ค์ง์ด๋ฒ๋ฆฐ ํ๋ช
์ ์ธ ๋ณํ์ผ!
๊ธฐ์กด ๋ฐฉ์์ XML๋ก ๋ ์ด์์ ์ ์ํ๊ณ , ์ฝ๋์์ findViewById()๋ ViewBinding์ผ๋ก ๋ทฐ๋ฅผ ์ฐพ์์ ์กฐ์ํ๋ ๋ฐฉ์์ด์์์.
Compose๋ ์ ์ธํ(Declarative) UI ๋ฐฉ์์ด์ผ. "์ด ์ํ์ผ ๋ UI๊ฐ ์ด๋ ๊ฒ ๋ณด์ฌ์ผ ํด"๋ผ๊ณ ์ ์ธํ๋ฉด, ์ํ๊ฐ ๋ฐ๋ ๋ UI๊ฐ ์์์ ์
๋ฐ์ดํธ๋ผ!
๋ช ๋ นํ (๊ธฐ์กด XML ๋ฐฉ์)
"๋ฒํผ ์ฐพ์์ โ ํ ์คํธ ๋ณ๊ฒฝํด โ ์์ ๋ฐ๊ฟ โ ๋ณด์ด๊ฒ ํด"
โ ์ํ ๋ณํ๋ง๋ค ์ง์ ๋ทฐ๋ฅผ ์กฐ์ํด์ผ ํจ
์ ์ธํ (Compose ๋ฐฉ์)
"isLoading์ด true๋ฉด ๋ก๋ฉ ์คํผ๋ ๋ณด์ฌ์ค, false๋ฉด ์ฝํ ์ธ ๋ณด์ฌ์ค"
โ ์ํ๋ง ๋ฐ๊พธ๋ฉด UI๊ฐ ์์์ ์ฌ๊ตฌ์ฑ๋จ
// Composable ํจ์ ์ ์
@Composable
fun UserCard(
user: User,
onFollowClick: (User) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
elevation = CardDefaults.cardElevation(4.dp),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// ํ๋กํ ์ด๋ฏธ์ง
AsyncImage(
model = user.profileImageUrl,
contentDescription = "ํ๋กํ ์ด๋ฏธ์ง",
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
// ์ฌ์ฉ์ ์ ๋ณด
Column(modifier = Modifier.weight(1f)) {
Text(
text = user.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = user.bio,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// ํ๋ก์ฐ ๋ฒํผ
Button(
onClick = { onFollowClick(user) },
shape = RoundedCornerShape(20.dp)
) {
Text("ํ๋ก์ฐ")
}
}
}
}
// ์ํ ๊ด๋ฆฌ
@Composable
fun UserListScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> LazyColumn {
items(uiState.users) { user ->
UserCard(
user = user,
onFollowClick = viewModel::followUser
)
}
}
is UiState.Error -> ErrorScreen(message = uiState.message)
}
}
Compose๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ํด๋น Composable ํจ์๋ฅผ ๋ค์ ์คํํด(์ด๊ฑธ Recomposition์ด๋ผ๊ณ ํด).
์ด๋ ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ํจ์จ์ ์ผ๋ก ์ ๋ฐ์ดํธํ๋ ๊ฒ ํต์ฌ์ด์ผ.
remember์ rememberSaveable๋ก ์ํ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๊ด๋ฆฌํ๋ ๊ฒ ์ค์ํด!
Compose์ ์ฃผ์ ์ฅ์
@Preview ์ด๋
ธํ
์ด์
์ผ๋ก Android Studio์์ ์ค์๊ฐ UI ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ๋ฅ. ์๋ฎฌ๋ ์ดํฐ ์์ด๋ ํ์ธ OK!animate*AsState, AnimatedVisibility ๋ฑ์ผ๋ก ๋ณต์กํ ์ ๋๋ฉ์ด์
์ ์ฝ๊ฒ ๊ตฌํ ๊ฐ๋ฅ.๊ธฐ์กด XML ๊ธฐ๋ฐ ํ๋ก์ ํธ์ Compose๋ฅผ ์ ์ง์ ์ผ๋ก ๋์ ํ ์ ์์ด.
ComposeView๋ AndroidView๋ก ์ํธ ์ด์ฉ ๊ฐ๋ฅ.ํ์ง๋ง ํ ์ ์ฒด๊ฐ Compose์ ์ต์ํด์ง๋ ๋ฐ ์๊ฐ์ด ํ์ํด. ๋ฌ๋ ์ปค๋ธ๊ฐ ์์ผ๋ ์ถฉ๋ถํ ํ์ต ์๊ฐ์ ํ๋ณดํ๋ ๊ฒ ์ข์!
๐พ DataStore: SharedPreferences์ ์งํํ
SharedPreferences ์จ๋ณธ ์ฌ๋? ๐
๊ฐ๋จํ ์ค์ ๊ฐ ์ ์ฅํ ๋ ํธํ๊ธด ํ๋ฐ... ๋ฉ์ธ ์ค๋ ๋์์ I/O ์์
์ด ๋ฐ์ํ ์ ์๊ณ , ํ์
์์ ์ฑ๋ ์๊ณ , ํธ๋์ญ์
์ง์๋ ์ ๋๊ณ .
DataStore๋ ์ด ๋ชจ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ SharedPreferences์ ํ๋์ ๋์์ด์ผ! ๐
DataStore๋ ๋ ๊ฐ์ง ๊ตฌํ์ฒด๊ฐ ์์ด:
| ๊ตฌ๋ถ | Preferences DataStore | Proto DataStore |
|---|---|---|
| ๋ฐ์ดํฐ ํ์ | ํค-๊ฐ ์ | Protocol Buffers (ํ์ ์ ์) |
| ํ์ ์์ ์ฑ | ์ ํ์ | ์์ ํ ํ์ ์์ ์ฑ |
| ์คํค๋ง ์ ์ | ๋ถํ์ | .proto ํ์ผ ํ์ |
| ์ฌ์ฉ ๋์ด๋ | ์ฌ์ | ์ค๊ฐ |
| ๊ถ์ฅ ์ฉ๋ | ๊ฐ๋จํ ์ค์ ๊ฐ | ๋ณต์กํ ๊ตฌ์กฐํ ๋ฐ์ดํฐ |
// DataStore ์์ฑ (Application ๋ ๋ฒจ์์ ์ฑ๊ธํค์ผ๋ก)
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "user_settings"
)
// ํค ์ ์
object PreferencesKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val USER_NAME = stringPreferencesKey("user_name")
val FONT_SIZE = intPreferencesKey("font_size")
}
// Repository์์ DataStore ์ฌ์ฉ
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
// ๋ฐ์ดํฐ ์ฝ๊ธฐ (Flow๋ก ๋ฐํ - ๋ณ๊ฒฝ ์ ์๋ emit)
val darkModeFlow: Flow<Boolean> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[PreferencesKeys.DARK_MODE] ?: false
}
// ๋ฐ์ดํฐ ์ฐ๊ธฐ (suspend ํจ์ - ์ฝ๋ฃจํด์์ ํธ์ถ)
suspend fun setDarkMode(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.DARK_MODE] = enabled
}
}
suspend fun setUserName(name: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.USER_NAME] = name
}
}
}
๋น๋๊ธฐ ์ฒ๋ฆฌ โ DataStore๋ Kotlin Coroutines์ Flow๋ฅผ ์ฌ์ฉํด์ ์์ ๋น๋๊ธฐ๋ก ๋์ํด. ๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน ์์!
์ค๋ฅ ์ฒ๋ฆฌ โ ๋ฐ์ดํฐ ์ฝ๊ธฐ/์ฐ๊ธฐ ์คํจ๋ฅผ Flow์ catch ์ฐ์ฐ์๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ ๊ฐ๋ฅ.
ํธ๋์ญ์ โ
edit ๋ธ๋ก ๋ด์ ๋ณ๊ฒฝ์ฌํญ์ ์์์ ์ผ๋ก ์ฒ๋ฆฌ๋ผ. ๋ถ๋ถ ์
๋ฐ์ดํธ๋ก ์ธํ ๋ฐ์ดํฐ ๋ถ์ผ์น ์์!
๐ Paging 3: ๋ฌดํ ์คํฌ๋กค์ ์ฐ์ํ๊ฒ
SNS ํผ๋๋ ์ํ ๋ชฉ๋ก์ฒ๋ผ ๋ฐ์ดํฐ๊ฐ ๋ง์ ๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํด? ๐ค
ํ ๋ฒ์ ๋ค ๋ถ๋ฌ์ค๋ฉด ๋ฉ๋ชจ๋ฆฌ ํฐ์ง๊ณ , ๋ก๋ฉ ์๊ฐ๋ ์์ฒญ ๊ฑธ๋ฆฌ์์.
๊ทธ๋์ ํ์ด์ง๋ค์ด์
(Pagination)์ด ํ์ํ๋ฐ, ์ด๊ฑธ ์ง์ ๊ตฌํํ๋ฉด ์๊ฐ๋ณด๋ค ๋ณต์กํด.
Paging 3๋ ์ด ๋ณต์กํ ํ์ด์ง๋ค์ด์
๋ก์ง์ ๊น๋ํ๊ฒ ์ฒ๋ฆฌํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ!
// PagingSource ์ ์
class UserPagingSource(
private val apiService: ApiService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val page = params.key ?: 1
return try {
val response = apiService.getUsers(
page = page,
pageSize = params.loadSize
)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.users.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
// Repository์์ Pager ์์ฑ
class UserRepository(private val apiService: ApiService) {
fun getUsersPagingData(): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
prefetchDistance = 5 // 5๊ฐ ๋จ์ผ๋ฉด ๋ฏธ๋ฆฌ ๋ก๋
),
pagingSourceFactory = { UserPagingSource(apiService) }
).flow
}
}
// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val users = repository.getUsersPagingData()
.cachedIn(viewModelScope) // ํ๋ฉด ํ์ ์ ์บ์ ์ ์ง!
}
// Fragment์์ PagingDataAdapter ์ฌ์ฉ
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
// ViewHolder ์์ฑ
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
companion object {
val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User) =
oldItem == newItem
}
}
}
LoadStateAdapter๋ฅผ ์ฌ์ฉํ๋ฉด ๋ก๋ฉ ์ค, ์ค๋ฅ ์ํ๋ฅผ ๋ฆฌ์คํธ ์๋จ/ํ๋จ์ ์๋์ผ๋ก ํ์ํ ์ ์์ด.
RemoteMediator๋ฅผ ์ฌ์ฉํ๋ฉด ๋คํธ์ํฌ ๋ฐ์ดํฐ๋ฅผ Room์ ์บ์ฑํ๋ฉด์ ์คํ๋ผ์ธ ์ง์๋ ๊ตฌํ ๊ฐ๋ฅ!
Compose์์๋
collectAsLazyPagingItems()๋ก ๋ ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์ ์์ด.
๐ธ CameraX: ์นด๋ฉ๋ผ ๊ฐ๋ฐ์ ๊ตฌ์์
์๋๋ก์ด๋ ์นด๋ฉ๋ผ API... ์ง์ง ์
๋ช
๋์ง. ๐ค
Camera1 API๋ ๋๋ฌด ๊ตฌ์์ด๊ณ , Camera2 API๋ ๋๋ฌด ๋ณต์กํ๊ณ , ๊ธฐ๊ธฐ๋ง๋ค ๋์์ด ๋ฌ๋ผ์ ํ
์คํธํ๋ค๊ฐ ๋จธ๋ฆฌ ๋น ์ง๊ณ ...
CameraX๋ ์ด ๋ชจ๋ ๋ณต์กํจ์ ์ถ์ํํด์ ์ผ๊ด๋ ์นด๋ฉ๋ผ ๊ฒฝํ์ ์ ๊ณตํด์ค!
CameraX๋ ๋ด๋ถ์ ์ผ๋ก Camera2 API๋ฅผ ์ฌ์ฉํ์ง๋ง, ๊ฐ๋ฐ์์๊ฒ๋ ํจ์ฌ ๊ฐ๋จํ API๋ฅผ ์ ๊ณตํด.
๊ทธ๋ฆฌ๊ณ ๊ธฐ๊ธฐ ํธํ์ฑ ๋ฌธ์ ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค์ ๋ค์ํ ์๋๋ก์ด๋ ๊ธฐ๊ธฐ์์ ์ผ๊ด๋๊ฒ ๋์ํด.
class CameraFragment : Fragment() {
private lateinit var cameraExecutor: ExecutorService
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cameraExecutor = Executors.newSingleThreadExecutor()
startCamera()
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// Preview UseCase
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
// ImageCapture UseCase
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// ImageAnalysis UseCase (์ค์๊ฐ ๋ถ์)
val imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor) { imageProxy ->
analyzeImage(imageProxy)
}
}
// ์นด๋ฉ๋ผ ๋ฐ์ธ๋ฉ
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
viewLifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
imageAnalyzer
)
} catch (e: Exception) {
Log.e("CameraX", "์นด๋ฉ๋ผ ๋ฐ์ธ๋ฉ ์คํจ", e)
}
}, ContextCompat.getMainExecutor(requireContext()))
}
override fun onDestroyView() {
super.onDestroyView()
cameraExecutor.shutdown()
}
}
Preview โ ์นด๋ฉ๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ. PreviewView์ ์ค์๊ฐ ์์ ํ์.
ImageCapture โ ์ฌ์ง ์ดฌ์. ๊ณ ํ์ง ์ด๋ฏธ์ง๋ฅผ ํ์ผ์ด๋ ๋ฉ๋ชจ๋ฆฌ ๋ฒํผ๋ก ์ ์ฅ.
ImageAnalysis โ ์ค์๊ฐ ์ด๋ฏธ์ง ๋ถ์. ML Kit์ ์ฐ๋ํด์ QR์ฝ๋ ์ธ์, ์ผ๊ตด ๊ฐ์ง ๋ฑ ๊ตฌํ ๊ฐ๋ฅ!
โจ Android KTX: Kotlin์ ํ์ ์ต๋๋ก!
KTX๋ Kotlin Extensions์ ์ฝ์์ผ.
์๋๋ก์ด๋ API๋ฅผ Kotlin ์นํ์ ์ผ๋ก ๋ง๋ค์ด์ฃผ๋ ํ์ฅ ํจ์ ๋ชจ์์ด์ผ.
์ฝ๋๊ฐ ์ผ๋ง๋ ๊ฐ๊ฒฐํด์ง๋์ง ์ง์ ๋น๊ตํด๋ณผ๊ฒ! ๐
// โ KTX ์์ด
val sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("key", "value")
editor.apply()
// โ
KTX ์ฌ์ฉ
context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit {
putString("key", "value")
}
// โ KTX ์์ด - Fragment ํธ๋์ญ์
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.addToBackStack(null)
.commit()
// โ
KTX ์ฌ์ฉ
supportFragmentManager.commit {
replace(R.id.container, fragment)
addToBackStack(null)
}
// โ KTX ์์ด - Bundle ์์ฑ
val bundle = Bundle()
bundle.putString("name", "ํ๊ธธ๋")
bundle.putInt("age", 25)
// โ
KTX ์ฌ์ฉ
val bundle = bundleOf("name" to "ํ๊ธธ๋", "age" to 25)
// โ KTX ์์ด - View ๊ฐ์์ฑ
view.visibility = View.VISIBLE
view.visibility = View.GONE
// โ
KTX ์ฌ์ฉ
view.isVisible = true
view.isGone = true
// โ KTX ์์ด - ์์ ๊ฐ์ ธ์ค๊ธฐ
ContextCompat.getColor(context, R.color.primary)
// โ
KTX ์ฌ์ฉ
context.getColor(R.color.primary) // API 23+์์ ์๋ ์ฒ๋ฆฌ
core-ktx โ ํต์ฌ ์๋๋ก์ด๋ API ํ์ฅ
fragment-ktx โ Fragment ๊ด๋ จ ํ์ฅ (by viewModels() ๋ฑ)
lifecycle-ktx โ Lifecycle ๊ด๋ จ ํ์ฅ (lifecycleScope ๋ฑ)
room-ktx โ Room ์ฝ๋ฃจํด ์ง์
navigation-ktx โ Navigation ํ์ฅ
work-runtime-ktx โ WorkManager ์ฝ๋ฃจํด ์ง์
KTX๋ฅผ ์ฐ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๊ฐ๊ฒฐํ๊ณ ์ฝ๊ธฐ ์ฌ์์ ธ.
ํนํ lifecycleScope์ viewModelScope๋ ์ฝ๋ฃจํด์ ์๋ช
์ฃผ๊ธฐ์ ๋ง๊ฒ ์๋์ผ๋ก ์ทจ์ํด์ค์ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฑฑ์ ์ด ์์ด! ๐
๐๏ธ ์ค์ ! Jetpack์ผ๋ก ์์ฑํ๋ ์ฑ ์ํคํ ์ฒ
์, ์ด์ ๋ฐฐ์ด ๊ฒ๋ค์ ์ข
ํฉํด์ ์ค์ ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ๋ณด์ฌ์ค๊ฒ! ๐ฏ
๊ตฌ๊ธ์ด ๊ถ์ฅํ๋ Clean Architecture + MVVM ํจํด์ Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋
น์ฌ๋ธ ๊ตฌ์กฐ์ผ.
app/src/main/java/com/example/myapp/โโโ data/ โ ๋ฐ์ดํฐ ๋ ์ด์ด
โ โโโ local/ โ Room DB, DataStore
โ โโโ remote/ โ Retrofit API ์๋น์ค
โ โโโ repository/ โ Repository ๊ตฌํ์ฒด
โโโ domain/ โ ๋๋ฉ์ธ ๋ ์ด์ด (์ ํ์ )
โ โโโ model/ โ ๋๋ฉ์ธ ๋ชจ๋ธ
โ โโโ usecase/ โ ๋น์ฆ๋์ค ๋ก์ง
โโโ presentation/ โ ํ๋ ์ ํ ์ด์ ๋ ์ด์ด
โ โโโ home/ โ ํ ํ๋ฉด (Fragment + ViewModel)
โ โโโ detail/ โ ์์ธ ํ๋ฉด
โ โโโ common/ โ ๊ณตํต UI ์ปดํฌ๋ํธ
โโโ di/ โ Hilt ๋ชจ๋
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getUsersUseCase: GetUsersUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// UI ์ํ๋ฅผ ํ๋์ sealed class๋ก ๊ด๋ฆฌ
sealed class UiState {
object Loading : UiState()
data class Success(val users: List<User>) : UiState()
data class Error(val message: String) : UiState()
}
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// ๊ฒ์์ด ์ํ
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
getUsersUseCase()
.catch { e ->
_uiState.value = UiState.Error(e.message ?: "์ ์ ์๋ ์ค๋ฅ")
}
.collect { users ->
_uiState.value = UiState.Success(users)
}
}
}
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
fun refreshUsers() {
loadUsers()
}
}
์ด ๊ตฌ์กฐ์ ํต์ฌ์ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ(UDF, Unidirectional Data Flow)์ด์ผ.
UI๋ ์ด๋ฒคํธ๋ฅผ ViewModel์ ์ ๋ฌํ๊ณ , ViewModel์ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๊ณ , UI๋ ์ํ๋ฅผ ๊ด์ฐฐํด์ ํ๋ฉด์ ์
๋ฐ์ดํธํด.
๋ฐ์ดํฐ๊ฐ ํ ๋ฐฉํฅ์ผ๋ก๋ง ํ๋ฅด๋๊น ๋๋ฒ๊น
์ด ์ฝ๊ณ , ์์ธก ๊ฐ๋ฅํ ๋์์ ๋ณด์ฅํ ์ ์์ด! ๐ฏ
๐ฏ Jetpack ๋์ ์ค์ ํ & ์ฃผ์์ฌํญ
์, ์ด์ ์ค์ ๋ก Jetpack์ ํ๋ก์ ํธ์ ๋์ ํ ๋ ์์๋๋ฉด ์ข์ ํ๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ! ๐ช
โ ๋ฒ์ ๊ด๋ฆฌ: BOM(Bill of Materials) ํ์ฉ
Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์๋ก ์์กด์ฑ์ด ์์ด์ ๋ฒ์ ์ถฉ๋์ด ์๊ธธ ์ ์์ด.
Compose BOM์ ์ฌ์ฉํ๋ฉด ๋ชจ๋ Compose ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ ์ ํ ๋ฒ์ ๊ด๋ฆฌํ ์ ์์ด!
// build.gradle.kts (app)
dependencies {
// Compose BOM - ๋ฒ์ ํ๋๋ก ๋ชจ๋ Compose ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ด๋ฆฌ
val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
implementation(composeBom)
// ๋ฒ์ ๋ช
์ ๋ถํ์!
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
// Jetpack ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1") // KSP ์ฌ์ฉ ๊ถ์ฅ!
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.google.dagger:hilt-android:2.51")
ksp("com.google.dagger:hilt-compiler:2.51")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.paging:paging-runtime-ktx:3.2.1")
}
Room, Hilt ๋ฑ ์ด๋ ธํ ์ด์ ํ๋ก์ธ์๋ฅผ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ KAPT ๋์ KSP(Kotlin Symbol Processing)๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์.
KSP๋ KAPT๋ณด๋ค ๋น๋ ์๋๊ฐ ์ต๋ 2๋ฐฐ ๋น ๋ฅด๊ณ , Kotlin ์นํ์ ์ด์ผ.
build.gradle.kts์์ kapt ๋์ ksp๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ!
๐ ์ ์ง์ ๋์ ์ ๋ต
๐ ๊ณต์ ํ์ต ๋ฆฌ์์ค
developer.android.com/jetpack โ Jetpack ๊ณต์ ๋ฌธ์
developer.android.com/codelabs โ ๋จ๊ณ๋ณ ์ค์ต ์ฝ๋๋ฉ
github.com/android/architecture-samples โ ๊ณต์ ์ํคํ ์ฒ ์ํ
github.com/android/nowinandroid โ ์ค์ ์ฑ ์์ค์ ์์ฑ๋ ๋์ ์ํ ์ฑ
Compose, Hilt, Room, Navigation, WorkManager ๋ฑ ์ต์ Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋ชจ๋ ์ ์ฉ๋์ด ์์ด์ ์ค์ ์ฝ๋๋ฅผ ์ฐธ๊ณ ํ๊ธฐ์ ์ต๊ณ ์ผ!
โก Jetpack์ผ๋ก ์ฑ ์ฑ๋ฅ๊น์ง ์ก๊ธฐ
์์ฐ์ฑ๋ง ์ฌ๋ฆฌ๋ ๊ฒ ์๋์ผ! Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์ ๋๋ก ํ์ฉํ๋ฉด ์ฑ ์ฑ๋ฅ๋ ํฌ๊ฒ ํฅ์๋ผ. ๐
๐ Benchmark ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Jetpack Benchmark๋ ์ฝ๋ ์ฑ๋ฅ์ ์ ํํ๊ฒ ์ธก์ ํ ์ ์๋ ๋๊ตฌ์ผ.
์ผ๋ฐ JUnit ํ
์คํธ์ ๋ฌ๋ฆฌ JIT ์ปดํ์ผ, ๊ฐ๋น์ง ์ปฌ๋ ์
๋ฑ์ ์ํฅ์ ์ ๊ฑฐํ๊ณ ์ ํํ ์ธก์ ๊ฐ์ ์ ๊ณตํด.
@RunWith(AndroidJUnit4::class)
class MyBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun benchmarkSomeWork() {
benchmarkRule.measureRepeated {
// ์ฑ๋ฅ ์ธก์ ํ ์ฝ๋
doSomeExpensiveWork()
}
}
}
๐ Compose ์ฑ๋ฅ ์ต์ ํ ํ
1. ๋๋ค ์์ ์ฑ โ ๋๋ค๋ฅผ
remember๋ก ๊ฐ์ธ๊ฑฐ๋ ํด๋์ค ๋ฉ์๋๋ก ์ ๋ฌํด์ ๋ถํ์ํ recomposition ๋ฐฉ์ง.2. ์์ ์ ์ธ ํ์ ์ฌ์ฉ โ
@Stable, @Immutable ์ด๋
ธํ
์ด์
์ผ๋ก Compose์๊ฒ ํ์
์์ ์ฑ ์๋ ค์ฃผ๊ธฐ.3. LazyList ์ต์ ํ โ
key ํ๋ผ๋ฏธํฐ๋ฅผ ํญ์ ์ง์ ํด์ ์์ดํ
์ฌ์ฌ์ฉ ํจ์จ ๋์ด๊ธฐ.4. derivedStateOf โ ๋ค๋ฅธ ์ํ์์ ํ์๋ ์ํ๋
derivedStateOf๋ก ๊ฐ์ธ์ ๋ถํ์ํ recomposition ๋ฐฉ์ง.
// โ ๋งค๋ฒ ์๋ก์ด ๋๋ค ์์ฑ - ๋ถํ์ํ recomposition ์ ๋ฐ
@Composable
fun UserList(users: List<User>, onUserClick: (User) -> Unit) {
LazyColumn {
items(users) { user ->
UserItem(
user = user,
onClick = { onUserClick(user) } // ๋งค๋ฒ ์ ๋๋ค ์์ฑ!
)
}
}
}
// โ
key ํ๋ผ๋ฏธํฐ + ์์ ์ ์ธ ๋๋ค ์ ๋ฌ
@Composable
fun UserList(users: List<User>, onUserClick: (User) -> Unit) {
LazyColumn {
items(
items = users,
key = { user -> user.id } // ๊ณ ์ ํค ์ง์ !
) { user ->
UserItem(
user = user,
onClick = onUserClick // ๋๋ค ์ง์ ์ ๋ฌ
)
}
}
}
// derivedStateOf ํ์ฉ
@Composable
fun SearchScreen(users: List<User>, query: String) {
// query๋ users๊ฐ ๋ฐ๋ ๋๋ง ์ฌ๊ณ์ฐ
val filteredUsers by remember(users, query) {
derivedStateOf {
users.filter { it.name.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filteredUsers, key = { it.id }) { user ->
UserItem(user = user)
}
}
}
๐๏ธ Room ์ฑ๋ฅ ์ต์ ํ
@Index ์ด๋
ธํ
์ด์
์ผ๋ก ์ธ๋ฑ์ค ์ถ๊ฐ. ์ฟผ๋ฆฌ ์๋๊ฐ ํฌ๊ฒ ํฅ์๋ผ.Flow๋ก ๋ฐํ. ๋ถํ์ํ ํด๋ง ์์ด ํจ์จ์ ์ธ ์
๋ฐ์ดํธ ๊ฐ๋ฅ.@Transaction์ผ๋ก ๋ฌถ์ผ๋ฉด ์ฑ๋ฅ๊ณผ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ชจ๋ ํฅ์.SELECT * ๋์ ํ์ํ ์ปฌ๋ผ๋ง ์ ํํ๋ ์ฟผ๋ฆฌ ์์ฑ. ํนํ ๋์ฉ๋ ๋ฐ์ดํฐ์์ ํจ๊ณผ์ .๐งช Jetpack์ผ๋ก ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
ํ
์คํธ ์ฝ๋... ๊ท์ฐฎ๋ค๊ณ ์ ์ฐ๋ ์ฌ๋ ๋ง์ง? ๐
๊ทผ๋ฐ Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ํ
์คํธ๋ฅผ ์ ๋ง ์ฝ๊ฒ ๋ง๋ค์ด์ค.
ํนํ ViewModel, Room, WorkManager๋ ์ ์ฉ ํ
์คํธ ๋๊ตฌ๊ฐ ์์ด!
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule() // ํ
์คํธ์ฉ ๋์คํจ์ฒ
private lateinit var viewModel: HomeViewModel
private val fakeRepository = FakeUserRepository() // ๊ฐ์ง Repository
@Before
fun setup() {
viewModel = HomeViewModel(fakeRepository)
}
@Test
fun `์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋ ์ฑ๊ณต ์ Success ์ํ`() = runTest {
// Given
fakeRepository.setUsers(listOf(
User(id = 1, name = "ํ๊ธธ๋"),
User(id = 2, name = "๊น์ฒ ์")
))
// When
viewModel.loadUsers()
// Then
val state = viewModel.uiState.value
assertTrue(state is HomeViewModel.UiState.Success)
assertEquals(2, (state as HomeViewModel.UiState.Success).users.size)
}
@Test
fun `๋คํธ์ํฌ ์ค๋ฅ ์ Error ์ํ`() = runTest {
// Given
fakeRepository.setShouldThrowError(true)
// When
viewModel.loadUsers()
// Then
assertTrue(viewModel.uiState.value is HomeViewModel.UiState.Error)
}
}
// Room ์ธ๋ฉ๋ชจ๋ฆฌ DB ํ
์คํธ
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).allowMainThreadQueries().build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertAndGetUser() = runTest {
val user = User(name = "ํ
์คํธ ์ ์ ", email = "test@test.com")
userDao.insertUser(user)
val users = userDao.getAllUsers().first()
assertEquals(1, users.size)
assertEquals("ํ
์คํธ ์ ์ ", users[0].name)
}
}
androidx.test โ ์๋๋ก์ด๋ ํ ์คํธ ๊ธฐ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
androidx.test.espresso โ UI ํ ์คํธ ์๋ํ
androidx.compose.ui.test โ Compose UI ํ ์คํธ
kotlinx-coroutines-test โ ์ฝ๋ฃจํด ํ ์คํธ ์ ํธ๋ฆฌํฐ
androidx.work.testing โ WorkManager ํ ์คํธ
hilt-android-testing โ Hilt ํ ์คํธ ์ง์
ํนํ ์ฌ๋ฅ๋ท์์ ์๋๋ก์ด๋ ๊ฐ๋ฐ ํ๋ฆฌ๋์๋ก ํ๋ํ๋ค๋ฉด, ํ
์คํธ ์ฝ๋ ์์ฑ ๋ฅ๋ ฅ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ขฐ๋ฅผ ์ค ์ ์๋ ์ค์ํ ์ญ๋์ด์ผ.
Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ํ
์คํธ๋ฅผ ์ฝ๊ฒ ๋ง๋ค์ด์ฃผ๋๊น, ๊ผญ ํ
์คํธ ์ฝ๋ ์์ฑ ์ต๊ด์ ๋ค์ฌ๋ด! ๐ช
๐ฎ 2024๋ Jetpack ์ต์ ํธ๋ ๋
Jetpack์ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด! ์ต์ ํธ๋ ๋๋ฅผ ์์๋๋ฉด ๋ฏธ๋ ์ค๋น๋ ๋๊ณ , ๊ธฐ์ ๋ฉด์ ์์๋ ๋น๋ ์ ์์ด. โจ
1. Compose Multiplatform ํ์ฅ
JetBrains์ ๊ตฌ๊ธ์ด ํ๋ ฅํด์ Compose๋ฅผ iOS, ๋ฐ์คํฌํฑ, ์น๊น์ง ํ์ฅํ๊ณ ์์ด.
Kotlin Multiplatform Mobile(KMM)๊ณผ ๊ฒฐํฉํ๋ฉด ํ๋์ ์ฝ๋๋ฒ ์ด์ค๋ก ์๋๋ก์ด๋์ iOS ์ฑ์ ๋์์ ๊ฐ๋ฐํ ์ ์์ด!
์์ง ์์ ํ ์์ ํ๋์ง ์์์ง๋ง, 2024๋
์ ๋น ๋ฅด๊ฒ ์ฑ์ํด์ง๊ณ ์์ด. ์ฃผ๋ชฉํด์ผ ํ ๊ธฐ์ ์ด์ผ! ๐
2. Credential Manager
๊ธฐ์กด์ ๋ถ์ฐ๋ ์ธ์ฆ API๋ค(SmartLock, FIDO2, Google Sign-In)์ ํตํฉํ ์๋ก์ด ์ธ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
ํจ์คํค(Passkey) ์ง์์ด ํต์ฌ์ด์ผ. ๋น๋ฐ๋ฒํธ ์๋ ์ธ์ฆ์ ๋ฏธ๋! ๐
3. Health Connect
๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๊ณต์ ํ๊ณ ๊ด๋ฆฌํ๋ ํ๋ซํผ์ด์ผ.
ํผํธ๋์ค ์ฑ, ๊ฑด๊ฐ ๋ชจ๋ํฐ๋ง ์ฑ ๊ฐ๋ฐํ ๋ ํ์๊ฐ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
4. Macrobenchmark
์ฑ ์์ ์๊ฐ, ์คํฌ๋กค ์ฑ๋ฅ ๋ฑ ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ๊น์ด ์ฑ๋ฅ์ ์ธก์ ํ๋ ๋๊ตฌ์ผ.
Baseline Profiles์ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ฑ ์์ ์๋๋ฅผ ์ต๋ 40%๊น์ง ๊ฐ์ ํ ์ ์์ด! โก
// Macrobenchmark๋ก Baseline Profile ์์ฑ
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect("com.example.myapp") {
// ์ฑ ์์ ๋ฐ ์ฃผ์ ์ฌ์ฉ์ ์ฌ์ ์๋ฎฌ๋ ์ด์
pressHome()
startActivityAndWait()
// ์ฃผ์ ํ๋ฉด ํ์
device.findObject(By.text("ํ")).click()
device.waitForIdle()
}
}
}
Compose 1.6+ โ ์ฑ๋ฅ ๊ฐ์ , ์๋ก์ด ์ ๋๋ฉ์ด์ API, ์ ๊ทผ์ฑ ํฅ์
Room 2.6+ โ KSP ์์ ์ง์, ๋ฉํฐ๋งต ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ง์
Lifecycle 2.7+ โ SavedStateHandle ๊ฐ์ , collectAsStateWithLifecycle ์์ ํ
Navigation 2.7+ โ Compose Navigation ๊ฐ์ , ํ์ ์์ ๋ผ์ฐํ
Hilt 2.50+ โ KSP ์์ ์ง์, ์ฑ๋ฅ ๊ฐ์
๐ ๋ง๋ฌด๋ฆฌ: Jetpack์ผ๋ก ๋ ๋์ ๊ฐ๋ฐ์ ๋๊ธฐ
์, ์ค๋ ์ ๋ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค์ด! ๐
Jetpack์ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์ ๋ฆฌํด๋ณด๋ฉด:
๊ฐ๋ฐ ์๋ ํฅ์ โ ๋ฐ๋ณต์ ์ธ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋ ๊ฐ์๋ก ํต์ฌ ๊ธฐ๋ฅ ๊ฐ๋ฐ์ ์ง์ค ๊ฐ๋ฅ
๋ฒ๊ทธ ๊ฐ์ โ ์ปดํ์ผ ํ์ ๊ฒ์ฆ, ํ์ ์์ ์ฑ์ผ๋ก ๋ฐํ์ ์ค๋ฅ ์ฌ์ ๋ฐฉ์ง
์ ์ง๋ณด์์ฑ ํฅ์ โ ํ์คํ๋ ์ํคํ ์ฒ๋ก ํ ํ์ ๊ณผ ์ฝ๋ ์ดํด๋ ํฅ์
ํ ์คํธ ์ฉ์ด์ฑ โ ์์กด์ฑ ์ฃผ์ , ์ธ๋ฉ๋ชจ๋ฆฌ DB ๋ฑ์ผ๋ก ๋จ์ ํ ์คํธ ์์ฑ์ด ์ฌ์์ง
์ฑ๋ฅ ์ต์ ํ โ ์๋ช ์ฃผ๊ธฐ ์ธ์, ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ก ์ฑ ์ฑ๋ฅ ํฅ์
์๋๋ก์ด๋ ๊ฐ๋ฐ์ ๊ณ์ ๋ฐ์ ํ๊ณ ์๊ณ , Jetpack๋ ํจ๊ป ์งํํ๊ณ ์์ด.
์ฒ์์๋ ๋ฐฐ์ธ ๊ฒ ๋ง์์ ๋ถ๋ด์ค๋ฌ์ธ ์ ์์ง๋ง, ํ๋์ฉ ์ตํ๊ฐ๋ค ๋ณด๋ฉด ์ด๋ ์๊ฐ "์ด๊ฒ ์์ผ๋ฉด ์ด๋ป๊ฒ ๊ฐ๋ฐํ์ง?"๋ผ๋ ์๊ฐ์ด ๋ค ๊ฑฐ์ผ. ๐
์ค์ํ ๊ฑด ์๋ฒฝํ๊ฒ ๋ค ์๊ณ ์์ํ๋ ค ํ์ง ๋ง๊ณ , ์ผ๋จ ์จ๋ณด๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ์ด์ผ.
๊ณต์ ์ฝ๋๋ฉ๋ถํฐ ์์ํด์ ์ค์ ํ๋ก์ ํธ์ ํ๋์ฉ ์ ์ฉํด๋ด. ๊ทธ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ํ์ต ๋ฐฉ๋ฒ์ด์ผ! ๐
Android Studio์ New Project ํ ํ๋ฆฟ์ ํ์ฉํด๋ด.
"Empty Compose Activity"๋ "Navigation Drawer Activity" ๊ฐ์ ํ ํ๋ฆฟ๋ค์ด Jetpack ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๊ธฐ๋ณธ์ผ๋ก ํฌํจํ๊ณ ์์ด์, ์ฒ์ ์์ํ ๋ ๊ตฌ์กฐ๋ฅผ ์ฐธ๊ณ ํ๊ธฐ ์ข์.
๊ทธ๋ฆฌ๊ณ Android Studio์ Gemini AI ๊ธฐ๋ฅ๋ Jetpack ์ฝ๋ ์์ฑ์ ํฐ ๋์์ด ๋ผ! ๐ค
์, ์ด์ Jetpack์ผ๋ก ๋ ๋น ๋ฅด๊ณ , ๋ ์์ ์ ์ด๊ณ , ๋ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์ด ์ฑ์ ๋ง๋ค์ด๋ด! ๐ช
๊ถ๊ธํ ์ ์ด ์์ผ๋ฉด ๊ตฌ๊ธ ๊ณต์ ๋ฌธ์๋ Android Developers YouTube ์ฑ๋์ ์ฐธ๊ณ ํด.
๊ทธ๋ฆฌ๊ณ ๊ฐ์ ๊ณ ๋ฏผ์ ํ๋ ๊ฐ๋ฐ์๋ค๊ณผ ์ง์์ ๋๋๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์๋ก์ ๊ฒฝํ์ ๊ณต์ ํ๋ ๊ฒ๋ ์ ๋ง ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.