Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 코틀린 트리거 버튼
- 코틀린 이미지저장 #파일저장
- livedata
- 스레드 #코루틴
- Room
- 안드로이드 스튜디오 애뮬레이터
- TwomonUSB
- #큐구조 #큐다운로드
- TowmonUSB 연결오류
- compse collectAsState
- var 와 val
- 라이브데이터 postValue
- 토글 험수
- withContext
- json 저장
- compse state
- Room 데이터베이스 업데이트
- 데이터바인딩
- mutable
- apk이름변경
Archives
- Today
- Total
EnjoyLife
mvvm 기반의 앱 폴더 구조 본문
안드로이드앱에서 주로 사용하는 네트워크 통신,db 기능을 기반으로 공통적으로 제가 사용하는 패턴입니다.
app/
├── data/
│ ├── local/
│ │ ├── SampleEntity.kt // Room 엔티티
│ │ ├── SampleDao.kt // Room DAO
│ │ └── AppDatabase.kt // Room Database (싱글톤)
│ └── remote/
│ ├── ApiService.kt // Retrofit API 인터페이스 (독립적 구현)
│ └── SampleData.kt // 네트워크 데이터 모델
├── repository/
│ └── SampleRepository.kt // Repository (API + Room, safeApiCall 적용)
├── ui/
│ ├── viewmodel/
│ │ ├── SampleViewModel.kt // ViewModel (Flow, LiveData 변환)
│ │ └── SampleViewModelFactory.kt // ViewModelProvider.Factory
│ └── MainActivity.kt // 액티비티 예제 (ViewModel 사용)
├── di/
│ └── InjectUtil.kt // 의존성 주입을 위한 유틸리티
└── utils/
└── ApiHelper.kt // safeApiCall 및 ResultWrapper 정의
1. API 호출 안전 처리 (ApiHelper.kt)
// utils/ApiHelper.kt
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
suspend fun <T> safeApiCall(
dispatcher: CoroutineDispatcher,
apiCall: suspend () -> T
): ResultWrapper<T> {
return withContext(dispatcher) {
try {
ResultWrapper.Success(apiCall.invoke())
} catch (throwable: Throwable) {
ResultWrapper.Error(throwable)
}
}
}
sealed class ResultWrapper<out T> {
data class Success<out T>(val value: T): ResultWrapper<T>()
data class Error(val value: Throwable): ResultWrapper<Nothing>()
}
2. 데이터 계층 (Room & Retrofit)
Room 구성
SampleEntity.kt
// data/local/SampleEntity.kt
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "sample_table")
data class SampleEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String
)
SampleDao.kt
// data/local/SampleDao.kt
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface SampleDao {
@Query("SELECT * FROM sample_table")
fun getAllSamples(): Flow<List<SampleEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSample(sample: SampleEntity)
}
AppDatabase.kt
// data/local/AppDatabase.kt
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [SampleEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun sampleDao(): SampleDao
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,AppDatabase::class.java,"app_database")
.build().also { INSTANCE = it }
}
}
}
3. Retrofit 구성 – ApiService 독립적 구현
ApiService.kt
// data/remote/ApiService.kt
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import java.util.concurrent.TimeUnit
import timber.log.Timber
interface ApiService {
companion object {
@JvmStatic
fun create(): ApiService {
val httpClient = OkHttpClient.Builder().apply {
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
writeTimeout(30, TimeUnit.SECONDS)
addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("x-api-key", "ZVJN9H5Z0JSUKDNMJ8JEITND4NPUS35H")
.build()
Log.e("ApiService").e("Request: ${request}")
chain.proceed(request)
}
}
val retrofit = Retrofit.Builder()
.baseUrl(AddrConstants.BASE)
.client(httpClient.build())
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
}
@FormUrlEncoded
@POST(AddrConstants.LOGIN)
suspend fun login(
@Field("id") id: String,
@Field("password") password: String
): Login
@FormUrlEncoded
@POST(AddrConstants.LOGOUT)
suspend fun logout(
@Field("id") id: String,
@Field("password") password: String,
): Logout
}
SampleData.kt
// data/remote/SampleData.kt
data class SampleData(
val id: Int,
val password: String
)
3.1 Repository (safeApiCall 적용)
SampleRepository.kt
// repository/SampleRepository.kt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
class SampleRepository(
private val sampleDao: SampleDao
) {
// 외부에서 직접 접근 가능한 ApiService 인스턴스 (싱글톤 형태)
private val apiService = ApiService.create()
// Room 데이터 Flow 제공
val samples: Flow<List<SampleEntity>> = sampleDao.getAllSamples()
// 네트워크 데이터를 safeApiCall을 통해 가져와 DB에 저장
suspend fun refreshSamples() {
when (val result = safeApiCall(Dispatchers.IO) { apiService.login("value1", "value2", "value3") }) {
is ResultWrapper.Success -> {
// 성공 시 받아온 데이터를 활용 (예시: Login API 응답)
// 실제 프로젝트에 맞게 데이터를 처리하세요.
}
is ResultWrapper.Error -> {
// 오류 발생 시 로그 출력 또는 사용자 알림 등 적절한 처리를 합니다.
// 예: Log.e("SampleRepository", "API 호출 오류", result.value)
}
}
}
}
4. ViewModel 및 Factory
SampleViewModel.kt
// ui/viewmodel/SampleViewModel.kt
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class SampleViewModel(private val repository: SampleRepository) : ViewModel() {
// Repository의 Flow를 LiveData로 변환하여 UI에 제공
val samples: LiveData<List<SampleEntity>> = repository.samples.asLiveData()
init {
viewModelScope.launch {
repository.refreshSamples()
}
}
}
SampleViewModelFactory.kt
// ui/viewmodel/SampleViewModelFactory.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class SampleViewModelFactory(private val repository: SampleRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SampleViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return SampleViewModel(repository) as T
}
throw IllegalArgumentException("알 수 없는 ViewModel 클래스")
}
}
5. 의존성 주입 (InjectUtil)
InjectUtil.kt
// di/InjectUtil.kt
import android.content.Context
object InjectUtil {
// AppDatabase 인스턴스 제공
private fun provideAppDatabase(context: Context) = AppDatabase.getInstance(context)
// Repository 인스턴스 제공 (SampleRepository는 내부에서 ApiService를 생성함)
fun provideSampleRepository(context: Context): SampleRepository {
val database = provideAppDatabase(context)
return SampleRepository(
sampleDao = database.sampleDao()
)
}
// ViewModel Factory 인스턴스 제공
fun provideSampleViewModelFactory(context: Context): SampleViewModelFactory {
return SampleViewModelFactory(provideSampleRepository(context))
}
}
6. 액티비티에서 ViewModel 사용 예제
MainActivity.kt
// ui/MainActivity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
class MainActivity : AppCompatActivity() {
private lateinit var sampleViewModel: SampleViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// InjectUtil을 통해 의존성 주입 및 ViewModel 초기화
sampleViewModel = ViewModelProvider(
this,
InjectUtil.provideSampleViewModelFactory(this)
).get(SampleViewModel::class.java)
// LiveData 관찰하여 UI 업데이트
sampleViewModel.samples.observe(this, Observer { sampleList ->
// RecyclerView 어댑터 등 UI 업데이트 처리
})
}
}
7. Gradle 의존성(모듈:app)
dependencies {
// Room
implementation "androidx.room:room-runtime:2.5.0"
kapt "androidx.room:room-compiler:2.5.0"
implementation "androidx.room:room-ktx:2.5.0"
// Retrofit 및 Gson
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
// 코루틴 및 Flow
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
// Lifecycle (ViewModel, LiveData)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
}
8.기능 요약
사용 방법 요약
- 프로젝트 생성 및 패키지 구성
위와 같은 폴더 구조대로 data, repository, ui, di, utils 등의 패키지를 생성하고 파일들을 추가 - 각 계층 구현
- 데이터 계층: Room 엔티티/DAO/Database와 Retrofit ApiService 및 데이터 모델을 구현
- 유틸: safeApiCall 및 ResultWrapper를 ApiHelper.kt에 작성
- Repository: 네트워크 통신 시 safeApiCall을 활용하여 데이터를 가져오고 Room에 저장하는 로직 구현
- ViewModel: Repository의 Flow를 LiveData로 변환하고, 초기 데이터 로딩 처리
- DI: InjectUtil을 통해 필요한 의존성을 제공
- ViewModel 사용
액티비티나 다이얼로그에서 InjectUtil로 제공받은 ViewModelFactory를 사용해 ViewModel을 초기화
이 템플릿을 기반으로 하면 어떤 코틀린 앱 프로젝트에서 네트워크, Room, MVVM, Flow, 의존성 주입, 그리고 API 오류 처리(safeApiCall)를 모두 갖춘 기본 구조를 빠르게 작성하면 좋을 듯 합니다.
'안드로이드 개발 > 개발팁' 카테고리의 다른 글
아직도 스레드를 아직도 사용하는 개발자들에게 고함 (0) | 2025.03.27 |
---|---|
Room 프로세스 (0) | 2024.12.10 |
Queue 구조로 다운로드 하기 (2) | 2024.11.05 |
var 와 MutableList 사용이 헷갈릴 경우 (3) | 2024.09.28 |
계속 잊어버리는 onActivityResult 사용법 (0) | 2024.07.10 |